From 33c401813d5397b2e18b65934b07c9f50a51498f Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 26 Mar 2026 16:45:26 -0400 Subject: [PATCH 01/47] Adding the diagnostic benchmark --- benchmark/CMakeLists.txt | 1 + .../bench_CQRRT_linops/CQRRT_orth_gap.cc | 617 ++++++++++++++++++ 2 files changed, 618 insertions(+) create mode 100644 benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 6aa7749d1..0f74bcb89 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -158,6 +158,7 @@ set( ) add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_orth_gap CXX_SOURCES bench_CQRRT_linops/CQRRT_orth_gap.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc b/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc new file mode 100644 index 000000000..9f7efc956 --- /dev/null +++ b/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc @@ -0,0 +1,617 @@ +// CQRRT orthogonality gap benchmark: CQRRT_linop vs CQRRT_expl +// +// Compares the two CQRRT implementations on the same matrix and reports +// the orthogonality gap. Four modes: +// +// generate — synthetic sparse matrix with controlled condition number +// file — read a Matrix Market file from disk +// diag — step-by-step diagnostic on a Matrix Market file +// diag_gen — step-by-step diagnostic on a synthetic matrix +// +// The diagnostic modes manually execute both algorithms step by step, +// comparing intermediate results (sketch, QR, preconditioned product, Gram, +// Cholesky, final R) to pinpoint where numerical divergence occurs. +// All modes write CSV output to the current working directory. +// +// This benchmark lives on the paper branch (spring-2026-wip) and is not +// intended for inclusion in the main RandLAPACK library. +// +// Usage: +// ./CQRRT_orth_gap generate [nnz] [block_size] +// ./CQRRT_orth_gap file [nnz] [block_size] [compute_cond] +// ./CQRRT_orth_gap diag [nnz] +// ./CQRRT_orth_gap diag_gen [nnz] + +#include "RandLAPACK.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "rl_gen.hh" + +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif + +// Extras utilities for Matrix Market I/O +#include "../../extras/misc/ext_util.hh" +#include "RandLAPACK/testing/rl_test_utils.hh" + +// Linops algorithms +#include "rl_cqrrt_linops.hh" + +using std::chrono::steady_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + +// Compute Q = A * R^{-1} via materialize + trsm (backward stable). +template +static void compute_Q_from_R( + GLO& A_op, T* R, int64_t ldr, + T* Q_out, int64_t m, int64_t n) { + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); + delete[] Eye; + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); +} + +// Compute condition number via SVD. +template +static T compute_condition_number(T* A, int64_t m, int64_t n) { + std::vector A_copy(m * n); + std::copy(A, A + m * n, A_copy.data()); + std::vector sigma(n); + lapack::gesdd(lapack::Job::NoVec, m, n, A_copy.data(), m, sigma.data(), + nullptr, 1, nullptr, 1); + return sigma[0] / sigma[n - 1]; +} + +struct RunResult { + double linop_orth; + double expl_orth; + double gap_ratio; + long linop_time_us; + long expl_time_us; +}; + +// Relative difference between two buffers: ||A - B||_F / ||A||_F +template +static T rel_diff(const T* A, const T* B, int64_t len) { + T norm_diff = 0, norm_A = 0; + for (int64_t i = 0; i < len; ++i) { + T d = A[i] - B[i]; + norm_diff += d * d; + norm_A += A[i] * A[i]; + } + return std::sqrt(norm_diff) / std::sqrt(norm_A); +} + +// Step-by-step diagnostic: manually runs both algorithms and compares intermediates. +template +static void run_diagnostic( + RandLAPACK::linops::SparseLinOp>& A_linop, + int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, + RandBLAS::RNGState& state) { + + using RandBLAS::RNGState; + int64_t d = (int64_t)std::ceil(d_factor * n); + + printf("\n=== Step-by-step Diagnostic ===\n"); + printf(" m=%ld, n=%ld, d=%ld, sketch_nnz=%ld\n\n", m, n, d, sketch_nnz); + + // ----- Step 0: Materialize operator ----- + std::vector A_dense(m * n, 0.0); + { + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); + delete[] Eye; + } + + // ----- Step 1: Sketch S*A ----- + // Both use same RNG state → same S + auto state_linop = state; + auto state_expl = state; + + // Linop sketch: A_linop(Side::Right, S) → spgemm + RandBLAS::SparseDist Dl(d, m, sketch_nnz, RandBLAS::Axis::Short); + RandBLAS::SparseSkOp S_linop(Dl, state_linop); + std::vector Ahat_linop(d * n, 0.0); + A_linop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S_linop, (T)0.0, Ahat_linop.data(), d); + + // Expl sketch: sketch_general(S, A_dense) → left_spmm + RandBLAS::SparseDist De(d, m, sketch_nnz, RandBLAS::Axis::Short); + RandBLAS::SparseSkOp S_expl(De, state_expl); + std::vector Ahat_expl(d * n, 0.0); + RandBLAS::sketch_general(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S_expl, A_dense.data(), m, + (T)0.0, Ahat_expl.data(), d); + + double rd_sketch = (double)rel_diff(Ahat_linop.data(), Ahat_expl.data(), d * n); + printf(" %-40s %12.3e\n", "Step 1: Sketch ||Ahat_l - Ahat_e||/||Ahat_l||", rd_sketch); + + // ----- Step 2: QR of sketch → R_sk ----- + std::vector R_sk_linop(n * n, 0.0); + std::vector R_sk_expl(n * n, 0.0); + { + // QR on linop sketch + std::vector tau(n); + lapack::geqrf(d, n, Ahat_linop.data(), d, tau.data()); + // Extract R + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk_linop[i + j * n] = Ahat_linop[i + j * d]; + } + { + // QR on expl sketch + std::vector tau(n); + lapack::geqrf(d, n, Ahat_expl.data(), d, tau.data()); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk_expl[i + j * n] = Ahat_expl[i + j * d]; + } + + double rd_qr = (double)rel_diff(R_sk_linop.data(), R_sk_expl.data(), n * n); + printf(" %-40s %12.3e\n", "Step 2: QR ||R_sk_l - R_sk_e||/||R_sk_l||", rd_qr); + + // ----- Step 3 (linop only): Explicit inverse R_pre = R_sk^{-1} ----- + std::vector R_pre(n * n, 0.0); + RandLAPACK::util::eye(n, n, R_pre.data()); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_sk_linop.data(), n, R_pre.data(), n); + + // ----- Step 3: A_pre = A * R_pre (linop) vs A * R_sk^{-1} via TRSM (expl) ----- + // Linop: A_pre_l = A * R_pre (using the linop's explicit inverse) + std::vector A_pre_linop(m * n, 0.0); + A_linop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, R_pre.data(), n, (T)0.0, A_pre_linop.data(), m); + + // Expl: A_pre_e = A * R_sk^{-1} via in-place TRSM (backward stable, no explicit inverse) + std::vector A_pre_expl(A_dense.begin(), A_dense.end()); // copy + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, R_sk_expl.data(), n, A_pre_expl.data(), m); + + double rd_apre = (double)rel_diff(A_pre_linop.data(), A_pre_expl.data(), m * n); + printf(" %-40s %12.3e\n", "Step 3: A_pre ||Apre_l - Apre_e||/||Apre_l||", rd_apre); + + // ----- Step 5a (linop): Gram via A^T * A_pre then R_pre^T * ... ----- + std::vector G_linop(n * n, 0.0); + A_linop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_pre_linop.data(), m, (T)0.0, G_linop.data(), n); + // Apply R_pre^T on left: G = R_pre^T * (A^T * A_pre) + blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, + Diag::NonUnit, n, n, (T)1.0, R_pre.data(), n, G_linop.data(), n); + std::vector G_linop_final(G_linop.begin(), G_linop.end()); + + // Compute A^T * A_pre for BOTH paths via dense GEMM (apples-to-apples, no SYRK vs GEMM difference) + std::vector AtApre_linop(n * n, 0.0); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_dense.data(), m, A_pre_linop.data(), m, + (T)0.0, AtApre_linop.data(), n); + std::vector AtApre_expl(n * n, 0.0); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_dense.data(), m, A_pre_expl.data(), m, + (T)0.0, AtApre_expl.data(), n); + + double rd_atapre = (double)rel_diff(AtApre_linop.data(), AtApre_expl.data(), n * n); + printf(" %-40s %12.3e\n", "Step 4a: A^T*Apre ||l - e||/||l|| (GEMM)", rd_atapre); + + // ----- Step 5b: Full Gram: linop uses GEMM+TRMM, expl uses SYRK ----- + std::vector G_expl(n * n, 0.0); + blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, + n, m, (T)1.0, A_pre_expl.data(), m, (T)0.0, G_expl.data(), n); + // Fill lower triangle + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + G_expl[i + j * n] = G_expl[j + i * n]; + + // Also compare SYRK vs GEMM on the SAME A_pre_expl buffer (isolate SYRK effect) + std::vector G_expl_gemm(n * n, 0.0); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_pre_expl.data(), m, A_pre_expl.data(), m, + (T)0.0, G_expl_gemm.data(), n); + + double rd_gram = (double)rel_diff(G_linop_final.data(), G_expl.data(), n * n); + double rd_syrk_gemm = (double)rel_diff(G_expl.data(), G_expl_gemm.data(), n * n); + printf(" %-40s %12.3e\n", "Step 4b: Gram ||G_l - G_e||/||G_l||", rd_gram); + printf(" %-40s %12.3e\n", "Step 4c: SYRK vs GEMM on same Apre_e", rd_syrk_gemm); + + // ----- Step 5: Cholesky ----- + std::vector R_chol_linop(G_linop_final.begin(), G_linop_final.end()); + std::vector R_chol_expl(G_expl.begin(), G_expl.end()); + lapack::potrf(Uplo::Upper, n, R_chol_linop.data(), n); + lapack::potrf(Uplo::Upper, n, R_chol_expl.data(), n); + // Zero below diagonal + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) { + R_chol_linop[i + j * n] = 0; + R_chol_expl[i + j * n] = 0; + } + + double rd_chol = (double)rel_diff(R_chol_linop.data(), R_chol_expl.data(), n * n); + printf(" %-40s %12.3e\n", "Step 5: Cholesky ||Rc_l - Rc_e||/||Rc_l||", rd_chol); + + // ----- Step 6: Final R = R_chol * R_sk ----- + std::vector R_final_linop(n * n, 0.0); + std::vector R_final_expl(n * n, 0.0); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, n, n, (T)1.0, R_chol_linop.data(), n, R_sk_linop.data(), n, + (T)0.0, R_final_linop.data(), n); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, n, n, (T)1.0, R_chol_expl.data(), n, R_sk_expl.data(), n, + (T)0.0, R_final_expl.data(), n); + + double rd_finalR = (double)rel_diff(R_final_linop.data(), R_final_expl.data(), n * n); + printf(" %-40s %12.3e\n", "Step 6: Final R ||R_l - R_e||/||R_l||", rd_finalR); + + // ----- Step 7: Orthogonality of Q = A * R^{-1} ----- + std::vector Q(m * n); + compute_Q_from_R(A_linop, R_final_linop.data(), n, Q.data(), m, n); + T orth_linop = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); + + compute_Q_from_R(A_linop, R_final_expl.data(), n, Q.data(), m, n); + T orth_expl = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); + + printf(" %-40s linop=%.3e, expl=%.3e, gap=%.1fx\n", + "Step 7: Orthogonality", + (double)orth_linop, (double)orth_expl, (double)(orth_linop / orth_expl)); + printf("\n"); + + // Write diagnostic CSV + // Filename: diag_.csv in current directory + auto now = std::chrono::system_clock::now(); + auto t = std::chrono::system_clock::to_time_t(now); + char ts[32]; + std::strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", std::localtime(&t)); + + std::string csv_name = std::string("orth_gap_diag_") + ts + ".csv"; + std::ofstream csv(csv_name); + if (csv.is_open()) { + std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; + csv << "# CQRRT orth gap diagnostic\n"; + csv << "# precision=" << prec_name << ", m=" << m << ", n=" << n << ", d=" << d << ", sketch_nnz=" << sketch_nnz << "\n"; + csv << "step,description,rel_diff\n"; + csv << std::scientific << std::setprecision(6); + csv << "1,sketch," << rd_sketch << "\n"; + csv << "2,qr_R_sk," << rd_qr << "\n"; + csv << "3,A_pre," << rd_apre << "\n"; + csv << "4,gram," << rd_gram << "\n"; + csv << "5,cholesky," << rd_chol << "\n"; + csv << "6,final_R," << rd_finalR << "\n"; + csv << "7,orth_linop," << (double)orth_linop << "\n"; + csv << "7,orth_expl," << (double)orth_expl << "\n"; + csv << "7,orth_gap," << (double)(orth_linop / orth_expl) << "\n"; + csv.close(); + printf(" Diagnostic CSV: %s\n\n", csv_name.c_str()); + } +} + +template +static RunResult run_comparison( + RandLAPACK::linops::SparseLinOp>& A_linop, + int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, int64_t block_size, + RandBLAS::RNGState& state) { + + T tol = std::pow(std::numeric_limits::epsilon(), 0.85); + std::vector Q(m * n); + RunResult res; + + // --- CQRRT_linop --- + { + std::vector R(n * n, 0.0); + auto state_copy = state; + RandLAPACK::CQRRT_linops algo(true, tol, false); + algo.nnz = sketch_nnz; + algo.block_size = block_size; + + auto t0 = steady_clock::now(); + algo.call(A_linop, R.data(), n, d_factor, state_copy); + auto t1 = steady_clock::now(); + res.linop_time_us = duration_cast(t1 - t0).count(); + + compute_Q_from_R(A_linop, R.data(), n, Q.data(), m, n); + res.linop_orth = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); + } + + // --- CQRRT_expl (materialize + dense CQRRT) --- + { + // Materialize the operator + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + T* A_dense = new T[m * n](); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense, m); + delete[] Eye; + + std::vector R(n * n, 0.0); + auto state_copy = state; + RandLAPACK::CQRRT algo(true, tol); + algo.compute_Q = false; + algo.orthogonalization = false; + algo.nnz = sketch_nnz; + + auto t0 = steady_clock::now(); + algo.call(m, n, A_dense, m, R.data(), n, d_factor, state_copy); + auto t1 = steady_clock::now(); + res.expl_time_us = duration_cast(t1 - t0).count(); + + delete[] A_dense; + + compute_Q_from_R(A_linop, R.data(), n, Q.data(), m, n); + res.expl_orth = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); + } + + res.gap_ratio = res.linop_orth / res.expl_orth; + return res; +} + +template +static int run_generate_mode(int argc, char* argv[]) { + // ./CQRRT_orth_gap generate [sketch_nnz] [block_size] + if (argc < 10) { + std::cerr << "Usage: " << argv[0] + << " generate [sketch_nnz] [block_size]\n"; + return 1; + } + int64_t m = std::stol(argv[3]); + int64_t n = std::stol(argv[4]); + double cond_num = std::stod(argv[5]); + double density = std::stod(argv[6]); + double d_factor = std::stod(argv[7]); + int64_t num_runs = std::stol(argv[8]); + int64_t sketch_nnz = (argc >= 10) ? std::stol(argv[9]) : 4; + int64_t block_size = (argc >= 11) ? std::stol(argv[10]) : 0; + + printf("\n=== CQRRT Orthogonality Gap Benchmark (generate mode) ===\n"); + printf(" Matrix: %ld x %ld, kappa=%.2e, density=%.3f\n", m, n, cond_num, density); + printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", + d_factor, sketch_nnz, block_size, num_runs); +#ifdef _OPENMP + printf(" OpenMP threads: %d\n", omp_get_max_threads()); +#endif + printf("=========================================================\n\n"); + + auto state = RandBLAS::RNGState(); + auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, (T)cond_num, state, (T)density); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + + // Timestamp for CSV filenames + auto now_gen = std::chrono::system_clock::now(); + auto t_gen = std::chrono::system_clock::to_time_t(now_gen); + char ts_gen[32]; + std::strftime(ts_gen, sizeof(ts_gen), "%Y%m%d_%H%M%S", std::localtime(&t_gen)); + std::string csv_name = std::string("orth_gap_gen_") + ts_gen + ".csv"; + std::ofstream csv(csv_name); + if (csv.is_open()) { + std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; + csv << "# CQRRT orth gap benchmark (generate mode)\n"; + csv << "# precision=" << prec_name << ", m=" << m << ", n=" << n << ", kappa=" << cond_num + << ", density=" << density << ", d_factor=" << d_factor + << ", sketch_nnz=" << sketch_nnz << ", block_size=" << block_size << "\n"; + csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; + } + + printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", + "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); + printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", + "---", "-----------", "----------", "-------", "----------", "---------"); + + for (int64_t r = 0; r < num_runs; ++r) { + auto run_state = state; + auto res = run_comparison(A_linop, m, n, (T)d_factor, sketch_nnz, block_size, run_state); + printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", + r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); + if (csv.is_open()) { + csv << r << "," << std::scientific << std::setprecision(6) + << res.linop_orth << "," << res.expl_orth << "," + << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; + } + state = RandBLAS::RNGState(state.key.incr()); + } + if (csv.is_open()) { + csv.close(); + printf(" Results CSV: %s\n", csv_name.c_str()); + } + printf("\n"); + return 0; +} + +template +static int run_file_mode(int argc, char* argv[]) { + // ./CQRRT_orth_gap file [sketch_nnz] [block_size] [compute_cond] + if (argc < 6) { + std::cerr << "Usage: " << argv[0] + << " file [sketch_nnz] [block_size] [compute_cond]\n"; + return 1; + } + std::string mtx_path = argv[3]; + double d_factor = std::stod(argv[4]); + int64_t num_runs = std::stol(argv[5]); + int64_t sketch_nnz = (argc >= 7) ? std::stol(argv[6]) : 4; + int64_t block_size = (argc >= 8) ? std::stol(argv[7]) : 0; + bool compute_cond = (argc >= 9) ? (std::stol(argv[8]) != 0) : false; + + printf("\n=== CQRRT Orthogonality Gap Benchmark (file mode) ===\n"); + printf(" File: %s\n", mtx_path.c_str()); + printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", + d_factor, sketch_nnz, block_size, num_runs); +#ifdef _OPENMP + printf(" OpenMP threads: %d\n", omp_get_max_threads()); +#endif + printf("=====================================================\n\n"); + + // Read Matrix Market file + printf("Loading %s...", mtx_path.c_str()); + fflush(stdout); + auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); + int64_t m = A_coo.n_rows; + int64_t n = A_coo.n_cols; + printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); + + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + + if (compute_cond) { + printf("Computing condition number via SVD (%ld x %ld)...", m, n); + fflush(stdout); + std::vector A_dense(m * n, 0.0); + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); + delete[] Eye; + T kappa = compute_condition_number(A_dense.data(), m, n); + printf(" kappa = %.6e\n", (double)kappa); + } + + auto state = RandBLAS::RNGState(); + + // Extract matrix name from path for CSV + std::string mtx_name = mtx_path; + auto slash = mtx_name.rfind('/'); + if (slash != std::string::npos) mtx_name = mtx_name.substr(slash + 1); + auto dot = mtx_name.rfind('.'); + if (dot != std::string::npos) mtx_name = mtx_name.substr(0, dot); + + // Timestamp for CSV + auto now_file = std::chrono::system_clock::now(); + auto t_file = std::chrono::system_clock::to_time_t(now_file); + char ts_file[32]; + std::strftime(ts_file, sizeof(ts_file), "%Y%m%d_%H%M%S", std::localtime(&t_file)); + std::string csv_name = std::string("orth_gap_") + mtx_name + "_" + ts_file + ".csv"; + std::ofstream csv(csv_name); + if (csv.is_open()) { + std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; + csv << "# CQRRT orth gap benchmark (file mode)\n"; + csv << "# precision=" << prec_name << ", file=" << mtx_path << ", m=" << m << ", n=" << n + << ", d_factor=" << d_factor << ", sketch_nnz=" << sketch_nnz + << ", block_size=" << block_size << "\n"; + csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; + } + + printf("\n %-6s %-14s %-14s %-10s %-12s %-12s\n", + "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); + printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", + "---", "-----------", "----------", "-------", "----------", "---------"); + + for (int64_t r = 0; r < num_runs; ++r) { + auto run_state = state; + auto res = run_comparison(A_linop, m, n, (T)d_factor, sketch_nnz, block_size, run_state); + printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", + r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); + if (csv.is_open()) { + csv << r << "," << std::scientific << std::setprecision(6) + << res.linop_orth << "," << res.expl_orth << "," + << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; + } + state = RandBLAS::RNGState(state.key.incr()); + } + if (csv.is_open()) { + csv.close(); + printf(" Results CSV: %s\n", csv_name.c_str()); + } + printf("\n"); + return 0; +} + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cerr << "Usage:\n" + << " " << argv[0] << " generate [sketch_nnz] [block_size]\n" + << " " << argv[0] << " file [sketch_nnz] [block_size] [compute_cond]\n"; + return 1; + } + + std::string precision = argv[1]; + std::string mode = argv[2]; + + // Diagnostic mode: step-by-step comparison on a file + if (mode == "diag") { + // ./CQRRT_orth_gap diag [sketch_nnz] + if (argc < 5) { + std::cerr << "Usage: " << argv[0] << " diag [sketch_nnz]\n"; + return 1; + } + std::string mtx_path = argv[3]; + double d_factor = std::stod(argv[4]); + int64_t sketch_nnz = (argc >= 6) ? std::stol(argv[5]) : 4; + + printf("Loading %s...", mtx_path.c_str()); fflush(stdout); + if (precision == "double") { + auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); + int64_t m = A_coo.n_rows, n = A_coo.n_cols; + printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + auto state = RandBLAS::RNGState(); + run_diagnostic(A_linop, m, n, d_factor, sketch_nnz, state); + } else { + auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); + int64_t m = A_coo.n_rows, n = A_coo.n_cols; + printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + auto state = RandBLAS::RNGState(); + run_diagnostic(A_linop, m, n, (float)d_factor, sketch_nnz, state); + } + return 0; + } + + // Diagnostic mode on synthetic matrix + if (mode == "diag_gen") { + // ./CQRRT_orth_gap diag_gen [sketch_nnz] + if (argc < 8) { + std::cerr << "Usage: " << argv[0] << " diag_gen [sketch_nnz]\n"; + return 1; + } + int64_t m = std::stol(argv[3]); + int64_t n = std::stol(argv[4]); + double cond_num = std::stod(argv[5]); + double density = std::stod(argv[6]); + double d_factor = std::stod(argv[7]); + int64_t sketch_nnz = (argc >= 9) ? std::stol(argv[8]) : 4; + + printf("Generating %ld x %ld matrix, kappa=%.2e, density=%.3f...", m, n, cond_num, density); + fflush(stdout); + auto state = RandBLAS::RNGState(); + if (precision == "double") { + auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, cond_num, state, density); + printf(" done (nnz=%ld)\n", A_coo.nnz); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + run_diagnostic(A_linop, m, n, d_factor, sketch_nnz, state); + } else { + auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, (float)cond_num, state, (float)density); + printf(" done (nnz=%ld)\n", A_coo.nnz); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + run_diagnostic(A_linop, m, n, (float)d_factor, sketch_nnz, state); + } + return 0; + } + + if (precision == "double") { + if (mode == "generate") return run_generate_mode(argc, argv); + if (mode == "file") return run_file_mode(argc, argv); + } else if (precision == "float") { + if (mode == "generate") return run_generate_mode(argc, argv); + if (mode == "file") return run_file_mode(argc, argv); + } + + std::cerr << "Unknown precision '" << precision << "' or mode '" << mode << "'\n"; + return 1; +} From ab711218eb4122b735a791d6381234a0821b4d16 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 2 Apr 2026 11:00:40 -0400 Subject: [PATCH 02/47] Support for composite FEM operator --- .../bench_CQRRT_linops/CQRRT_orth_gap.cc | 123 ++++++++++++++++-- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc b/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc index 9f7efc956..38c2c8392 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc @@ -1,12 +1,13 @@ // CQRRT orthogonality gap benchmark: CQRRT_linop vs CQRRT_expl // // Compares the two CQRRT implementations on the same matrix and reports -// the orthogonality gap. Four modes: +// the orthogonality gap. Five modes: // -// generate — synthetic sparse matrix with controlled condition number -// file — read a Matrix Market file from disk -// diag — step-by-step diagnostic on a Matrix Market file -// diag_gen — step-by-step diagnostic on a synthetic matrix +// generate — synthetic sparse matrix with controlled condition number +// file — read a Matrix Market file from disk +// composite — composite operator from K.mtx + V.mtx → L^{-1}V +// diag — step-by-step diagnostic on a Matrix Market file +// diag_gen — step-by-step diagnostic on a synthetic matrix // // The diagnostic modes manually execute both algorithms step by step, // comparing intermediate results (sketch, QR, preconditioned product, Gram, @@ -19,6 +20,7 @@ // Usage: // ./CQRRT_orth_gap generate [nnz] [block_size] // ./CQRRT_orth_gap file [nnz] [block_size] [compute_cond] +// ./CQRRT_orth_gap composite [nnz] [block_size] // ./CQRRT_orth_gap diag [nnz] // ./CQRRT_orth_gap diag_gen [nnz] @@ -28,6 +30,8 @@ #include "rl_gen.hh" #include +#include +#include #include #include #include @@ -35,12 +39,14 @@ #include #endif -// Extras utilities for Matrix Market I/O +// Extras utilities for Matrix Market I/O and CholSolver #include "../../extras/misc/ext_util.hh" +#include "../../extras/linops/ext_cholsolver_linop.hh" #include "RandLAPACK/testing/rl_test_utils.hh" // Linops algorithms #include "rl_cqrrt_linops.hh" +#include "rl_composite_linop.hh" using std::chrono::steady_clock; using std::chrono::duration_cast; @@ -92,9 +98,9 @@ static T rel_diff(const T* A, const T* B, int64_t len) { } // Step-by-step diagnostic: manually runs both algorithms and compares intermediates. -template +template static void run_diagnostic( - RandLAPACK::linops::SparseLinOp>& A_linop, + GLO& A_linop, int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, RandBLAS::RNGState& state) { @@ -293,9 +299,9 @@ static void run_diagnostic( } } -template +template static RunResult run_comparison( - RandLAPACK::linops::SparseLinOp>& A_linop, + GLO& A_linop, int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, int64_t block_size, RandBLAS::RNGState& state) { @@ -528,7 +534,10 @@ int main(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage:\n" << " " << argv[0] << " generate [sketch_nnz] [block_size]\n" - << " " << argv[0] << " file [sketch_nnz] [block_size] [compute_cond]\n"; + << " " << argv[0] << " file [sketch_nnz] [block_size] [compute_cond]\n" + << " " << argv[0] << " composite [sketch_nnz] [block_size]\n" + << " " << argv[0] << " diag [sketch_nnz]\n" + << " " << argv[0] << " diag_gen [sketch_nnz]\n"; return 1; } @@ -604,6 +613,98 @@ int main(int argc, char* argv[]) { return 0; } + // Composite mode: K.mtx + V.mtx → CholSolver(K, half_solve) ∘ Sparse(V) = L^{-1}V + if (mode == "composite") { + // ./CQRRT_orth_gap composite [nnz] [block_size] + if (argc < 7) { + std::cerr << "Usage: " << argv[0] + << " composite [sketch_nnz] [block_size]\n"; + return 1; + } + std::string K_file = argv[3]; + std::string V_file = argv[4]; + double d_factor = std::stod(argv[5]); + int64_t num_runs = std::stol(argv[6]); + int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; + int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 256; + + if (precision == "double") { + using T = double; + + printf("\n=== CQRRT Orthogonality Gap Benchmark (composite mode) ===\n"); + printf(" K file: %s\n V file: %s\n", K_file.c_str(), V_file.c_str()); + printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", + d_factor, sketch_nnz, block_size, num_runs); +#ifdef _OPENMP + printf(" OpenMP threads: %d\n", omp_get_max_threads()); +#endif + printf("==========================================================\n\n"); + + // Load V + printf("Loading V from %s...", V_file.c_str()); fflush(stdout); + auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); + int64_t m = V_coo.n_rows, n = V_coo.n_cols; + printf(" done (%ld x %ld, nnz=%ld)\n", m, n, V_coo.nnz); + + RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); + RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); + + // Build CholSolver for K (half_solve = true → L^{-1}) + printf("Factorizing K = LL^T from %s...", K_file.c_str()); fflush(stdout); + RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); + L_inv_op.factorize(); + printf(" done\n"); + + // Composite operator: L^{-1} * V (CTAD deduces template args) + RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); + + printf("Composite operator L^{-1}V: %ld x %ld\n\n", m, n); + + auto state = RandBLAS::RNGState(); + + // CSV output + auto now = std::chrono::system_clock::now(); + auto t = std::chrono::system_clock::to_time_t(now); + char ts[32]; + std::strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", std::localtime(&t)); + std::string csv_name = std::string("orth_gap_composite_") + ts + ".csv"; + std::ofstream csv(csv_name); + if (csv.is_open()) { + csv << "# CQRRT orth gap benchmark (composite mode)\n"; + csv << "# K_file=" << K_file << ", V_file=" << V_file + << ", m=" << m << ", n=" << n + << ", d_factor=" << d_factor << ", sketch_nnz=" << sketch_nnz + << ", block_size=" << block_size << "\n"; + csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; + } + + printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", + "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); + printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", + "---", "-----------", "----------", "-------", "----------", "---------"); + + for (int64_t r = 0; r < num_runs; ++r) { + auto run_state = state; + auto res = run_comparison(LiV_op, m, n, (T)d_factor, sketch_nnz, block_size, run_state); + printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", + r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); + if (csv.is_open()) { + csv << r << "," << std::scientific << std::setprecision(6) + << res.linop_orth << "," << res.expl_orth << "," + << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; + } + state = RandBLAS::RNGState(state.key.incr()); + } + if (csv.is_open()) { csv.close(); printf(" Results CSV: %s\n", csv_name.c_str()); } + printf("\n"); + } else { + std::cerr << "Composite mode only supports double precision\n"; + return 1; + } + return 0; + } + if (precision == "double") { if (mode == "generate") return run_generate_mode(argc, argv); if (mode == "file") return run_file_mode(argc, argv); From 383acd11819f730ba169fe2382d6e7ce2f7080b5 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Fri, 3 Apr 2026 15:01:22 -0400 Subject: [PATCH 03/47] Adding a new benchmark --- benchmark/CMakeLists.txt | 7 +- .../CQRRT_linop_composite_applications_v2.cc | 790 ++++++++++++++++++ 2 files changed, 794 insertions(+), 3 deletions(-) create mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 0f74bcb89..9dfc44735 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -156,9 +156,10 @@ set( Eigen3::Eigen fast_matrix_market ) -add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -add_benchmark(NAME CQRRT_orth_gap CXX_SOURCES bench_CQRRT_linops/CQRRT_orth_gap.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_composite_applications_v2 CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_orth_gap CXX_SOURCES bench_CQRRT_linops/CQRRT_orth_gap.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc new file mode 100644 index 000000000..b5914b576 --- /dev/null +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc @@ -0,0 +1,790 @@ +// Generalized SVD / Generalized LS benchmark +// +// Pipeline: +// 1. Load K.mtx (m x m SPD) and V.mtx (m x n sparse) +// 2. Cholesky factorize K = LL^T, create L^{-1} operator (half_solve=true) +// 3. Create composite operator: CompositeOperator(L_inv_op, V_op) = L^{-1}V +// 4. Run Q-less QR on L^{-1}V via CQRRT_linops, CholQR_linops, sCholQR3_linops +// 5. Application (a): Generalized LS — solve min_x ||Vx - b||_{K^{-1}} +// 6. Application (b): Generalized singular values — SVD of R +// 7. Application (c): Generalized singular vectors — full SVD of R +// +// Usage: +// ./GSVD_benchmark +// [sketch_nnz] [block_size] [skip_apps] [compute_cond] + +#include "RandLAPACK.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "rl_gen.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif +#include +#include + +// Extras utilities (Eigen-dependent) +#include "../../extras/misc/ext_util.hh" +#include "../../extras/linops/ext_cholsolver_linop.hh" +#include "RandLAPACK/testing/rl_test_utils.hh" + +// Linops algorithms (now in main RandLAPACK) +#include "rl_cqrrt_linops.hh" +#include "rl_cholqr_linops.hh" +#include "rl_scholqr3_linops.hh" +#include "RandLAPACK/testing/rl_memory_tracker.hh" + +using std::chrono::steady_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + +// ============================================================================ +// Result struct +// ============================================================================ + +template +struct gsvd_result { + int64_t m, n; + int64_t run_idx; + std::string alg_name; + + // Cholesky factorization time (shared, measured once) + long chol_time_us; + + // Q-less QR time + long qr_time_us; + + // Orthogonality of Q = (L^{-1}V) R^{-1} + T orth_error; + bool is_orthonormal; + int64_t max_orth_cols; + + // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 + double r_backward_error; + + // Orthogonality computed with upcast reconstruction (float->double or double->long double) + double orth_error_upcast; + + // Application (a): Generalized LS + long app_a_time_us; // Post-processing time only + T ls_rel_error; // ||x - x_true|| / ||x_true|| + + // Application (b): Generalized singular values + long app_b_time_us; // SVD of R time + + // Application (c): Generalized singular vectors + long app_c_time_us; // Full SVD of R + V_R orthogonality check + T right_svec_orth_error; // ||V_R^T V_R - I||_F / sqrt(n) + + // Totals (QR + application post-processing) + long total_a_time_us; + long total_b_time_us; + long total_c_time_us; + + // QR timing breakdown (from algo.times[]) + std::vector qr_breakdown; + + // Memory tracking + long peak_rss_kb; // Peak RSS increase during QR call (KB) + long analytical_kb; // Analytical peak working memory (KB) +}; + +// Compute Q = A * R^{-1} uniformly for all algorithms +template +static void compute_Q_from_R( + GLO& A_op, T* R, int64_t ldr, + T* Q_out, int64_t m, int64_t n) { + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); + delete[] Eye; + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, + blas::Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); +} + +// Compute A^T A via blocked linop calls. Peak memory: O(m*b + n^2). +// No m x n materialization needed. +template +static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { + std::fill(AtA, AtA + n * n, (T)0.0); + std::vector E_block(n * b, 0.0); // n x b identity block + std::vector A_block(m * b, 0.0); // m x b = A * E_block + std::vector AtA_block(n * b, 0.0); // n x b = A^T * A_block + + for (int64_t j0 = 0; j0 < n; j0 += b) { + int64_t bk = std::min(b, n - j0); + + // E_block = I[:, j0:j0+bk] + std::fill(E_block.begin(), E_block.end(), (T)0.0); + for (int64_t j = 0; j < bk; ++j) + E_block[(j0 + j) + j * n] = (T)1.0; + + // A_block = A * E_block (m x bk) + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, bk, n, (T)1.0, E_block.data(), n, (T)0.0, A_block.data(), m); + + // AtA[:, j0:j0+bk] = A^T * A_block (n x bk) + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); + + // Copy into AtA columns j0..j0+bk + for (int64_t j = 0; j < bk; ++j) + for (int64_t i = 0; i < n; ++i) + AtA[i + (j0 + j) * n] = AtA_block[i + j * n]; + } +} + +// Compute R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 +// Uses blocked linop calls — peak memory O(m*b + n^2), no m x n materialization. +template +static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { + int64_t b = (block_size > 0) ? block_size : 256; + + // A^T A via blocked linop (n x n) + std::vector AtA(n * n, 0.0); + compute_AtA_blocked(A_op, m, n, AtA.data(), b); + + // R^T R (n x n) + std::vector RtR(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + RtR[i + j * n] = RtR[j + i * n]; + + // ||A||_F^2 = trace(A^T A) + T norm_A_sq = 0; + for (int64_t i = 0; i < n; ++i) + norm_A_sq += AtA[i + i * n]; + + // ||A^T A - R^T R||_F + T diff_norm_sq = 0; + for (int64_t i = 0; i < n * n; ++i) { + T d = AtA[i] - RtR[i]; + diff_norm_sq += d * d; + } + + return (double)(std::sqrt(diff_norm_sq) / (norm_A_sq)); +} + +// Compute orthogonality with upcast reconstruction. +// Algorithm runs in precision T, Q = A R^{-1} computed in precision U (one level up). +// float -> double, double -> long double. Uses Eigen for TRSM in precision U. +// If A_dense is non-null, uses it directly; otherwise materializes via linop. +template +static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, + const T* A_dense = nullptr) { + const T* A_ptr; + std::vector A_buf; + if (A_dense) { + A_ptr = A_dense; + } else { + A_buf.resize(m * n); + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_buf.data(), m); + delete[] Eye; + A_ptr = A_buf.data(); + } + + // Upcast A and R to precision U + std::vector A_U(m * n), R_U(n * n); + for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; + for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; + + // Q = A * R^{-1} via Eigen triangular solve in precision U + Eigen::Map> + A_map(A_U.data(), m, n); + Eigen::Map> + R_map(R_U.data(), n, n); + + // Solve R^T * Q^T = A^T for Q^T + Eigen::Matrix Qt = + R_map.transpose().template triangularView().solve(A_map.transpose()); + + // Compute orthogonality: ||Q^T Q - I||_F / sqrt(n) + Eigen::Matrix QtQ = Qt * Qt.transpose(); // n x n + for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; + return (double)(QtQ.norm() / std::sqrt((U)n)); +} + +// ============================================================================ +// Application (a): Generalized Least Squares +// min_x ||Vx - b||_{K^{-1}} via R from QR of L^{-1}V +// +// Solution: x = R^{-1} R^{-T} V^T K^{-1} b +// Steps: c = K^{-1}b, d = V^T c, solve R^T y = d, solve Rx = y +// ============================================================================ + +template +static void app_generalized_ls( + RandLAPACK_extras::linops::CholSolverLinOp& K_inv_op, + VLinOp& V_op, + const T* R, int64_t ldr, int64_t n, + const T* b, int64_t m, + T* x, + long& app_time_us) +{ + auto start = steady_clock::now(); + + // Step 1: c = K^{-1} b (m x 1) + std::vector c(m, 0.0); + K_inv_op(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, m, (T)1.0, b, m, (T)0.0, c.data(), m); + + // Step 2: d = V^T c (n x 1) + std::vector d(n, 0.0); + V_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, 1, m, (T)1.0, c.data(), m, (T)0.0, d.data(), n); + + // Step 3: solve R^T y = d (n x 1) + std::copy(d.begin(), d.end(), x); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, + blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); + + // Step 4: solve R x = y (n x 1) + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, + blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); + + auto stop = steady_clock::now(); + app_time_us = duration_cast(stop - start).count(); +} + +// ============================================================================ +// Application (b): Generalized Singular Values +// SVD of R gives the generalized singular values of (V, K) +// ============================================================================ + +template +static void app_generalized_svals( + const T* R, int64_t ldr, int64_t n, + T* sigma, + long& app_time_us) +{ + auto start = steady_clock::now(); + + // Copy R to work buffer (gesdd destroys input) + std::vector R_copy(n * n); + lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); + + // SVD of n x n R: only singular values (no vectors) + std::vector dummy_U(1), dummy_Vt(1); + lapack::gesdd(lapack::Job::NoVec, n, n, R_copy.data(), n, + sigma, dummy_U.data(), 1, dummy_Vt.data(), 1); + + auto stop = steady_clock::now(); + app_time_us = duration_cast(stop - start).count(); +} + +// ============================================================================ +// Application (c): Generalized Singular Vectors +// Full SVD of R: R = U_R * Sigma * V_R^T +// Right generalized singular vectors = columns of V_R +// ============================================================================ + +template +static void app_generalized_svecs( + const T* R, int64_t ldr, int64_t n, + T* sigma, + T* V_R, // n x n right singular vectors + T* U_R, // n x n left singular vectors of R + T& right_svec_orth_error, + long& app_time_us) +{ + auto start = steady_clock::now(); + + // Copy R to work buffer + std::vector R_copy(n * n); + lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); + + // Full SVD of n x n R: R = U_R * Sigma * V_R^T + lapack::gesdd(lapack::Job::AllVec, n, n, R_copy.data(), n, + sigma, U_R, n, V_R, n); + + auto stop = steady_clock::now(); + app_time_us = duration_cast(stop - start).count(); + + // Verify right singular vector orthogonality: ||V_R^T V_R - I||_F / sqrt(n) + right_svec_orth_error = RandLAPACK::testing::orthogonality_error(V_R, n, n); +} + +// ============================================================================ +// CSV output +// ============================================================================ + +template +static void write_common_header_comments( + std::ofstream& out, int64_t m, int64_t n, int num_runs, + const std::string& K_file, const std::string& V_file, + T d_factor, int64_t sketch_nnz, int64_t block_size, + bool skip_apps, bool compute_cond) +{ + time_t now = time(nullptr); + out << "# Date: " << ctime(&now) + << "# Matrix dimensions: m=" << m << " n=" << n << "\n" + << "# Runs per algorithm: " << num_runs << "\n" +#ifdef _OPENMP + << "# OpenMP threads: " << omp_get_max_threads() << "\n" +#else + << "# OpenMP threads: 1\n" +#endif + << "# K_file: " << K_file << "\n" + << "# V_file: " << V_file << "\n" + << "# d_factor: " << d_factor << "\n" + << "# sketch_nnz: " << sketch_nnz << "\n" + << "# block_size: " << block_size << "\n" + << "# skip_apps: " << (skip_apps ? 1 : 0) << "\n" + << "# compute_cond: " << (compute_cond ? 1 : 0) << "\n"; +} + +template +static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_runs, + const std::string& K_file, const std::string& V_file, + T d_factor, int64_t sketch_nnz, int64_t block_size, + bool skip_apps, bool compute_cond) { + out << "# GSVD Benchmark results\n"; + write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast,max_orth_cols," + << "app_a_time_us,ls_rel_error," + << "app_b_time_us," + << "app_c_time_us,right_svec_orth_error," + << "total_a_time_us,total_b_time_us,total_c_time_us," + << "peak_rss_kb,analytical_kb\n"; +} + +template +static void write_csv_row(std::ofstream& out, const gsvd_result& r) { + out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name << "," + << r.chol_time_us << "," + << r.qr_time_us << "," + << std::scientific << std::setprecision(6) << r.orth_error << "," + << std::scientific << std::setprecision(6) << r.r_backward_error << "," + << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," + << r.max_orth_cols << "," + << r.app_a_time_us << "," + << std::scientific << std::setprecision(6) << r.ls_rel_error << "," + << r.app_b_time_us << "," + << r.app_c_time_us << "," + << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," + << r.total_a_time_us << "," << r.total_b_time_us << "," << r.total_c_time_us << "," + << r.peak_rss_kb << "," << r.analytical_kb + << "\n"; +} + +// ============================================================================ +// Breakdown CSV output +// ============================================================================ + +template +static void write_breakdown_csv( + const std::string& filename, + const std::vector>& results, + int64_t m, int64_t n, int num_runs, + const std::string& K_file, const std::string& V_file, + T d_factor, int64_t sketch_nnz, int64_t block_size, + bool skip_apps, bool compute_cond) +{ + std::ofstream out(filename); + out << "# GSVD Benchmark runtime breakdown\n"; + write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + out << "# Times are in microseconds\n"; + out << "# CQRRT_linop breakdown (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n"; + out << "# CholQR breakdown (6): alloc, fwd, adj, chol, rest, total\n"; + out << "# sCholQR3 breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total\n"; + out << "# sCholQR3_basic breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total\n"; + + // Column header: m, n, run, algorithm, then all breakdown times + // Max breakdown length is 18 (sCholQR3) + out << "m,n,run,algorithm"; + for (int i = 0; i < 18; ++i) out << ",t" << i; + out << "\n"; + + for (const auto& r : results) { + out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name; + for (size_t i = 0; i < r.qr_breakdown.size(); ++i) { + out << "," << r.qr_breakdown[i]; + } + // Pad with zeros if fewer than 18 columns + for (size_t i = r.qr_breakdown.size(); i < 18; ++i) { + out << ",0"; + } + out << "\n"; + } +} + +// ============================================================================ +// Console summary +// ============================================================================ + +template +static void print_summary(const std::string& alg_name, const std::vector>& results) { + printf("\n %s:\n", alg_name.c_str()); + for (const auto& r : results) { + printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, max_orth=%ld/%ld, QR=%ld us\n", + (long)r.run_idx, (double)r.orth_error, r.r_backward_error, (long)r.max_orth_cols, (long)r.n, r.qr_time_us); + if (r.orth_error_upcast > 0) + printf(" orth_upcast=%.2e\n", r.orth_error_upcast); + if (r.app_a_time_us > 0 || r.ls_rel_error > 0) { + printf(" LS_err=%.2e, App(a)=%ld us, App(b)=%ld us, App(c)=%ld us\n", + (double)r.ls_rel_error, r.app_a_time_us, r.app_b_time_us, r.app_c_time_us); + } + printf(" Memory: peak_RSS=%ld KB, predicted=%ld KB\n", + r.peak_rss_kb, r.analytical_kb); + } +} + +// ============================================================================ +// Main benchmark +// ============================================================================ + +template +int run_benchmark(int argc, char* argv[]) { + // Parse arguments + if (argc < 7) { + std::cerr << "Usage: " << argv[0] + << " " + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond]\n"; + return 1; + } + + std::string output_dir = argv[2]; + int64_t num_runs = std::stol(argv[3]); + std::string K_file = argv[4]; + std::string V_file = argv[5]; + T d_factor = std::stod(argv[6]); + int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; + int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; + bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; + bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; + bool run_expl = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; + bool upcast_orth = (argc >= 13) ? (std::stol(argv[12]) != 0) : false; + + std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; + std::cout << " K file: " << K_file << "\n"; + std::cout << " V file: " << V_file << "\n"; + std::cout << " d_factor: " << d_factor << "\n"; + std::cout << " sketch_nnz: " << sketch_nnz << "\n"; + std::cout << " block_size: " << block_size << "\n"; + std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; + std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; + std::cout << " run_expl: " << (run_expl ? "yes" : "no") << "\n"; + std::cout << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n"; + std::cout << " num_runs: " << num_runs << "\n"; +#ifdef _OPENMP + std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; +#else + std::cout << " OpenMP threads: 1\n\n"; +#endif + + // ================================================================ + // Step 1: Load V from Matrix Market + // ================================================================ + std::cout << "Loading V from " << V_file << "... " << std::flush; + auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); + int64_t m = V_coo.n_rows; + int64_t n = V_coo.n_cols; + RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); + RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); + std::cout << "done (" << m << " x " << n << ", nnz=" << V_coo.nnz << ")\n"; + + // ================================================================ + // Step 2: Create L^{-1} operator (half_solve=true) and K^{-1} operator + // ================================================================ + std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; + + RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); + auto chol_start = steady_clock::now(); + L_inv_op.factorize(); + auto chol_stop = steady_clock::now(); + long chol_time_us = duration_cast(chol_stop - chol_start).count(); + + // Also create full K^{-1} for App (a) — only needed when running apps + std::unique_ptr> K_inv_op_ptr; + if (!skip_apps) { + K_inv_op_ptr = std::make_unique>(K_file, /*half_solve=*/false); + K_inv_op_ptr->factorize(); + } + + std::cout << "done (" << chol_time_us << " us)\n"; + + // ================================================================ + // Step 3: Form composite operator L^{-1} * V + // ================================================================ + RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); + LiV_op.block_size = block_size; + std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n"; + + // Condition number diagnostic (materializes L^{-1}V, runs two SVDs) + if (compute_cond) { + RandLAPACK::testing::print_condition_diagnostics(LiV_op, "L^{-1}V"); + } + + // ================================================================ + // Step 4: Generate synthetic RHS: b = V * x_true (only when running apps) + // ================================================================ + RandBLAS::RNGState rng_state(42); + std::vector x_true(n); + std::vector b(m, 0.0); + T x_true_norm = 0.0; + if (!skip_apps) { + RandBLAS::DenseDist D(n, 1); + auto next_state = RandBLAS::fill_dense(D, x_true.data(), rng_state); + rng_state = next_state; + + V_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b.data(), m); + + x_true_norm = blas::nrm2(n, x_true.data(), 1); + std::cout << "Generated b = V * x_true (||x_true|| = " << x_true_norm << ")\n"; + } + std::cout << "\n"; + + // ================================================================ + // Prepare RNG states for each run + // ================================================================ + RandBLAS::RNGState main_state(123); + std::vector> run_states(num_runs); + for (int64_t r = 0; r < num_runs; ++r) { + run_states[r] = main_state; + if (r > 0) run_states[r].key.incr(r); + } + + // Shared buffers + std::vector Q_buf(m * n); + T tol = std::pow(std::numeric_limits::epsilon(), 0.85); + + // Storage for all results + std::vector> all_results; + + // ================================================================ + // Run warmup (unreported) + // ================================================================ + std::cout << "Running warmup... " << std::flush; + { + auto warmup_state = run_states[0]; + std::vector R_warmup(n * n, 0.0); + RandLAPACK::CQRRT_linops warmup_algo(false, tol, false); + warmup_algo.nnz = sketch_nnz; + warmup_algo.block_size = block_size; + warmup_algo.call(LiV_op, R_warmup.data(), n, d_factor, warmup_state); + } + std::cout << "done\n\n"; + + // ================================================================ + // Common per-algorithm loop: run QR, check orthogonality, run apps + // ================================================================ + // call_algo(res, R, run_idx) fills res.qr_time_us, res.qr_breakdown, + // res.peak_rss_kb, res.analytical_kb — everything else is shared. + auto run_algo = [&](const std::string& name, auto call_algo) { + std::cout << "\n=== " << name << " ===\n"; + std::vector> results(num_runs); + for (int64_t r = 0; r < num_runs; ++r) { + auto& res = results[r]; + res.m = m; res.n = n; res.run_idx = r; + res.alg_name = name; + res.chol_time_us = chol_time_us; + + std::vector R(n * n, 0.0); + call_algo(res, R, r); + + compute_Q_from_R(LiV_op, R.data(), n, Q_buf.data(), m, n); + res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); + res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); + res.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_buf.data(), m, n); + + // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 + res.r_backward_error = compute_r_backward_error(LiV_op, R.data(), m, n, block_size); + + // Upcast orthogonality: reconstruct Q in higher precision + if (upcast_orth) { + if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n); + } else if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n); + } + } else { + res.orth_error_upcast = 0.0; + } + + if (!skip_apps) { + std::vector x_computed(n, 0.0); + app_generalized_ls(*K_inv_op_ptr, V_linop, R.data(), n, n, + b.data(), m, x_computed.data(), res.app_a_time_us); + blas::axpy(n, (T)-1.0, x_true.data(), 1, x_computed.data(), 1); + res.ls_rel_error = blas::nrm2(n, x_computed.data(), 1) / x_true_norm; + + std::vector sigma_b(n, 0.0); + app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); + + std::vector sigma_c(n, 0.0), V_R(n * n, 0.0), U_R(n * n, 0.0); + app_generalized_svecs(R.data(), n, n, sigma_c.data(), V_R.data(), U_R.data(), + res.right_svec_orth_error, res.app_c_time_us); + } + + res.total_a_time_us = res.qr_time_us + res.app_a_time_us; + res.total_b_time_us = res.qr_time_us + res.app_b_time_us; + res.total_c_time_us = res.qr_time_us + res.app_c_time_us; + all_results.push_back(res); + } + print_summary(name, results); + }; + + // ================================================================ + // CQRRT_linop + // ================================================================ + run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { + auto state = run_states[r]; + RandLAPACK::CQRRT_linops algo(true, tol, false); + algo.nnz = sketch_nnz; algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(LiV_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[10]; + // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + }); + + // ================================================================ + // CholQR + // ================================================================ + run_algo("CholQR", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::CholQR_linops algo(true, tol, false); + algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(LiV_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[5]; + // breakdown: alloc, fwd, adj, chol, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 6); + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); + }); + + // ================================================================ + // sCholQR3 + // ================================================================ + run_algo("sCholQR3", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::sCholQR3_linops algo(true, tol, false); + algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(LiV_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[17]; + // breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 18); + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); + }); + + // ================================================================ + // sCholQR3_basic (non-blocked, matches standard pseudocode) + // ================================================================ + run_algo("sCholQR3_basic", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::sCholQR3_linops_basic algo(true, tol, false); + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(LiV_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[14]; + // breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 15); + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); + }); + + // ================================================================ + // CQRRT_expl (materialize L^{-1}V, run dense CQRRT) + // ================================================================ + if (run_expl) { + // Materialize the composite operator once + std::cout << "\nMaterializing L^{-1}V (" << m << " x " << n << ", " + << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; + T* A_dense = new T[m * n](); + { + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + LiV_op(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense, m); + delete[] Eye; + } + std::cout << "done\n"; + + run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { + // Copy A_dense since CQRRT modifies it in-place + T* A_copy = new T[m * n]; + std::copy(A_dense, A_dense + m * n, A_copy); + + auto state = run_states[r]; + RandLAPACK::CQRRT algo(true, tol); + algo.compute_Q = false; + algo.orthogonalization = false; + algo.nnz = sketch_nnz; + + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(m, n, A_copy, m, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times.back(); // total time is last entry + // CQRRT_expl breakdown matches CQRRT_linop structure approximately + res.qr_breakdown.assign(algo.times.begin(), algo.times.end()); + // Pad to standard length + while (res.qr_breakdown.size() < 11) res.qr_breakdown.push_back(0); + res.analytical_kb = (m * n * sizeof(T)) / 1024; // just the dense matrix + + delete[] A_copy; + }); + + delete[] A_dense; + } + + // ================================================================ + // Write CSV output + // ================================================================ + // Generate timestamped filenames + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + + std::string results_file = output_dir + "/" + time_buf + "_gsvd_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_gsvd_breakdown.csv"; + + std::ofstream out(results_file); + write_csv_header(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + for (const auto& r : all_results) { + write_csv_row(out, r); + } + out.close(); + std::cout << "\n\nResults written to " << results_file << "\n"; + + write_breakdown_csv(breakdown_file, all_results, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + std::cout << "Runtime breakdown written to " << breakdown_file << "\n"; + + return 0; +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] + << " " + << " [sketch_nnz] [block_size] [skip_apps]\n"; + return 1; + } + + std::string precision = argv[1]; + if (precision == "double") { + return run_benchmark(argc, argv); + } else if (precision == "float") { + return run_benchmark(argc, argv); + } else { + std::cerr << "Unknown precision: " << precision << " (use 'double' or 'float')\n"; + return 1; + } +} From 8bcb860d080e6de8dddca8bee5be48a8fc4f9f20 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Fri, 3 Apr 2026 20:08:19 -0400 Subject: [PATCH 04/47] Install script update --- CMakeLists.txt | 5 ++++- extras/CMakeLists.txt | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 20687ffc7..67d3db665 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,10 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}) include(compiler_flags) # Configure the build -enable_testing() +option(BUILD_TESTING "Build tests" ON) +if(BUILD_TESTING) + enable_testing() +endif() include(rl_build_options) include(rl_version) diff --git a/extras/CMakeLists.txt b/extras/CMakeLists.txt index d1cc91a76..7b03979de 100644 --- a/extras/CMakeLists.txt +++ b/extras/CMakeLists.txt @@ -146,5 +146,8 @@ endif() # Testing Support # ----------------------------------------------------------------------------- -enable_testing() -add_subdirectory(test) +option(BUILD_TESTING "Build tests" ON) +if(BUILD_TESTING) + enable_testing() + add_subdirectory(test) +endif() From 7eaa6ec66d76b2634c2097eac0a270cf636a2fd9 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Sat, 4 Apr 2026 17:30:03 -0400 Subject: [PATCH 05/47] Removing inefficiency --- .../CQRRT_linop_composite_applications_v2.cc | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc index b5914b576..199d29074 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc @@ -582,6 +582,22 @@ int run_benchmark(int argc, char* argv[]) { } std::cout << "done\n\n"; + // ================================================================ + // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) + // ================================================================ + T* A_materialized = nullptr; + if (upcast_orth || run_expl) { + std::cout << "Materializing L^{-1}V for upcast/expl (" << m << " x " << n << ", " + << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; + A_materialized = new T[m * n](); + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + LiV_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_materialized, m); + delete[] Eye; + std::cout << "done\n\n"; + } + // ================================================================ // Common per-algorithm loop: run QR, check orthogonality, run apps // ================================================================ @@ -599,20 +615,50 @@ int run_benchmark(int argc, char* argv[]) { std::vector R(n * n, 0.0); call_algo(res, R, r); - compute_Q_from_R(LiV_op, R.data(), n, Q_buf.data(), m, n); + // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop + if (A_materialized) { + std::copy(A_materialized, A_materialized + m * n, Q_buf.data()); + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, + blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); + } else { + compute_Q_from_R(LiV_op, R.data(), n, Q_buf.data(), m, n); + } res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); res.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_buf.data(), m, n); // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 - res.r_backward_error = compute_r_backward_error(LiV_op, R.data(), m, n, block_size); + // Use pre-materialized A for direct SYRK if available, else blocked linop + if (A_materialized) { + std::vector AtA(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, (T)1.0, A_materialized, m, (T)0.0, AtA.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + AtA[i + j * n] = AtA[j + i * n]; + + std::vector RtR(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + RtR[i + j * n] = RtR[j + i * n]; + + T norm_A_sq = 0; + for (int64_t i = 0; i < n; ++i) norm_A_sq += AtA[i + i * n]; + T diff_sq = 0; + for (int64_t i = 0; i < n * n; ++i) { T d = AtA[i] - RtR[i]; diff_sq += d * d; } + res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq); + } else { + res.r_backward_error = compute_r_backward_error(LiV_op, R.data(), m, n, block_size); + } - // Upcast orthogonality: reconstruct Q in higher precision + // Upcast orthogonality: reconstruct Q in higher precision (uses pre-materialized A) if (upcast_orth) { if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n); + res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n, A_materialized); } else if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n); + res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n, A_materialized); } } else { res.orth_error_upcast = 0.0; @@ -702,26 +748,13 @@ int run_benchmark(int argc, char* argv[]) { }); // ================================================================ - // CQRRT_expl (materialize L^{-1}V, run dense CQRRT) + // CQRRT_expl (uses pre-materialized A, run dense CQRRT) // ================================================================ if (run_expl) { - // Materialize the composite operator once - std::cout << "\nMaterializing L^{-1}V (" << m << " x " << n << ", " - << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; - T* A_dense = new T[m * n](); - { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - LiV_op(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense, m); - delete[] Eye; - } - std::cout << "done\n"; - run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { - // Copy A_dense since CQRRT modifies it in-place + // Copy A_materialized since CQRRT modifies it in-place T* A_copy = new T[m * n]; - std::copy(A_dense, A_dense + m * n, A_copy); + std::copy(A_materialized, A_materialized + m * n, A_copy); auto state = run_states[r]; RandLAPACK::CQRRT algo(true, tol); @@ -741,10 +774,11 @@ int run_benchmark(int argc, char* argv[]) { delete[] A_copy; }); - - delete[] A_dense; } + // Free pre-materialized A + delete[] A_materialized; + // ================================================================ // Write CSV output // ================================================================ From d641656b17112308f52e0c1618875a21767b3433 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 6 Apr 2026 11:34:35 -0400 Subject: [PATCH 06/47] Optimizing the benchmark --- .../CQRRT_linop_composite_applications_v2.cc | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc index 199d29074..e40e67a9b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc @@ -66,7 +66,6 @@ struct gsvd_result { // Orthogonality of Q = (L^{-1}V) R^{-1} T orth_error; bool is_orthonormal; - int64_t max_orth_cols; // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 double r_backward_error; @@ -179,7 +178,8 @@ static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t // Compute orthogonality with upcast reconstruction. // Algorithm runs in precision T, Q = A R^{-1} computed in precision U (one level up). -// float -> double, double -> long double. Uses Eigen for TRSM in precision U. +// float -> double: uses BLAS++/LAPACK++ (MKL-backed DTRSM + DSYRK + DLANSY). +// double -> long double: uses Eigen (BLAS++ does not support long double). // If A_dense is non-null, uses it directly; otherwise materializes via linop. template static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, @@ -203,20 +203,32 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; - // Q = A * R^{-1} via Eigen triangular solve in precision U - Eigen::Map> - A_map(A_U.data(), m, n); - Eigen::Map> - R_map(R_U.data(), n, n); - - // Solve R^T * Q^T = A^T for Q^T - Eigen::Matrix Qt = - R_map.transpose().template triangularView().solve(A_map.transpose()); - - // Compute orthogonality: ||Q^T Q - I||_F / sqrt(n) - Eigen::Matrix QtQ = Qt * Qt.transpose(); // n x n - for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; - return (double)(QtQ.norm() / std::sqrt((U)n)); + if constexpr (std::is_same_v) { + // float->double: BLAS++ path — MKL DTRSM + DSYRK + DLANSY. + // Solve in-place: A_U = A_U * R_U^{-1} (becomes Q in double). + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + m, n, 1.0, R_U.data(), n, A_U.data(), m); + // GmI = Q^T Q - I (upper triangle via SYRK, then lansy for Frobenius norm) + std::vector GmI(n * n, 0.0); + RandLAPACK::util::eye(n, n, GmI.data()); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, 1.0, A_U.data(), m, -1.0, GmI.data(), n); + return lapack::lansy(lapack::Norm::Fro, blas::Uplo::Upper, n, GmI.data(), n) / std::sqrt((double)n); + } else { + // double->long double: Eigen path (BLAS++ does not support long double). + Eigen::Map> + A_map(A_U.data(), m, n); + Eigen::Map> + R_map(R_U.data(), n, n); + // Solve R^T * Q^T = A^T for Q^T + Eigen::Matrix Qt = + R_map.transpose().template triangularView().solve(A_map.transpose()); + // ||Q^T Q - I||_F / sqrt(n) + Eigen::Matrix QtQ = Qt * Qt.transpose(); + for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; + return (double)(QtQ.norm() / std::sqrt((U)n)); + } } // ============================================================================ @@ -355,7 +367,7 @@ static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_r bool skip_apps, bool compute_cond) { out << "# GSVD Benchmark results\n"; write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast,max_orth_cols," + out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," << "app_a_time_us,ls_rel_error," << "app_b_time_us," << "app_c_time_us,right_svec_orth_error," @@ -371,7 +383,6 @@ static void write_csv_row(std::ofstream& out, const gsvd_result& r) { << std::scientific << std::setprecision(6) << r.orth_error << "," << std::scientific << std::setprecision(6) << r.r_backward_error << "," << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," - << r.max_orth_cols << "," << r.app_a_time_us << "," << std::scientific << std::setprecision(6) << r.ls_rel_error << "," << r.app_b_time_us << "," @@ -431,8 +442,8 @@ template static void print_summary(const std::string& alg_name, const std::vector>& results) { printf("\n %s:\n", alg_name.c_str()); for (const auto& r : results) { - printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, max_orth=%ld/%ld, QR=%ld us\n", - (long)r.run_idx, (double)r.orth_error, r.r_backward_error, (long)r.max_orth_cols, (long)r.n, r.qr_time_us); + printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, QR=%ld us\n", + (long)r.run_idx, (double)r.orth_error, r.r_backward_error, r.qr_time_us); if (r.orth_error_upcast > 0) printf(" orth_upcast=%.2e\n", r.orth_error_upcast); if (r.app_a_time_us > 0 || r.ls_rel_error > 0) { @@ -598,6 +609,22 @@ int run_benchmark(int argc, char* argv[]) { std::cout << "done\n\n"; } + // ================================================================ + // Pre-compute A^T A and ||A||_F^2 for r_backward_error (A is constant) + // ================================================================ + std::vector AtA_precomputed; + T norm_A_sq_precomputed = 0; + if (A_materialized) { + AtA_precomputed.resize(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + AtA_precomputed[i + j * n] = AtA_precomputed[j + i * n]; + for (int64_t i = 0; i < n; ++i) + norm_A_sq_precomputed += AtA_precomputed[i + i * n]; + } + // ================================================================ // Common per-algorithm loop: run QR, check orthogonality, run apps // ================================================================ @@ -625,18 +652,10 @@ int run_benchmark(int argc, char* argv[]) { } res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - res.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_buf.data(), m, n); // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 - // Use pre-materialized A for direct SYRK if available, else blocked linop + // Use precomputed A^T A (A is constant across algorithms), else blocked linop if (A_materialized) { - std::vector AtA(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, (T)1.0, A_materialized, m, (T)0.0, AtA.data(), n); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - AtA[i + j * n] = AtA[j + i * n]; - std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); @@ -644,11 +663,9 @@ int run_benchmark(int argc, char* argv[]) { for (int64_t i = j + 1; i < n; ++i) RtR[i + j * n] = RtR[j + i * n]; - T norm_A_sq = 0; - for (int64_t i = 0; i < n; ++i) norm_A_sq += AtA[i + i * n]; T diff_sq = 0; - for (int64_t i = 0; i < n * n; ++i) { T d = AtA[i] - RtR[i]; diff_sq += d * d; } - res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq); + for (int64_t i = 0; i < n * n; ++i) { T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; } + res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); } else { res.r_backward_error = compute_r_backward_error(LiV_op, R.data(), m, n, block_size); } From 8d9cc77fe0bc48457a10d657cfb1ea810073c78e Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 6 Apr 2026 13:32:24 -0400 Subject: [PATCH 07/47] Minor updates --- .../CQRRT_linop_composite_applications_v2.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc index e40e67a9b..338a4043e 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc @@ -157,6 +157,7 @@ static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); + #pragma omp parallel for schedule(static) for (int64_t j = 0; j < n; ++j) for (int64_t i = j + 1; i < n; ++i) RtR[i + j * n] = RtR[j + i * n]; @@ -168,6 +169,7 @@ static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t // ||A^T A - R^T R||_F T diff_norm_sq = 0; + #pragma omp parallel for reduction(+:diff_norm_sq) schedule(static) for (int64_t i = 0; i < n * n; ++i) { T d = AtA[i] - RtR[i]; diff_norm_sq += d * d; @@ -200,7 +202,9 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, // Upcast A and R to precision U std::vector A_U(m * n), R_U(n * n); + #pragma omp parallel for schedule(static) for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; + #pragma omp parallel for schedule(static) for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; if constexpr (std::is_same_v) { @@ -600,7 +604,7 @@ int run_benchmark(int argc, char* argv[]) { if (upcast_orth || run_expl) { std::cout << "Materializing L^{-1}V for upcast/expl (" << m << " x " << n << ", " << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; - A_materialized = new T[m * n](); + A_materialized = new T[m * n]; T* Eye = new T[n * n](); RandLAPACK::util::eye(n, n, Eye); LiV_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, @@ -644,7 +648,8 @@ int run_benchmark(int argc, char* argv[]) { // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop if (A_materialized) { - std::copy(A_materialized, A_materialized + m * n, Q_buf.data()); + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < m * n; ++i) Q_buf[i] = A_materialized[i]; blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); } else { @@ -659,11 +664,13 @@ int run_benchmark(int argc, char* argv[]) { std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); + #pragma omp parallel for schedule(static) for (int64_t j = 0; j < n; ++j) for (int64_t i = j + 1; i < n; ++i) RtR[i + j * n] = RtR[j + i * n]; T diff_sq = 0; + #pragma omp parallel for reduction(+:diff_sq) schedule(static) for (int64_t i = 0; i < n * n; ++i) { T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; } res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); } else { @@ -771,7 +778,8 @@ int run_benchmark(int argc, char* argv[]) { run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { // Copy A_materialized since CQRRT modifies it in-place T* A_copy = new T[m * n]; - std::copy(A_materialized, A_materialized + m * n, A_copy); + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < m * n; ++i) A_copy[i] = A_materialized[i]; auto state = run_states[r]; RandLAPACK::CQRRT algo(true, tol); From 3a5e801b44f78b314fdb42e88618fbfdad12e51a Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 7 Apr 2026 14:23:05 -0400 Subject: [PATCH 08/47] CQRRT benchmarks redesigned --- RandLAPACK/drivers/rl_cqrrt_linops.hh | 119 ++- benchmark/CMakeLists.txt | 7 +- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 486 ++++++++++ .../CQRRT_linop_composite_applications.cc | 676 ++++++++++---- .../CQRRT_linop_composite_applications_v2.cc | 849 ------------------ .../bench_CQRRT_linops/CQRRT_orth_gap.cc | 718 --------------- 6 files changed, 1106 insertions(+), 1749 deletions(-) create mode 100644 benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc delete mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc delete mode 100644 benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc diff --git a/RandLAPACK/drivers/rl_cqrrt_linops.hh b/RandLAPACK/drivers/rl_cqrrt_linops.hh index b61be25c1..928aff9a9 100644 --- a/RandLAPACK/drivers/rl_cqrrt_linops.hh +++ b/RandLAPACK/drivers/rl_cqrrt_linops.hh @@ -14,6 +14,18 @@ using namespace std::chrono; namespace RandLAPACK { +/// Method used to form R_sk^{-1} in CQRRT_linops. +/// +/// TRSM_IDENTITY — original method: TRSM(I, R_sk) → R_sk^{-1}. +/// Fast (O(n³/2)) but unstable when κ(R_sk) is large. +/// GEQP3 — stabilized method: GEQP3(R_sk) = Q*R_buf*P^T, +/// then R_sk^{-1} = P * R_buf^{-1} * Q^T via ungqr + TRSM. +/// More expensive (O(11n³/6)) but accurate for ill-conditioned R_sk. +enum class CQRRTLinopPrecond { + TRSM_IDENTITY, + GEQP3 +}; + /// Sketch-preconditioned Cholesky QR for abstract linear operators. /// /// Linop analogue of CQRRT (rl_cqrrt.hh). Computes A = QR where A is any @@ -56,6 +68,11 @@ class CQRRT_linops { // require O(d*m) storage and O(d*m*n) work for application. bool use_dense_sketch; + // Method for computing R_sk^{-1}. See CQRRTLinopPrecond enum. + // Default: TRSM_IDENTITY (original behavior). + // Set to GEQP3 for the stabilized variant. + CQRRTLinopPrecond precond_method; + // Column-block size for the precondition + Gram computation. // // When block_size > 0, the two expensive linear operator calls: @@ -92,6 +109,7 @@ class CQRRT_linops { nnz = 2; use_dense_sketch = false; block_size = 0; + precond_method = CQRRTLinopPrecond::TRSM_IDENTITY; test_mode = enable_test_mode; Q = nullptr; Q_rows = 0; @@ -222,26 +240,79 @@ class CQRRT_linops { if(this -> timing) qr_t_stop = steady_clock::now(); - // Compute R_sk^{-1} via TRSM with identity (since A may not be dense, - // we cannot apply TRSM directly to the operator as in rl_cqrrt.hh). + // Compute R_sk^{-1}. Method selected by this->precond_method. if(this -> timing) trtri_t_start = steady_clock::now(); - // Instead of doing TRTRI to find R_sk_inv, we do TRSM with an identity, since trtri is not optimized in MKL - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - if (!RandLAPACK::util::diag_is_nonzero(n, A_hat, d)) { - delete[] A_hat; - delete[] tau; - delete[] Eye; - return 1; - } - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, 1.0, A_hat, d, Eye, n); - if (n > 1) { - // Clear the below-diagonal - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &Eye[1], n); + T* R_sk_inv = nullptr; + + if (this->precond_method == CQRRTLinopPrecond::TRSM_IDENTITY) { + // Original: TRSM(I, R_sk) → R_sk^{-1} (upper triangular result) + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + if (!RandLAPACK::util::diag_is_nonzero(n, A_hat, d)) { + delete[] A_hat; + delete[] tau; + delete[] Eye; + return 1; + } + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, A_hat, d, Eye, n); + if (n > 1) { + lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &Eye[1], n); + } + R_sk_inv = Eye; + } else { + // Stabilized: GEQP3(R_sk) = Q_buf * R_buf * P^T + // R_sk^{-1} = P * R_buf^{-1} * Q_buf^T + // Via: ungqr (explicit Q_buf) + TRSM (W = R_buf^{-1} * Q_buf^T) + scatter by jpiv + // R_sk_inv is dense (not upper triangular). + + // Extract R_sk from A_hat upper triangle into n×n buffer + T* R_sk_copy = new T[n * n](); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk_copy[i + j*n] = A_hat[i + j*d]; + + if (!RandLAPACK::util::diag_is_nonzero(n, R_sk_copy, n)) { + delete[] A_hat; + delete[] tau; + delete[] R_sk_copy; + return 1; + } + + int64_t* jpiv = new int64_t[n](); + T* tau_qr = new T[n]; + lapack::geqp3(n, n, R_sk_copy, n, jpiv, tau_qr); + + // Extract upper triangular R_buf before overwriting R_sk_copy with Q_buf + T* R_buf = new T[n * n](); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_buf[i + j*n] = R_sk_copy[i + j*n]; + + // Expand Q_buf from Householder reflectors (overwrites R_sk_copy) + lapack::ungqr(n, n, n, R_sk_copy, n, tau_qr); + + // W = Q_buf^T (explicit transpose), then solve R_buf * W = Q_buf^T in-place + T* W = new T[n * n]; + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + W[i + j*n] = R_sk_copy[j + i*n]; + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_buf, n, W, n); + + // R_sk_inv = P * W: row (jpiv[k]-1) of R_sk_inv gets row k of W + R_sk_inv = new T[n * n](); + for (int64_t k = 0; k < n; ++k) + for (int64_t j = 0; j < n; ++j) + R_sk_inv[(jpiv[k]-1) + j*n] = W[k + j*n]; + + delete[] R_sk_copy; + delete[] R_buf; + delete[] W; + delete[] jpiv; + delete[] tau_qr; } - T* R_sk_inv = Eye; if(this -> timing) { trtri_t_stop = steady_clock::now(); @@ -328,8 +399,20 @@ class CQRRT_linops { trmm_gram_t_start = steady_clock::now(); } - // (R_sk_inv)^T * (A^T * A_pre) - blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, n, n, (T) 1.0, R_sk_inv, n, R, ldr); + // Complete Gram: R := R_sk_inv^T * R (= R_sk_inv^T * A^T * A * R_sk_inv = A_pre^T * A_pre) + // TRSM_IDENTITY: R_sk_inv is upper triangular — use TRMM (O(n³/2)) + // GEQP3: R_sk_inv is dense — use GEMM (O(n³)) + if (this->precond_method == CQRRTLinopPrecond::TRSM_IDENTITY) { + blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, n, n, (T)1.0, R_sk_inv, n, R, ldr); + } else { + T* tmp = new T[n * n]; + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, n, n, n, + (T)1.0, R_sk_inv, n, R, ldr, (T)0.0, tmp, n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i < n; ++i) + R[i + j*ldr] = tmp[i + j*n]; + delete[] tmp; + } if(this -> timing) { trmm_gram_t_stop = steady_clock::now(); diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 9dfc44735..4b0906442 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -156,10 +156,9 @@ set( Eigen3::Eigen fast_matrix_market ) -add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -add_benchmark(NAME CQRRT_linop_composite_applications_v2 CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -add_benchmark(NAME CQRRT_orth_gap CXX_SOURCES bench_CQRRT_linops/CQRRT_orth_gap.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_diagnostic CXX_SOURCES bench_CQRRT_linops/CQRRT_diagnostic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc new file mode 100644 index 000000000..e75a215dd --- /dev/null +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -0,0 +1,486 @@ +// CQRRT preconditioner comparison benchmark +// +// Isolates the effect of different methods for forming R_sk^{-1} on the final +// orthogonality quality of CQRRT. Tests four paths: +// +// [1] expl_trsm: DTRSM_R(A, R_sk) in-place ← CQRRT_expl path +// [2] expl_inv_trsm: TRSM(I, R_sk) → R_inv; DGEMM(A, R_inv) (TRSM on identity) +// [3] expl_inv_trtri: TRTRI(R_sk) → R_inv; DGEMM(A, R_inv) (LAPACK trtri) +// [4] expl_inv_geqp3: GEQP3(R_sk) = Q*R_buf*P^T; +// R_inv = P * TRTRI(R_buf) * Q^T; DGEMM(A, R_inv) +// +// Path [1] never forms R_sk^{-1} explicitly (backward stable). +// Paths [2]-[4] all form R_sk^{-1} explicitly via different methods. +// Path [4] uses a rank-revealing QR to invert R_sk; the Q factor makes +// the inversion well-conditioned even when R_sk itself is ill-conditioned. +// +// All four paths use the same sketch (same RNG state). +// +// Per-path metrics: +// cond(A_pre) — condition number of the preconditioned matrix +// cond(G = A_pre^T A_pre) — condition number of the Gram matrix (input to Cholesky) +// orth_error(Q) — full-pipeline: G=SYRK(A_pre), R_chol=chol(G), +// R_final=R_chol*R_sk, Q=A_orig*R_final^{-1} +// +// Cross-path relative differences of A_pre (reference = path [1]): +// rd_12 = ||A_pre[1] - A_pre[2]|| / ||A_pre[1]|| +// rd_13 = ||A_pre[1] - A_pre[3]|| / ||A_pre[1]|| +// rd_14 = ||A_pre[1] - A_pre[4]|| / ||A_pre[1]|| +// +// Step-by-step pipeline divergence between paths [1] and [2] (CQRRT_expl vs CQRRT_linop): +// Reproduces the "step-by-step divergence" plot from CQRRT_orth_gap diag mode. +// Since all paths share the same sketch, M^sk and R^sk diffs are 0 by construction +// (the shared-sketch design cleanly eliminates sketch method as a variable). +// rd_G_12 = ||G1 - G2|| / ||G1|| (Gram matrix: G = A_pre^T A_pre) +// rd_Rchol_12 = ||Rchol1 - Rchol2|| / ||Rchol1|| (Cholesky factor) +// rd_Rfinal_12= ||Rfinal1 - Rfinal2|| / ||Rfinal1|| (final R = R_chol * R_sk) +// +// Sketch diagnostic: +// cond(R_sk) +// +// Usage: +// ./CQRRT_diagnostic [sketch_nnz] + +#include "RandLAPACK.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "rl_gen.hh" + +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif + +#include "../../extras/misc/ext_util.hh" +#include "RandLAPACK/testing/rl_test_utils.hh" + +using std::chrono::steady_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; +using blas::Layout; +using blas::Op; +using blas::Side; +using blas::Uplo; +using blas::Diag; + +// ============================================================================ +// Helpers +// ============================================================================ + +template +static T orth_error(const T* Q, int64_t m, int64_t n) { + return RandLAPACK::testing::orthogonality_error(Q, m, n); +} + +template +static T rel_diff(const T* A, const T* B, int64_t len) { + T nd = 0, na = 0; + for (int64_t i = 0; i < len; ++i) { + T d = A[i] - B[i]; + nd += d * d; na += A[i] * A[i]; + } + return (na > 0) ? std::sqrt(nd / na) : std::sqrt(nd); +} + +template +static T condition_number(const T* A, int64_t m, int64_t n) { + std::vector tmp(A, A + m * n); + std::vector s(n); + lapack::gesdd(lapack::Job::NoVec, m, n, tmp.data(), m, + s.data(), nullptr, 1, nullptr, 1); + return (s[n-1] > 0) ? s[0] / s[n-1] : std::numeric_limits::infinity(); +} + +// Condition number of the Gram matrix G = A_pre^T A_pre +template +static T gram_condition_number(const T* A_pre, int64_t m, int64_t n) { + std::vector G(n * n, 0.0); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_pre, m, A_pre, m, (T)0.0, G.data(), n); + return condition_number(G.data(), n, n); +} + +// Full CQRRT pipeline (matching V2 benchmark orth_error computation): +// G = A_pre^T A_pre (SYRK) +// R_chol = chol(G) (POTRF, overwrites G with upper triangular factor) +// R_final = R_chol * R_sketch (TRMM) +// Q = A_orig * R_final^{-1} (TRSM on copy of A_orig) +// return orth_error(Q) +// Does NOT modify A_pre or A_orig. +template +static T cholqr_orth_error(const std::vector& A_pre, const T* A_orig, + int64_t m, int64_t n, const T* R_sketch) { + std::vector G(n * n, 0.0); + blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, + n, m, (T)1.0, A_pre.data(), m, (T)0.0, G.data(), n); + if (lapack::potrf(Uplo::Upper, n, G.data(), n)) + return std::numeric_limits::infinity(); + blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_sketch, n, G.data(), n); + std::vector Q(A_orig, A_orig + m * n); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, G.data(), n, Q.data(), m); + return orth_error(Q.data(), m, n); +} + +// ============================================================================ +// One trial: three paths, same sketch +// ============================================================================ + +template +struct TrialResult { + // Per-path metrics + double cond_Apre[4]; + double cond_G[4]; + double orth_Q[4]; + // Cross-path relative differences of A_pre (reference = path [1]) + double rd_Apre_12; // TRSM in-place vs TRSM-on-identity + double rd_Apre_13; // TRSM in-place vs trtri + double rd_Apre_14; // TRSM in-place vs geqp3 + // Step-by-step pipeline divergence: paths [1] vs [2] (CQRRT_expl vs CQRRT_linop) + double rd_G_12; // Gram matrix: G = A_pre^T A_pre + double rd_Rchol_12; // Cholesky factor: R_chol = chol(G) + double rd_Rfinal_12; // Final R: R_final = R_chol * R_sk + // Sketch diagnostic + double cond_Rsk; +}; + +template +static TrialResult run_trial( + const T* A_dense, + int64_t m, int64_t n, + T d_factor, int64_t sketch_nnz, + RandBLAS::RNGState& state) +{ + TrialResult res{}; + int64_t d = (int64_t)std::ceil(d_factor * n); + + // ---------------------------------------------------------------- + // Sketch A → Ahat (d × n), then QR → R_sk + // ---------------------------------------------------------------- + RandBLAS::SparseDist Ds(d, m, sketch_nnz, RandBLAS::Axis::Short); + RandBLAS::SparseSkOp S(Ds, state); + std::vector Ahat(d * n, 0.0); + RandBLAS::sketch_general(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S, A_dense, m, + (T)0.0, Ahat.data(), d); + + std::vector R_sk(n * n, 0.0); + { + std::vector tau(n); + lapack::geqrf(d, n, Ahat.data(), d, tau.data()); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk[i + j*n] = Ahat[i + j*d]; + } + res.cond_Rsk = (double)condition_number(R_sk.data(), n, n); + + // ---------------------------------------------------------------- + // Explicit inverses of R_sk via two methods + // ---------------------------------------------------------------- + + // Method A: TRSM on identity (path [2]) + std::vector R_inv_trsm(n * n, 0.0); + RandLAPACK::util::eye(n, n, R_inv_trsm.data()); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, + R_sk.data(), n, R_inv_trsm.data(), n); + + // Method B: LAPACK trtri (path [3]) + std::vector R_inv_trtri(R_sk.begin(), R_sk.end()); + lapack::trtri(Uplo::Upper, Diag::NonUnit, n, R_inv_trtri.data(), n); + + // Method C: GEQP3 factorization of R_sk (path [4]) + // R_sk * P = Q_buf * R_buf (GEQP3) + // R_sk^{-1} = P * R_buf^{-1} * Q_buf^T + // Computed via Option A: ungqr (explicit Q) + TRSM (cheaper than trtri + ormqr) + // 1. ungqr -> Q_buf explicit (~4n^3/3 flops) + // 2. W = Q_buf^T (explicit transpose, O(n^2)) + // 3. TRSM(Left, R_buf, W) -> W = R_buf^{-1} * Q_buf^T (~n^3/2 flops) + // 4. scatter W by jpiv -> R_sk^{-1} = P * W + // Total: ~11n^3/6. Alternative (trtri + ormqr) costs ~7n^3/3 — see dev log. + std::vector R_inv_geqp3(n * n, 0.0); + { + std::vector R_copy(R_sk.begin(), R_sk.end()); + std::vector jpiv(n, 0); + std::vector tau_qr(n); + lapack::geqp3(n, n, R_copy.data(), n, jpiv.data(), tau_qr.data()); + + // Extract upper triangular R_buf before overwriting with Q + std::vector R_buf(n * n, 0.0); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_buf[i + j*n] = R_copy[i + j*n]; + + // Expand Q_buf from Householder reflectors (overwrites R_copy) + lapack::ungqr(n, n, n, R_copy.data(), n, tau_qr.data()); + + // W = R_buf^{-1} * Q_buf^T via TRSM: initialize W as Q_buf^T, then solve in-place + std::vector W(n * n, 0.0); + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + W[i + j*n] = R_copy[j + i*n]; // W := Q_buf^T (col-major) + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, + R_buf.data(), n, W.data(), n); + + // R_sk^{-1} = P * W: row (jpiv[k]-1) of R_inv gets row k of W + for (int64_t k = 0; k < n; ++k) + for (int64_t j = 0; j < n; ++j) + R_inv_geqp3[(jpiv[k]-1) + j*n] = W[k + j*n]; + } + + // ---------------------------------------------------------------- + // Compute all four A_pre matrices + // ---------------------------------------------------------------- + + // Path [1]: TRSM in-place on A ← CQRRT_expl + std::vector Apre1(A_dense, A_dense + m*n); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, + R_sk.data(), n, Apre1.data(), m); + + // Path [2]: explicit inverse via TRSM-on-I + DGEMM + std::vector Apre2(m * n, 0.0); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, + A_dense, m, R_inv_trsm.data(), n, + (T)0.0, Apre2.data(), m); + + // Path [3]: explicit inverse via trtri + DGEMM + std::vector Apre3(m * n, 0.0); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, + A_dense, m, R_inv_trtri.data(), n, + (T)0.0, Apre3.data(), m); + + // Path [4]: explicit inverse via geqp3 + trtri + Q^T + permutation + DGEMM + std::vector Apre4(m * n, 0.0); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, + A_dense, m, R_inv_geqp3.data(), n, + (T)0.0, Apre4.data(), m); + + // ---------------------------------------------------------------- + // Cross-path relative differences (reference = path [1]) + // ---------------------------------------------------------------- + res.rd_Apre_12 = (double)rel_diff(Apre1.data(), Apre2.data(), m*n); + res.rd_Apre_13 = (double)rel_diff(Apre1.data(), Apre3.data(), m*n); + res.rd_Apre_14 = (double)rel_diff(Apre1.data(), Apre4.data(), m*n); + + // ---------------------------------------------------------------- + // Step-by-step pipeline intermediates: paths [1] vs [2] + // G = A_pre^T A_pre (upper triangle via SYRK) + // R_chol = chol(G) (POTRF in-place on upper triangle) + // R_final = R_chol * R_sk (TRMM) + // ---------------------------------------------------------------- + auto make_G = [&](const std::vector& Apre) { + std::vector G(n*n, 0.0); + blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, + n, m, (T)1.0, Apre.data(), m, (T)0.0, G.data(), n); + return G; + }; + auto make_Rchol = [&](std::vector G) { + lapack::potrf(Uplo::Upper, n, G.data(), n); + return G; + }; + auto make_Rfinal = [&](std::vector Rchol) { + blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_sk.data(), n, Rchol.data(), n); + return Rchol; + }; + + auto G1 = make_G(Apre1); + auto G2 = make_G(Apre2); + auto Rchol1 = make_Rchol(G1); + auto Rchol2 = make_Rchol(G2); + auto Rfinal1 = make_Rfinal(Rchol1); + auto Rfinal2 = make_Rfinal(Rchol2); + + res.rd_G_12 = (double)rel_diff(G1.data(), G2.data(), n*n); + res.rd_Rchol_12 = (double)rel_diff(Rchol1.data(), Rchol2.data(), n*n); + res.rd_Rfinal_12 = (double)rel_diff(Rfinal1.data(), Rfinal2.data(), n*n); + + // ---------------------------------------------------------------- + // Per-path metrics + // ---------------------------------------------------------------- + res.cond_Apre[0] = (double)condition_number(Apre1.data(), m, n); + res.cond_Apre[1] = (double)condition_number(Apre2.data(), m, n); + res.cond_Apre[2] = (double)condition_number(Apre3.data(), m, n); + res.cond_Apre[3] = (double)condition_number(Apre4.data(), m, n); + + res.cond_G[0] = (double)gram_condition_number(Apre1.data(), m, n); + res.cond_G[1] = (double)gram_condition_number(Apre2.data(), m, n); + res.cond_G[2] = (double)gram_condition_number(Apre3.data(), m, n); + res.cond_G[3] = (double)gram_condition_number(Apre4.data(), m, n); + + res.orth_Q[0] = (double)cholqr_orth_error(Apre1, A_dense, m, n, R_sk.data()); + res.orth_Q[1] = (double)cholqr_orth_error(Apre2, A_dense, m, n, R_sk.data()); + res.orth_Q[2] = (double)cholqr_orth_error(Apre3, A_dense, m, n, R_sk.data()); + res.orth_Q[3] = (double)cholqr_orth_error(Apre4, A_dense, m, n, R_sk.data()); + + return res; +} + +// ============================================================================ +// Main benchmark +// ============================================================================ + +template +int run_benchmark(int argc, char* argv[]) { + if (argc < 6) { + std::cerr << "Usage: " << argv[0] + << " [sketch_nnz]\n"; + return 1; + } + + std::string output_dir = argv[2]; + std::string mtx_path = argv[3]; + T d_factor = (T)std::stod(argv[4]); + int64_t num_runs = std::stol(argv[5]); + int64_t sketch_nnz = (argc >= 7) ? std::stol(argv[6]) : 4; + + // ---------------------------------------------------------------- + // Load matrix and materialize as dense + // ---------------------------------------------------------------- + auto coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); + int64_t m = coo.n_rows, n = coo.n_cols; + + RandBLAS::sparse_data::csr::CSRMatrix csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(coo, csr); + RandLAPACK::linops::SparseLinOp> + A_linop(m, n, csr); + + std::vector A_dense(m * n, 0.0); + { + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); + delete[] Eye; + } + + T cond_A = condition_number(A_dense.data(), m, n); + int64_t d = (int64_t)std::ceil(d_factor * n); + + std::cout << "\n=== CQRRT Preconditioner Comparison ===\n"; + std::cout << " Matrix: " << mtx_path << "\n"; + std::cout << " Size: " << m << " x " << n << " (nnz=" << coo.nnz << ")\n"; + std::cout << " d_factor: " << d_factor << " (d=" << d << ")\n"; + std::cout << " sketch_nnz: " << sketch_nnz << "\n"; + std::cout << " runs: " << num_runs << "\n"; + std::cout << " cond(A): " << std::scientific << std::setprecision(3) << cond_A << "\n"; +#ifdef _OPENMP + std::cout << " OMP threads: " << omp_get_max_threads() << "\n"; +#endif + std::cout << "\n"; + + // ---------------------------------------------------------------- + // CSV setup + // ---------------------------------------------------------------- + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + std::string csv_path = output_dir + "/diagnostic_" + time_buf + ".csv"; + std::ofstream csv(csv_path); + csv << "# CQRRT Preconditioner Comparison\n"; + csv << "# Date: " << ctime(&now); + csv << "# Matrix: " << mtx_path << "\n"; + csv << "# m=" << m << " n=" << n << " d_factor=" << d_factor + << " sketch_nnz=" << sketch_nnz << "\n"; + csv << "# cond_A=" << cond_A << "\n"; + csv << "run," + << "orth_Q1,orth_Q2,orth_Q3,orth_Q4," + << "cond_Apre1,cond_Apre2,cond_Apre3,cond_Apre4," + << "cond_G1,cond_G2,cond_G3,cond_G4," + << "rd_Apre_12,rd_Apre_13,rd_Apre_14," + << "rd_G_12,rd_Rchol_12,rd_Rfinal_12," + << "cond_Rsk\n"; + + // ---------------------------------------------------------------- + // Runs + // ---------------------------------------------------------------- + RandBLAS::RNGState base_state(42); + for (int64_t r = 0; r < num_runs; ++r) { + auto state = base_state; + if (r > 0) state.key.incr(r); + + auto res = run_trial(A_dense.data(), m, n, d_factor, sketch_nnz, state); + + printf(" run %ld orth_error(Q = A * R_final^{-1}):\n", r); + printf(" [1] expl_trsm: %12.3e\n", res.orth_Q[0]); + printf(" [2] expl_inv_trsm: %12.3e\n", res.orth_Q[1]); + printf(" [3] expl_inv_trtri: %12.3e\n", res.orth_Q[2]); + printf(" [4] expl_inv_geqp3: %12.3e\n", res.orth_Q[3]); + + printf(" run %ld cond(A_pre):\n", r); + printf(" [1] expl_trsm: %12.3e\n", res.cond_Apre[0]); + printf(" [2] expl_inv_trsm: %12.3e\n", res.cond_Apre[1]); + printf(" [3] expl_inv_trtri: %12.3e\n", res.cond_Apre[2]); + printf(" [4] expl_inv_geqp3: %12.3e\n", res.cond_Apre[3]); + + printf(" run %ld cond(G = A_pre^T A_pre):\n", r); + printf(" [1] expl_trsm: %12.3e\n", res.cond_G[0]); + printf(" [2] expl_inv_trsm: %12.3e\n", res.cond_G[1]); + printf(" [3] expl_inv_trtri: %12.3e\n", res.cond_G[2]); + printf(" [4] expl_inv_geqp3: %12.3e\n", res.cond_G[3]); + + printf(" run %ld rel_diff(A_pre) vs [1]:\n", r); + printf(" rd_12 (trsm-on-I): %12.3e\n", res.rd_Apre_12); + printf(" rd_13 (trtri): %12.3e\n", res.rd_Apre_13); + printf(" rd_14 (geqp3): %12.3e\n", res.rd_Apre_14); + + printf(" run %ld step-by-step divergence [1] vs [2] (expl vs linop):\n", r); + printf(" M^sk: %12.3e (0 by construction — shared sketch)\n", 0.0); + printf(" R^sk: %12.3e (0 by construction — shared sketch)\n", 0.0); + printf(" MR^pre: %12.3e\n", res.rd_Apre_12); + printf(" G: %12.3e\n", res.rd_G_12); + printf(" R^chol: %12.3e\n", res.rd_Rchol_12); + printf(" R: %12.3e\n", res.rd_Rfinal_12); + + printf(" run %ld cond(R_sk): %9.3e\n\n", r, res.cond_Rsk); + + csv << r << "," + << std::scientific << std::setprecision(6) + << res.orth_Q[0] << "," << res.orth_Q[1] << "," + << res.orth_Q[2] << "," << res.orth_Q[3] << "," + << res.cond_Apre[0] << "," << res.cond_Apre[1] << "," + << res.cond_Apre[2] << "," << res.cond_Apre[3] << "," + << res.cond_G[0] << "," << res.cond_G[1] << "," + << res.cond_G[2] << "," << res.cond_G[3] << "," + << res.rd_Apre_12 << "," << res.rd_Apre_13 << "," + << res.rd_Apre_14 << "," + << res.rd_G_12 << "," << res.rd_Rchol_12 << "," + << res.rd_Rfinal_12 << "," + << res.cond_Rsk << "\n"; + } + csv.close(); + + std::cout << " Legend:\n"; + std::cout << " [1] expl_trsm: DTRSM_R(A, R_sk) in-place <- CQRRT_expl\n"; + std::cout << " [2] expl_inv_trsm: TRSM(I, R_sk)->R_inv; DGEMM(A, R_inv)\n"; + std::cout << " [3] expl_inv_trtri: TRTRI(R_sk)->R_inv; DGEMM(A, R_inv)\n"; + std::cout << " [4] expl_inv_geqp3: GEQP3(R_sk)=Q*R_buf*P^T; R_inv=P*TRTRI(R_buf)*Q^T; DGEMM(A, R_inv)\n"; + std::cout << "\n CSV written to: " << csv_path << "\n"; + + return 0; +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] + << " [sketch_nnz]\n"; + return 1; + } + std::string prec = argv[1]; + if (prec == "double") return run_benchmark(argc, argv); + if (prec == "float") return run_benchmark(argc, argv); + std::cerr << "Unknown precision '" << prec << "' (use double or float)\n"; + return 1; +} diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc index b3cef8c05..6b7258d0b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc @@ -10,8 +10,16 @@ // 7. Application (c): Generalized singular vectors — full SVD of R // // Usage: -// ./GSVD_benchmark -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] +// ./CQRRT_linop_composite_applications +// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask] +// +// method_mask (integer bitmask, optional, default = 0b001111 or 0b011111 if run_expl=1): +// bit 0 ( 1): CQRRT_linop +// bit 1 ( 2): CholQR +// bit 2 ( 4): sCholQR3 +// bit 3 ( 8): sCholQR3_basic +// bit 4 (16): CQRRT_expl (requires A materialization) +// bit 5 (32): CQRRT_linop_stb (GEQP3-stabilized preconditioner) #include "RandLAPACK.hh" #include "rl_blaspp.hh" @@ -30,6 +38,7 @@ #include #endif #include +#include #include // Extras utilities (Eigen-dependent) @@ -66,7 +75,12 @@ struct gsvd_result { // Orthogonality of Q = (L^{-1}V) R^{-1} T orth_error; bool is_orthonormal; - int64_t max_orth_cols; + + // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 + double r_backward_error; + + // Orthogonality computed with upcast reconstruction (float->double or double->long double) + double orth_error_upcast; // Application (a): Generalized LS long app_a_time_us; // Post-processing time only @@ -106,6 +120,130 @@ static void compute_Q_from_R( blas::Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); } +// Compute A^T A via blocked linop calls. Peak memory: O(m*b + n^2). +// No m x n materialization needed. +template +static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { + std::fill(AtA, AtA + n * n, (T)0.0); + std::vector E_block(n * b, 0.0); // n x b identity block + std::vector A_block(m * b, 0.0); // m x b = A * E_block + std::vector AtA_block(n * b, 0.0); // n x b = A^T * A_block + + for (int64_t j0 = 0; j0 < n; j0 += b) { + int64_t bk = std::min(b, n - j0); + + // E_block = I[:, j0:j0+bk] + std::fill(E_block.begin(), E_block.end(), (T)0.0); + for (int64_t j = 0; j < bk; ++j) + E_block[(j0 + j) + j * n] = (T)1.0; + + // A_block = A * E_block (m x bk) + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, bk, n, (T)1.0, E_block.data(), n, (T)0.0, A_block.data(), m); + + // AtA[:, j0:j0+bk] = A^T * A_block (n x bk) + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); + + // Copy into AtA columns j0..j0+bk + for (int64_t j = 0; j < bk; ++j) + for (int64_t i = 0; i < n; ++i) + AtA[i + (j0 + j) * n] = AtA_block[i + j * n]; + } +} + +// Compute R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 +// Uses blocked linop calls — peak memory O(m*b + n^2), no m x n materialization. +template +static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { + int64_t b = (block_size > 0) ? block_size : 256; + + // A^T A via blocked linop (n x n) + std::vector AtA(n * n, 0.0); + compute_AtA_blocked(A_op, m, n, AtA.data(), b); + + // R^T R (n x n) + std::vector RtR(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); + #pragma omp parallel for schedule(static) + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + RtR[i + j * n] = RtR[j + i * n]; + + // ||A||_F^2 = trace(A^T A) + T norm_A_sq = 0; + for (int64_t i = 0; i < n; ++i) + norm_A_sq += AtA[i + i * n]; + + // ||A^T A - R^T R||_F + T diff_norm_sq = 0; + #pragma omp parallel for reduction(+:diff_norm_sq) schedule(static) + for (int64_t i = 0; i < n * n; ++i) { + T d = AtA[i] - RtR[i]; + diff_norm_sq += d * d; + } + + return (double)(std::sqrt(diff_norm_sq) / (norm_A_sq)); +} + +// Compute orthogonality with upcast reconstruction. +// Algorithm runs in precision T, Q = A R^{-1} computed in precision U (one level up). +// float -> double: uses BLAS++/LAPACK++ (MKL-backed DTRSM + DSYRK + DLANSY). +// double -> long double: uses Eigen (BLAS++ does not support long double). +// If A_dense is non-null, uses it directly; otherwise materializes via linop. +template +static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, + const T* A_dense = nullptr) { + const T* A_ptr; + std::vector A_buf; + if (A_dense) { + A_ptr = A_dense; + } else { + A_buf.resize(m * n); + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_buf.data(), m); + delete[] Eye; + A_ptr = A_buf.data(); + } + + // Upcast A and R to precision U + std::vector A_U(m * n), R_U(n * n); + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; + + if constexpr (std::is_same_v) { + // float->double: BLAS++ path — MKL DTRSM + DSYRK + DLANSY. + // Solve in-place: A_U = A_U * R_U^{-1} (becomes Q in double). + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + m, n, 1.0, R_U.data(), n, A_U.data(), m); + // GmI = Q^T Q - I (upper triangle via SYRK, then lansy for Frobenius norm) + std::vector GmI(n * n, 0.0); + RandLAPACK::util::eye(n, n, GmI.data()); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, 1.0, A_U.data(), m, -1.0, GmI.data(), n); + return lapack::lansy(lapack::Norm::Fro, blas::Uplo::Upper, n, GmI.data(), n) / std::sqrt((double)n); + } else { + // double->long double: Eigen path (BLAS++ does not support long double). + Eigen::Map> + A_map(A_U.data(), m, n); + Eigen::Map> + R_map(R_U.data(), n, n); + // Solve R^T * Q^T = A^T for Q^T + Eigen::Matrix Qt = + R_map.transpose().template triangularView().solve(A_map.transpose()); + // ||Q^T Q - I||_F / sqrt(n) + Eigen::Matrix QtQ = Qt * Qt.transpose(); + for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; + return (double)(QtQ.norm() / std::sqrt((U)n)); + } +} + // ============================================================================ // Application (a): Generalized Least Squares // min_x ||Vx - b||_{K^{-1}} via R from QR of L^{-1}V @@ -242,7 +380,7 @@ static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_r bool skip_apps, bool compute_cond) { out << "# GSVD Benchmark results\n"; write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,max_orth_cols," + out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," << "app_a_time_us,ls_rel_error," << "app_b_time_us," << "app_c_time_us,right_svec_orth_error," @@ -256,7 +394,8 @@ static void write_csv_row(std::ofstream& out, const gsvd_result& r) { << r.chol_time_us << "," << r.qr_time_us << "," << std::scientific << std::setprecision(6) << r.orth_error << "," - << r.max_orth_cols << "," + << std::scientific << std::setprecision(6) << r.r_backward_error << "," + << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," << r.app_a_time_us << "," << std::scientific << std::setprecision(6) << r.ls_rel_error << "," << r.app_b_time_us << "," @@ -314,118 +453,42 @@ static void write_breakdown_csv( template static void print_summary(const std::string& alg_name, const std::vector>& results) { - std::cout << "\n " << alg_name.c_str() << ":\n"; + printf("\n %s:\n", alg_name.c_str()); for (const auto& r : results) { - std::cout << " Run " << (long)r.run_idx << ": orth_err=" << std::scientific << std::setprecision(2) << (double)r.orth_error << ", max_orth=" << (long)r.max_orth_cols << "/" << (long)r.n << ", QR=" << r.qr_time_us << " us\n"; + printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, QR=%ld us\n", + (long)r.run_idx, (double)r.orth_error, r.r_backward_error, r.qr_time_us); + if (r.orth_error_upcast > 0) + printf(" orth_upcast=%.2e\n", r.orth_error_upcast); if (r.app_a_time_us > 0 || r.ls_rel_error > 0) { - std::cout << " LS_err=" << std::scientific << std::setprecision(2) << (double)r.ls_rel_error << ", App(a)=" << r.app_a_time_us << " us, App(b)=" << r.app_b_time_us << " us, App(c)=" << r.app_c_time_us << " us\n"; + printf(" LS_err=%.2e, App(a)=%ld us, App(b)=%ld us, App(c)=%ld us\n", + (double)r.ls_rel_error, r.app_a_time_us, r.app_b_time_us, r.app_c_time_us); } - std::cout << " Memory: peak_RSS=" << r.peak_rss_kb << " KB, predicted=" << r.analytical_kb << " KB\n"; + printf(" Memory: peak_RSS=%ld KB, predicted=%ld KB\n", + r.peak_rss_kb, r.analytical_kb); } } // ============================================================================ -// Main benchmark +// Core benchmark logic (templated over operator type) // ============================================================================ -template -int run_benchmark(int argc, char* argv[]) { - // Parse arguments - if (argc < 7) { - std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond]\n"; - return 1; - } - - std::string output_dir = argv[2]; - int64_t num_runs = std::stol(argv[3]); - std::string K_file = argv[4]; - std::string V_file = argv[5]; - T d_factor = std::stod(argv[6]); - int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; - int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; - bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; - bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; - - std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; - std::cout << " K file: " << K_file << "\n"; - std::cout << " V file: " << V_file << "\n"; - std::cout << " d_factor: " << d_factor << "\n"; - std::cout << " sketch_nnz: " << sketch_nnz << "\n"; - std::cout << " block_size: " << block_size << "\n"; - std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; - std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; - std::cout << " num_runs: " << num_runs << "\n"; -#ifdef _OPENMP - std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; -#else - std::cout << " OpenMP threads: 1\n\n"; -#endif - - // ================================================================ - // Step 1: Load V from Matrix Market - // ================================================================ - std::cout << "Loading V from " << V_file << "... " << std::flush; - auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); - int64_t m = V_coo.n_rows; - int64_t n = V_coo.n_cols; - RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); - RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); - std::cout << "done (" << m << " x " << n << ", nnz=" << V_coo.nnz << ")\n"; - - // ================================================================ - // Step 2: Create L^{-1} operator (half_solve=true) and K^{-1} operator - // ================================================================ - std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; - - RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); - auto chol_start = steady_clock::now(); - L_inv_op.factorize(); - auto chol_stop = steady_clock::now(); - long chol_time_us = duration_cast(chol_stop - chol_start).count(); - - // Also create full K^{-1} for App (a) — only needed when running apps - std::unique_ptr> K_inv_op_ptr; - if (!skip_apps) { - K_inv_op_ptr = std::make_unique>(K_file, /*half_solve=*/false); - K_inv_op_ptr->factorize(); - } - - std::cout << "done (" << chol_time_us << " us)\n"; - - // ================================================================ - // Step 3: Form composite operator L^{-1} * V - // ================================================================ - RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); - LiV_op.block_size = block_size; - std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n"; - - // Condition number diagnostic (materializes L^{-1}V, runs two SVDs) +template +static int run_benchmark_inner( + OpType& A_op, + int64_t m, int64_t n, + const std::string& output_dir, + int64_t num_runs, + T d_factor, int64_t sketch_nnz, int64_t block_size, + bool skip_apps, bool compute_cond, bool upcast_orth, int64_t method_mask, + long chol_time_us, + const std::string& K_file, const std::string& V_file, + const std::string& op_label, + std::function&, gsvd_result&)> app_a_fn) +{ + // Condition number diagnostic (materializes A_op, runs two SVDs) if (compute_cond) { - RandLAPACK::testing::print_condition_diagnostics(LiV_op, "L^{-1}V"); - } - - // ================================================================ - // Step 4: Generate synthetic RHS: b = V * x_true (only when running apps) - // ================================================================ - RandBLAS::RNGState rng_state(42); - std::vector x_true(n); - std::vector b(m, 0.0); - T x_true_norm = 0.0; - if (!skip_apps) { - RandBLAS::DenseDist D(n, 1); - auto next_state = RandBLAS::fill_dense(D, x_true.data(), rng_state); - rng_state = next_state; - - V_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b.data(), m); - - x_true_norm = blas::nrm2(n, x_true.data(), 1); - std::cout << "Generated b = V * x_true (||x_true|| = " << x_true_norm << ")\n"; + RandLAPACK::testing::print_condition_diagnostics(A_op, op_label); } - std::cout << "\n"; // ================================================================ // Prepare RNG states for each run @@ -454,10 +517,42 @@ int run_benchmark(int argc, char* argv[]) { RandLAPACK::CQRRT_linops warmup_algo(false, tol, false); warmup_algo.nnz = sketch_nnz; warmup_algo.block_size = block_size; - warmup_algo.call(LiV_op, R_warmup.data(), n, d_factor, warmup_state); + warmup_algo.call(A_op, R_warmup.data(), n, d_factor, warmup_state); } std::cout << "done\n\n"; + // ================================================================ + // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) + // ================================================================ + T* A_materialized = nullptr; + if (upcast_orth || (method_mask & 16) || (method_mask & 32)) { + std::cout << "Materializing " << op_label << " for upcast/expl/stb (" << m << " x " << n << ", " + << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; + A_materialized = new T[m * n]; + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, n, n, (T)1.0, Eye, n, (T)0.0, A_materialized, m); + delete[] Eye; + std::cout << "done\n\n"; + } + + // ================================================================ + // Pre-compute A^T A and ||A||_F^2 for r_backward_error (A is constant) + // ================================================================ + std::vector AtA_precomputed; + T norm_A_sq_precomputed = 0; + if (A_materialized) { + AtA_precomputed.resize(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + AtA_precomputed[i + j * n] = AtA_precomputed[j + i * n]; + for (int64_t i = 0; i < n; ++i) + norm_A_sq_precomputed += AtA_precomputed[i + i * n]; + } + // ================================================================ // Common per-algorithm loop: run QR, check orthogonality, run apps // ================================================================ @@ -475,17 +570,53 @@ int run_benchmark(int argc, char* argv[]) { std::vector R(n * n, 0.0); call_algo(res, R, r); - compute_Q_from_R(LiV_op, R.data(), n, Q_buf.data(), m, n); + // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop + if (A_materialized) { + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < m * n; ++i) Q_buf[i] = A_materialized[i]; + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, + blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); + } else { + compute_Q_from_R(A_op, R.data(), n, Q_buf.data(), m, n); + } res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - res.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_buf.data(), m, n); + + // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 + // Use precomputed A^T A (A is constant across algorithms), else blocked linop + if (A_materialized) { + std::vector RtR(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); + #pragma omp parallel for schedule(static) + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + RtR[i + j * n] = RtR[j + i * n]; + + T diff_sq = 0; + #pragma omp parallel for reduction(+:diff_sq) schedule(static) + for (int64_t i = 0; i < n * n; ++i) { T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; } + res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); + } else { + res.r_backward_error = compute_r_backward_error(A_op, R.data(), m, n, block_size); + } + + // Upcast orthogonality: reconstruct Q in higher precision (uses pre-materialized A) + if (upcast_orth) { + if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); + } else if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); + } + } else { + res.orth_error_upcast = 0.0; + } if (!skip_apps) { - std::vector x_computed(n, 0.0); - app_generalized_ls(*K_inv_op_ptr, V_linop, R.data(), n, n, - b.data(), m, x_computed.data(), res.app_a_time_us); - blas::axpy(n, (T)-1.0, x_true.data(), 1, x_computed.data(), 1); - res.ls_rel_error = blas::nrm2(n, x_computed.data(), 1) / x_true_norm; + // App (a): generalized LS — only in composite mode (app_a_fn is null in sparse mode) + if (app_a_fn) { + app_a_fn(R, res); + } std::vector sigma_b(n, 0.0); app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); @@ -506,62 +637,123 @@ int run_benchmark(int argc, char* argv[]) { // ================================================================ // CQRRT_linop // ================================================================ - run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { - auto state = run_states[r]; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[10]; - // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); - }); + if (method_mask & 1) { + run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { + auto state = run_states[r]; + RandLAPACK::CQRRT_linops algo(true, tol, false); + algo.nnz = sketch_nnz; algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[10]; + // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + }); + } // ================================================================ // CholQR // ================================================================ - run_algo("CholQR", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::CholQR_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[5]; - // breakdown: alloc, fwd, adj, chol, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 6); - res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); - }); + if (method_mask & 2) { + run_algo("CholQR", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::CholQR_linops algo(true, tol, false); + algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[5]; + // breakdown: alloc, fwd, adj, chol, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 6); + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); + }); + } // ================================================================ // sCholQR3 // ================================================================ - run_algo("sCholQR3", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[17]; - // breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 18); - res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); - }); + if (method_mask & 4) { + run_algo("sCholQR3", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::sCholQR3_linops algo(true, tol, false); + algo.block_size = block_size; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[17]; + // breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 18); + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); + }); + } // ================================================================ // sCholQR3_basic (non-blocked, matches standard pseudocode) // ================================================================ - run_algo("sCholQR3_basic", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops_basic algo(true, tol, false); - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[14]; - // breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 15); - res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); - }); + if (method_mask & 8) { + run_algo("sCholQR3_basic", [&](gsvd_result& res, std::vector& R, int64_t) { + RandLAPACK::sCholQR3_linops_basic algo(true, tol, false); + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[14]; + // breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 15); + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); + }); + } + + // ================================================================ + // CQRRT_expl (uses pre-materialized A, run dense CQRRT) + // ================================================================ + if (method_mask & 16) { + run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { + // Copy A_materialized since CQRRT modifies it in-place + T* A_copy = new T[m * n]; + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < m * n; ++i) A_copy[i] = A_materialized[i]; + + auto state = run_states[r]; + RandLAPACK::CQRRT algo(true, tol); + algo.compute_Q = false; + algo.orthogonalization = false; + algo.nnz = sketch_nnz; + + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(m, n, A_copy, m, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times.back(); // total time is last entry + // CQRRT_expl breakdown matches CQRRT_linop structure approximately + res.qr_breakdown.assign(algo.times.begin(), algo.times.end()); + // Pad to standard length + while (res.qr_breakdown.size() < 11) res.qr_breakdown.push_back(0); + res.analytical_kb = (m * n * sizeof(T)) / 1024; // just the dense matrix + + delete[] A_copy; + }); + } + + // ================================================================ + // CQRRT_linop_stb (GEQP3-stabilized preconditioner, uses pre-materialized A for orth check) + // ================================================================ + if (method_mask & 32) { + run_algo("CQRRT_linop_stb", [&](gsvd_result& res, std::vector& R, int64_t r) { + auto state = run_states[r]; + RandLAPACK::CQRRT_linops algo(true, tol, false); + algo.nnz = sketch_nnz; + algo.block_size = block_size; + algo.precond_method = RandLAPACK::CQRRTLinopPrecond::GEQP3; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[10]; + // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + }); + } + + // Free pre-materialized A + delete[] A_materialized; // ================================================================ // Write CSV output @@ -588,11 +780,175 @@ int run_benchmark(int argc, char* argv[]) { return 0; } +// ============================================================================ +// Main benchmark dispatcher +// ============================================================================ + +template +int run_benchmark(int argc, char* argv[]) { + if (argc < 7) { + std::cerr << "Usage: " << argv[0] + << " " + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask]\n" + << " sparse mode: pass 'sparse' as K_file and a single A.mtx as V_file\n" + << " method_mask: bitmask selecting algorithms (default = 0b001111 or 0b011111 if run_expl=1)\n" + << " bit 0 ( 1): CQRRT_linop\n" + << " bit 1 ( 2): CholQR\n" + << " bit 2 ( 4): sCholQR3\n" + << " bit 3 ( 8): sCholQR3_basic\n" + << " bit 4 (16): CQRRT_expl (requires A materialization)\n" + << " bit 5 (32): CQRRT_linop_stb (GEQP3-stabilized preconditioner)\n"; + return 1; + } + + std::string output_dir = argv[2]; + int64_t num_runs = std::stol(argv[3]); + std::string K_file = argv[4]; + std::string V_file = argv[5]; + T d_factor = std::stod(argv[6]); + int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; + int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; + bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; + bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; + bool run_expl = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; + bool upcast_orth = (argc >= 13) ? (std::stol(argv[12]) != 0) : false; + // method_mask: bitmask selecting which algorithms to run. + // Default: bits 0-3 (all linop methods), plus bit 4 if run_expl=1 (backward compat). + // Explicit method_mask overrides run_expl for algorithm selection. + int64_t method_mask = (argc >= 14) ? std::stol(argv[13]) : (0b001111 | (run_expl ? 0b010000 : 0)); + + bool sparse_mode = (K_file == "sparse"); + + std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; + if (sparse_mode) { + std::cout << " Mode: sparse (single-matrix SparseLinOp)\n"; + std::cout << " A file: " << V_file << "\n"; + } else { + std::cout << " Mode: composite (L^{-1}V)\n"; + std::cout << " K file: " << K_file << "\n"; + std::cout << " V file: " << V_file << "\n"; + } + std::cout << " d_factor: " << d_factor << "\n"; + std::cout << " sketch_nnz: " << sketch_nnz << "\n"; + std::cout << " block_size: " << block_size << "\n"; + std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; + std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; + std::cout << " run_expl: " << (run_expl ? "yes" : "no") << " (backward compat; overridden by method_mask)\n"; + std::cout << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n"; + std::cout << " method_mask: " << method_mask << " (bits: linop=" << (method_mask&1) + << " CholQR=" << ((method_mask>>1)&1) << " sCholQR3=" << ((method_mask>>2)&1) + << " sCholQR3_basic=" << ((method_mask>>3)&1) << " expl=" << ((method_mask>>4)&1) + << " linop_stb=" << ((method_mask>>5)&1) << ")\n"; + std::cout << " num_runs: " << num_runs << "\n"; +#ifdef _OPENMP + std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; +#else + std::cout << " OpenMP threads: 1\n\n"; +#endif + + // ================================================================ + // Step 1: Load A (sparse mode) or V (composite mode) from Matrix Market + // ================================================================ + std::cout << "Loading " << (sparse_mode ? "A" : "V") << " from " << V_file << "... " << std::flush; + auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); + int64_t m = V_coo.n_rows; + int64_t n = V_coo.n_cols; + RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); + RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); + std::cout << "done (" << m << " x " << n << ", nnz=" << V_coo.nnz << ")\n"; + + if (m < n) { + std::cerr << "Error: matrix must be overdetermined (m >= n), got " << m << "x" << n << "\n"; + return 1; + } + + // ================================================================ + // Sparse mode: wrap A directly as SparseLinOp, skip Cholesky + // ================================================================ + if (sparse_mode) { + if (!skip_apps) { + std::cout << " Note: in sparse mode, app (a) (Gen. LS) is unavailable (requires K).\n" + << " Apps (b) and (c) will still run.\n"; + } + std::function&, gsvd_result&)> app_a_fn = nullptr; + return run_benchmark_inner( + V_linop, m, n, output_dir, num_runs, + d_factor, sketch_nnz, block_size, + skip_apps, compute_cond, upcast_orth, method_mask, + 0L /*chol_time_us*/, "sparse", V_file, + "A (" + V_file + ")", app_a_fn); + } + + // ================================================================ + // Composite mode: Steps 2-4 + // ================================================================ + + // Step 2: Create L^{-1} operator (half_solve=true) and K^{-1} operator + std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; + + RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); + auto chol_start = steady_clock::now(); + L_inv_op.factorize(); + auto chol_stop = steady_clock::now(); + long chol_time_us = duration_cast(chol_stop - chol_start).count(); + + // Also create full K^{-1} for App (a) — only needed when running apps + std::unique_ptr> K_inv_op_ptr; + if (!skip_apps) { + K_inv_op_ptr = std::make_unique>(K_file, /*half_solve=*/false); + K_inv_op_ptr->factorize(); + } + std::cout << "done (" << chol_time_us << " us)\n"; + + // Step 3: Form composite operator L^{-1} * V + RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); + LiV_op.block_size = block_size; + std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n"; + + // Step 4: Generate synthetic RHS: b = V * x_true (only when running apps) + RandBLAS::RNGState rng_state(42); + std::vector x_true(n); + std::vector b(m, 0.0); + T x_true_norm = 0.0; + if (!skip_apps) { + RandBLAS::DenseDist D(n, 1); + auto next_state = RandBLAS::fill_dense(D, x_true.data(), rng_state); + rng_state = next_state; + + V_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b.data(), m); + + x_true_norm = blas::nrm2(n, x_true.data(), 1); + std::cout << "Generated b = V * x_true (||x_true|| = " << x_true_norm << ")\n"; + } + std::cout << "\n"; + + // App (a) callback — captures K_inv_op_ptr, V_linop, b, x_true, x_true_norm + std::function&, gsvd_result&)> app_a_fn = nullptr; + if (!skip_apps) { + app_a_fn = [&](const std::vector& R, gsvd_result& res) { + std::vector x_computed(n, 0.0); + app_generalized_ls(*K_inv_op_ptr, V_linop, R.data(), n, n, + b.data(), m, x_computed.data(), res.app_a_time_us); + blas::axpy(n, (T)-1.0, x_true.data(), 1, x_computed.data(), 1); + res.ls_rel_error = blas::nrm2(n, x_computed.data(), 1) / x_true_norm; + }; + } + + return run_benchmark_inner( + LiV_op, m, n, output_dir, num_runs, + d_factor, sketch_nnz, block_size, + skip_apps, compute_cond, upcast_orth, method_mask, + chol_time_us, K_file, V_file, + "L^{-1}V", app_a_fn); +} + int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps]\n"; + << " " + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth]\n"; return 1; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc deleted file mode 100644 index 338a4043e..000000000 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications_v2.cc +++ /dev/null @@ -1,849 +0,0 @@ -// Generalized SVD / Generalized LS benchmark -// -// Pipeline: -// 1. Load K.mtx (m x m SPD) and V.mtx (m x n sparse) -// 2. Cholesky factorize K = LL^T, create L^{-1} operator (half_solve=true) -// 3. Create composite operator: CompositeOperator(L_inv_op, V_op) = L^{-1}V -// 4. Run Q-less QR on L^{-1}V via CQRRT_linops, CholQR_linops, sCholQR3_linops -// 5. Application (a): Generalized LS — solve min_x ||Vx - b||_{K^{-1}} -// 6. Application (b): Generalized singular values — SVD of R -// 7. Application (c): Generalized singular vectors — full SVD of R -// -// Usage: -// ./GSVD_benchmark -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] - -#include "RandLAPACK.hh" -#include "rl_blaspp.hh" -#include "rl_lapackpp.hh" -#include "rl_gen.hh" - -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _OPENMP -#include -#endif -#include -#include - -// Extras utilities (Eigen-dependent) -#include "../../extras/misc/ext_util.hh" -#include "../../extras/linops/ext_cholsolver_linop.hh" -#include "RandLAPACK/testing/rl_test_utils.hh" - -// Linops algorithms (now in main RandLAPACK) -#include "rl_cqrrt_linops.hh" -#include "rl_cholqr_linops.hh" -#include "rl_scholqr3_linops.hh" -#include "RandLAPACK/testing/rl_memory_tracker.hh" - -using std::chrono::steady_clock; -using std::chrono::duration_cast; -using std::chrono::microseconds; - -// ============================================================================ -// Result struct -// ============================================================================ - -template -struct gsvd_result { - int64_t m, n; - int64_t run_idx; - std::string alg_name; - - // Cholesky factorization time (shared, measured once) - long chol_time_us; - - // Q-less QR time - long qr_time_us; - - // Orthogonality of Q = (L^{-1}V) R^{-1} - T orth_error; - bool is_orthonormal; - - // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 - double r_backward_error; - - // Orthogonality computed with upcast reconstruction (float->double or double->long double) - double orth_error_upcast; - - // Application (a): Generalized LS - long app_a_time_us; // Post-processing time only - T ls_rel_error; // ||x - x_true|| / ||x_true|| - - // Application (b): Generalized singular values - long app_b_time_us; // SVD of R time - - // Application (c): Generalized singular vectors - long app_c_time_us; // Full SVD of R + V_R orthogonality check - T right_svec_orth_error; // ||V_R^T V_R - I||_F / sqrt(n) - - // Totals (QR + application post-processing) - long total_a_time_us; - long total_b_time_us; - long total_c_time_us; - - // QR timing breakdown (from algo.times[]) - std::vector qr_breakdown; - - // Memory tracking - long peak_rss_kb; // Peak RSS increase during QR call (KB) - long analytical_kb; // Analytical peak working memory (KB) -}; - -// Compute Q = A * R^{-1} uniformly for all algorithms -template -static void compute_Q_from_R( - GLO& A_op, T* R, int64_t ldr, - T* Q_out, int64_t m, int64_t n) { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); - delete[] Eye; - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); -} - -// Compute A^T A via blocked linop calls. Peak memory: O(m*b + n^2). -// No m x n materialization needed. -template -static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { - std::fill(AtA, AtA + n * n, (T)0.0); - std::vector E_block(n * b, 0.0); // n x b identity block - std::vector A_block(m * b, 0.0); // m x b = A * E_block - std::vector AtA_block(n * b, 0.0); // n x b = A^T * A_block - - for (int64_t j0 = 0; j0 < n; j0 += b) { - int64_t bk = std::min(b, n - j0); - - // E_block = I[:, j0:j0+bk] - std::fill(E_block.begin(), E_block.end(), (T)0.0); - for (int64_t j = 0; j < bk; ++j) - E_block[(j0 + j) + j * n] = (T)1.0; - - // A_block = A * E_block (m x bk) - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, bk, n, (T)1.0, E_block.data(), n, (T)0.0, A_block.data(), m); - - // AtA[:, j0:j0+bk] = A^T * A_block (n x bk) - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); - - // Copy into AtA columns j0..j0+bk - for (int64_t j = 0; j < bk; ++j) - for (int64_t i = 0; i < n; ++i) - AtA[i + (j0 + j) * n] = AtA_block[i + j * n]; - } -} - -// Compute R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 -// Uses blocked linop calls — peak memory O(m*b + n^2), no m x n materialization. -template -static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { - int64_t b = (block_size > 0) ? block_size : 256; - - // A^T A via blocked linop (n x n) - std::vector AtA(n * n, 0.0); - compute_AtA_blocked(A_op, m, n, AtA.data(), b); - - // R^T R (n x n) - std::vector RtR(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); - #pragma omp parallel for schedule(static) - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - RtR[i + j * n] = RtR[j + i * n]; - - // ||A||_F^2 = trace(A^T A) - T norm_A_sq = 0; - for (int64_t i = 0; i < n; ++i) - norm_A_sq += AtA[i + i * n]; - - // ||A^T A - R^T R||_F - T diff_norm_sq = 0; - #pragma omp parallel for reduction(+:diff_norm_sq) schedule(static) - for (int64_t i = 0; i < n * n; ++i) { - T d = AtA[i] - RtR[i]; - diff_norm_sq += d * d; - } - - return (double)(std::sqrt(diff_norm_sq) / (norm_A_sq)); -} - -// Compute orthogonality with upcast reconstruction. -// Algorithm runs in precision T, Q = A R^{-1} computed in precision U (one level up). -// float -> double: uses BLAS++/LAPACK++ (MKL-backed DTRSM + DSYRK + DLANSY). -// double -> long double: uses Eigen (BLAS++ does not support long double). -// If A_dense is non-null, uses it directly; otherwise materializes via linop. -template -static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, - const T* A_dense = nullptr) { - const T* A_ptr; - std::vector A_buf; - if (A_dense) { - A_ptr = A_dense; - } else { - A_buf.resize(m * n); - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_buf.data(), m); - delete[] Eye; - A_ptr = A_buf.data(); - } - - // Upcast A and R to precision U - std::vector A_U(m * n), R_U(n * n); - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; - - if constexpr (std::is_same_v) { - // float->double: BLAS++ path — MKL DTRSM + DSYRK + DLANSY. - // Solve in-place: A_U = A_U * R_U^{-1} (becomes Q in double). - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, - blas::Op::NoTrans, blas::Diag::NonUnit, - m, n, 1.0, R_U.data(), n, A_U.data(), m); - // GmI = Q^T Q - I (upper triangle via SYRK, then lansy for Frobenius norm) - std::vector GmI(n * n, 0.0); - RandLAPACK::util::eye(n, n, GmI.data()); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, 1.0, A_U.data(), m, -1.0, GmI.data(), n); - return lapack::lansy(lapack::Norm::Fro, blas::Uplo::Upper, n, GmI.data(), n) / std::sqrt((double)n); - } else { - // double->long double: Eigen path (BLAS++ does not support long double). - Eigen::Map> - A_map(A_U.data(), m, n); - Eigen::Map> - R_map(R_U.data(), n, n); - // Solve R^T * Q^T = A^T for Q^T - Eigen::Matrix Qt = - R_map.transpose().template triangularView().solve(A_map.transpose()); - // ||Q^T Q - I||_F / sqrt(n) - Eigen::Matrix QtQ = Qt * Qt.transpose(); - for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; - return (double)(QtQ.norm() / std::sqrt((U)n)); - } -} - -// ============================================================================ -// Application (a): Generalized Least Squares -// min_x ||Vx - b||_{K^{-1}} via R from QR of L^{-1}V -// -// Solution: x = R^{-1} R^{-T} V^T K^{-1} b -// Steps: c = K^{-1}b, d = V^T c, solve R^T y = d, solve Rx = y -// ============================================================================ - -template -static void app_generalized_ls( - RandLAPACK_extras::linops::CholSolverLinOp& K_inv_op, - VLinOp& V_op, - const T* R, int64_t ldr, int64_t n, - const T* b, int64_t m, - T* x, - long& app_time_us) -{ - auto start = steady_clock::now(); - - // Step 1: c = K^{-1} b (m x 1) - std::vector c(m, 0.0); - K_inv_op(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, m, (T)1.0, b, m, (T)0.0, c.data(), m); - - // Step 2: d = V^T c (n x 1) - std::vector d(n, 0.0); - V_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, 1, m, (T)1.0, c.data(), m, (T)0.0, d.data(), n); - - // Step 3: solve R^T y = d (n x 1) - std::copy(d.begin(), d.end(), x); - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, - blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); - - // Step 4: solve R x = y (n x 1) - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); - - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); -} - -// ============================================================================ -// Application (b): Generalized Singular Values -// SVD of R gives the generalized singular values of (V, K) -// ============================================================================ - -template -static void app_generalized_svals( - const T* R, int64_t ldr, int64_t n, - T* sigma, - long& app_time_us) -{ - auto start = steady_clock::now(); - - // Copy R to work buffer (gesdd destroys input) - std::vector R_copy(n * n); - lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - - // SVD of n x n R: only singular values (no vectors) - std::vector dummy_U(1), dummy_Vt(1); - lapack::gesdd(lapack::Job::NoVec, n, n, R_copy.data(), n, - sigma, dummy_U.data(), 1, dummy_Vt.data(), 1); - - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); -} - -// ============================================================================ -// Application (c): Generalized Singular Vectors -// Full SVD of R: R = U_R * Sigma * V_R^T -// Right generalized singular vectors = columns of V_R -// ============================================================================ - -template -static void app_generalized_svecs( - const T* R, int64_t ldr, int64_t n, - T* sigma, - T* V_R, // n x n right singular vectors - T* U_R, // n x n left singular vectors of R - T& right_svec_orth_error, - long& app_time_us) -{ - auto start = steady_clock::now(); - - // Copy R to work buffer - std::vector R_copy(n * n); - lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - - // Full SVD of n x n R: R = U_R * Sigma * V_R^T - lapack::gesdd(lapack::Job::AllVec, n, n, R_copy.data(), n, - sigma, U_R, n, V_R, n); - - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); - - // Verify right singular vector orthogonality: ||V_R^T V_R - I||_F / sqrt(n) - right_svec_orth_error = RandLAPACK::testing::orthogonality_error(V_R, n, n); -} - -// ============================================================================ -// CSV output -// ============================================================================ - -template -static void write_common_header_comments( - std::ofstream& out, int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) -{ - time_t now = time(nullptr); - out << "# Date: " << ctime(&now) - << "# Matrix dimensions: m=" << m << " n=" << n << "\n" - << "# Runs per algorithm: " << num_runs << "\n" -#ifdef _OPENMP - << "# OpenMP threads: " << omp_get_max_threads() << "\n" -#else - << "# OpenMP threads: 1\n" -#endif - << "# K_file: " << K_file << "\n" - << "# V_file: " << V_file << "\n" - << "# d_factor: " << d_factor << "\n" - << "# sketch_nnz: " << sketch_nnz << "\n" - << "# block_size: " << block_size << "\n" - << "# skip_apps: " << (skip_apps ? 1 : 0) << "\n" - << "# compute_cond: " << (compute_cond ? 1 : 0) << "\n"; -} - -template -static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) { - out << "# GSVD Benchmark results\n"; - write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," - << "app_a_time_us,ls_rel_error," - << "app_b_time_us," - << "app_c_time_us,right_svec_orth_error," - << "total_a_time_us,total_b_time_us,total_c_time_us," - << "peak_rss_kb,analytical_kb\n"; -} - -template -static void write_csv_row(std::ofstream& out, const gsvd_result& r) { - out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name << "," - << r.chol_time_us << "," - << r.qr_time_us << "," - << std::scientific << std::setprecision(6) << r.orth_error << "," - << std::scientific << std::setprecision(6) << r.r_backward_error << "," - << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," - << r.app_a_time_us << "," - << std::scientific << std::setprecision(6) << r.ls_rel_error << "," - << r.app_b_time_us << "," - << r.app_c_time_us << "," - << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," - << r.total_a_time_us << "," << r.total_b_time_us << "," << r.total_c_time_us << "," - << r.peak_rss_kb << "," << r.analytical_kb - << "\n"; -} - -// ============================================================================ -// Breakdown CSV output -// ============================================================================ - -template -static void write_breakdown_csv( - const std::string& filename, - const std::vector>& results, - int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) -{ - std::ofstream out(filename); - out << "# GSVD Benchmark runtime breakdown\n"; - write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "# Times are in microseconds\n"; - out << "# CQRRT_linop breakdown (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n"; - out << "# CholQR breakdown (6): alloc, fwd, adj, chol, rest, total\n"; - out << "# sCholQR3 breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total\n"; - out << "# sCholQR3_basic breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total\n"; - - // Column header: m, n, run, algorithm, then all breakdown times - // Max breakdown length is 18 (sCholQR3) - out << "m,n,run,algorithm"; - for (int i = 0; i < 18; ++i) out << ",t" << i; - out << "\n"; - - for (const auto& r : results) { - out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name; - for (size_t i = 0; i < r.qr_breakdown.size(); ++i) { - out << "," << r.qr_breakdown[i]; - } - // Pad with zeros if fewer than 18 columns - for (size_t i = r.qr_breakdown.size(); i < 18; ++i) { - out << ",0"; - } - out << "\n"; - } -} - -// ============================================================================ -// Console summary -// ============================================================================ - -template -static void print_summary(const std::string& alg_name, const std::vector>& results) { - printf("\n %s:\n", alg_name.c_str()); - for (const auto& r : results) { - printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, QR=%ld us\n", - (long)r.run_idx, (double)r.orth_error, r.r_backward_error, r.qr_time_us); - if (r.orth_error_upcast > 0) - printf(" orth_upcast=%.2e\n", r.orth_error_upcast); - if (r.app_a_time_us > 0 || r.ls_rel_error > 0) { - printf(" LS_err=%.2e, App(a)=%ld us, App(b)=%ld us, App(c)=%ld us\n", - (double)r.ls_rel_error, r.app_a_time_us, r.app_b_time_us, r.app_c_time_us); - } - printf(" Memory: peak_RSS=%ld KB, predicted=%ld KB\n", - r.peak_rss_kb, r.analytical_kb); - } -} - -// ============================================================================ -// Main benchmark -// ============================================================================ - -template -int run_benchmark(int argc, char* argv[]) { - // Parse arguments - if (argc < 7) { - std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond]\n"; - return 1; - } - - std::string output_dir = argv[2]; - int64_t num_runs = std::stol(argv[3]); - std::string K_file = argv[4]; - std::string V_file = argv[5]; - T d_factor = std::stod(argv[6]); - int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; - int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; - bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; - bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; - bool run_expl = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; - bool upcast_orth = (argc >= 13) ? (std::stol(argv[12]) != 0) : false; - - std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; - std::cout << " K file: " << K_file << "\n"; - std::cout << " V file: " << V_file << "\n"; - std::cout << " d_factor: " << d_factor << "\n"; - std::cout << " sketch_nnz: " << sketch_nnz << "\n"; - std::cout << " block_size: " << block_size << "\n"; - std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; - std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; - std::cout << " run_expl: " << (run_expl ? "yes" : "no") << "\n"; - std::cout << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n"; - std::cout << " num_runs: " << num_runs << "\n"; -#ifdef _OPENMP - std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; -#else - std::cout << " OpenMP threads: 1\n\n"; -#endif - - // ================================================================ - // Step 1: Load V from Matrix Market - // ================================================================ - std::cout << "Loading V from " << V_file << "... " << std::flush; - auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); - int64_t m = V_coo.n_rows; - int64_t n = V_coo.n_cols; - RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); - RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); - std::cout << "done (" << m << " x " << n << ", nnz=" << V_coo.nnz << ")\n"; - - // ================================================================ - // Step 2: Create L^{-1} operator (half_solve=true) and K^{-1} operator - // ================================================================ - std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; - - RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); - auto chol_start = steady_clock::now(); - L_inv_op.factorize(); - auto chol_stop = steady_clock::now(); - long chol_time_us = duration_cast(chol_stop - chol_start).count(); - - // Also create full K^{-1} for App (a) — only needed when running apps - std::unique_ptr> K_inv_op_ptr; - if (!skip_apps) { - K_inv_op_ptr = std::make_unique>(K_file, /*half_solve=*/false); - K_inv_op_ptr->factorize(); - } - - std::cout << "done (" << chol_time_us << " us)\n"; - - // ================================================================ - // Step 3: Form composite operator L^{-1} * V - // ================================================================ - RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); - LiV_op.block_size = block_size; - std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n"; - - // Condition number diagnostic (materializes L^{-1}V, runs two SVDs) - if (compute_cond) { - RandLAPACK::testing::print_condition_diagnostics(LiV_op, "L^{-1}V"); - } - - // ================================================================ - // Step 4: Generate synthetic RHS: b = V * x_true (only when running apps) - // ================================================================ - RandBLAS::RNGState rng_state(42); - std::vector x_true(n); - std::vector b(m, 0.0); - T x_true_norm = 0.0; - if (!skip_apps) { - RandBLAS::DenseDist D(n, 1); - auto next_state = RandBLAS::fill_dense(D, x_true.data(), rng_state); - rng_state = next_state; - - V_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b.data(), m); - - x_true_norm = blas::nrm2(n, x_true.data(), 1); - std::cout << "Generated b = V * x_true (||x_true|| = " << x_true_norm << ")\n"; - } - std::cout << "\n"; - - // ================================================================ - // Prepare RNG states for each run - // ================================================================ - RandBLAS::RNGState main_state(123); - std::vector> run_states(num_runs); - for (int64_t r = 0; r < num_runs; ++r) { - run_states[r] = main_state; - if (r > 0) run_states[r].key.incr(r); - } - - // Shared buffers - std::vector Q_buf(m * n); - T tol = std::pow(std::numeric_limits::epsilon(), 0.85); - - // Storage for all results - std::vector> all_results; - - // ================================================================ - // Run warmup (unreported) - // ================================================================ - std::cout << "Running warmup... " << std::flush; - { - auto warmup_state = run_states[0]; - std::vector R_warmup(n * n, 0.0); - RandLAPACK::CQRRT_linops warmup_algo(false, tol, false); - warmup_algo.nnz = sketch_nnz; - warmup_algo.block_size = block_size; - warmup_algo.call(LiV_op, R_warmup.data(), n, d_factor, warmup_state); - } - std::cout << "done\n\n"; - - // ================================================================ - // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) - // ================================================================ - T* A_materialized = nullptr; - if (upcast_orth || run_expl) { - std::cout << "Materializing L^{-1}V for upcast/expl (" << m << " x " << n << ", " - << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; - A_materialized = new T[m * n]; - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - LiV_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_materialized, m); - delete[] Eye; - std::cout << "done\n\n"; - } - - // ================================================================ - // Pre-compute A^T A and ||A||_F^2 for r_backward_error (A is constant) - // ================================================================ - std::vector AtA_precomputed; - T norm_A_sq_precomputed = 0; - if (A_materialized) { - AtA_precomputed.resize(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - AtA_precomputed[i + j * n] = AtA_precomputed[j + i * n]; - for (int64_t i = 0; i < n; ++i) - norm_A_sq_precomputed += AtA_precomputed[i + i * n]; - } - - // ================================================================ - // Common per-algorithm loop: run QR, check orthogonality, run apps - // ================================================================ - // call_algo(res, R, run_idx) fills res.qr_time_us, res.qr_breakdown, - // res.peak_rss_kb, res.analytical_kb — everything else is shared. - auto run_algo = [&](const std::string& name, auto call_algo) { - std::cout << "\n=== " << name << " ===\n"; - std::vector> results(num_runs); - for (int64_t r = 0; r < num_runs; ++r) { - auto& res = results[r]; - res.m = m; res.n = n; res.run_idx = r; - res.alg_name = name; - res.chol_time_us = chol_time_us; - - std::vector R(n * n, 0.0); - call_algo(res, R, r); - - // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop - if (A_materialized) { - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) Q_buf[i] = A_materialized[i]; - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); - } else { - compute_Q_from_R(LiV_op, R.data(), n, Q_buf.data(), m, n); - } - res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); - res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - - // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 - // Use precomputed A^T A (A is constant across algorithms), else blocked linop - if (A_materialized) { - std::vector RtR(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); - #pragma omp parallel for schedule(static) - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - RtR[i + j * n] = RtR[j + i * n]; - - T diff_sq = 0; - #pragma omp parallel for reduction(+:diff_sq) schedule(static) - for (int64_t i = 0; i < n * n; ++i) { T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; } - res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); - } else { - res.r_backward_error = compute_r_backward_error(LiV_op, R.data(), m, n, block_size); - } - - // Upcast orthogonality: reconstruct Q in higher precision (uses pre-materialized A) - if (upcast_orth) { - if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n, A_materialized); - } else if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(LiV_op, R.data(), m, n, A_materialized); - } - } else { - res.orth_error_upcast = 0.0; - } - - if (!skip_apps) { - std::vector x_computed(n, 0.0); - app_generalized_ls(*K_inv_op_ptr, V_linop, R.data(), n, n, - b.data(), m, x_computed.data(), res.app_a_time_us); - blas::axpy(n, (T)-1.0, x_true.data(), 1, x_computed.data(), 1); - res.ls_rel_error = blas::nrm2(n, x_computed.data(), 1) / x_true_norm; - - std::vector sigma_b(n, 0.0); - app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); - - std::vector sigma_c(n, 0.0), V_R(n * n, 0.0), U_R(n * n, 0.0); - app_generalized_svecs(R.data(), n, n, sigma_c.data(), V_R.data(), U_R.data(), - res.right_svec_orth_error, res.app_c_time_us); - } - - res.total_a_time_us = res.qr_time_us + res.app_a_time_us; - res.total_b_time_us = res.qr_time_us + res.app_b_time_us; - res.total_c_time_us = res.qr_time_us + res.app_c_time_us; - all_results.push_back(res); - } - print_summary(name, results); - }; - - // ================================================================ - // CQRRT_linop - // ================================================================ - run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { - auto state = run_states[r]; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[10]; - // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); - }); - - // ================================================================ - // CholQR - // ================================================================ - run_algo("CholQR", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::CholQR_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[5]; - // breakdown: alloc, fwd, adj, chol, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 6); - res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); - }); - - // ================================================================ - // sCholQR3 - // ================================================================ - run_algo("sCholQR3", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[17]; - // breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 18); - res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); - }); - - // ================================================================ - // sCholQR3_basic (non-blocked, matches standard pseudocode) - // ================================================================ - run_algo("sCholQR3_basic", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops_basic algo(true, tol, false); - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(LiV_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[14]; - // breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 15); - res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); - }); - - // ================================================================ - // CQRRT_expl (uses pre-materialized A, run dense CQRRT) - // ================================================================ - if (run_expl) { - run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { - // Copy A_materialized since CQRRT modifies it in-place - T* A_copy = new T[m * n]; - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) A_copy[i] = A_materialized[i]; - - auto state = run_states[r]; - RandLAPACK::CQRRT algo(true, tol); - algo.compute_Q = false; - algo.orthogonalization = false; - algo.nnz = sketch_nnz; - - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(m, n, A_copy, m, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times.back(); // total time is last entry - // CQRRT_expl breakdown matches CQRRT_linop structure approximately - res.qr_breakdown.assign(algo.times.begin(), algo.times.end()); - // Pad to standard length - while (res.qr_breakdown.size() < 11) res.qr_breakdown.push_back(0); - res.analytical_kb = (m * n * sizeof(T)) / 1024; // just the dense matrix - - delete[] A_copy; - }); - } - - // Free pre-materialized A - delete[] A_materialized; - - // ================================================================ - // Write CSV output - // ================================================================ - // Generate timestamped filenames - char time_buf[64]; - time_t now = time(nullptr); - strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); - - std::string results_file = output_dir + "/" + time_buf + "_gsvd_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_gsvd_breakdown.csv"; - - std::ofstream out(results_file); - write_csv_header(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - for (const auto& r : all_results) { - write_csv_row(out, r); - } - out.close(); - std::cout << "\n\nResults written to " << results_file << "\n"; - - write_breakdown_csv(breakdown_file, all_results, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - std::cout << "Runtime breakdown written to " << breakdown_file << "\n"; - - return 0; -} - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps]\n"; - return 1; - } - - std::string precision = argv[1]; - if (precision == "double") { - return run_benchmark(argc, argv); - } else if (precision == "float") { - return run_benchmark(argc, argv); - } else { - std::cerr << "Unknown precision: " << precision << " (use 'double' or 'float')\n"; - return 1; - } -} diff --git a/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc b/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc deleted file mode 100644 index 38c2c8392..000000000 --- a/benchmark/bench_CQRRT_linops/CQRRT_orth_gap.cc +++ /dev/null @@ -1,718 +0,0 @@ -// CQRRT orthogonality gap benchmark: CQRRT_linop vs CQRRT_expl -// -// Compares the two CQRRT implementations on the same matrix and reports -// the orthogonality gap. Five modes: -// -// generate — synthetic sparse matrix with controlled condition number -// file — read a Matrix Market file from disk -// composite — composite operator from K.mtx + V.mtx → L^{-1}V -// diag — step-by-step diagnostic on a Matrix Market file -// diag_gen — step-by-step diagnostic on a synthetic matrix -// -// The diagnostic modes manually execute both algorithms step by step, -// comparing intermediate results (sketch, QR, preconditioned product, Gram, -// Cholesky, final R) to pinpoint where numerical divergence occurs. -// All modes write CSV output to the current working directory. -// -// This benchmark lives on the paper branch (spring-2026-wip) and is not -// intended for inclusion in the main RandLAPACK library. -// -// Usage: -// ./CQRRT_orth_gap generate [nnz] [block_size] -// ./CQRRT_orth_gap file [nnz] [block_size] [compute_cond] -// ./CQRRT_orth_gap composite [nnz] [block_size] -// ./CQRRT_orth_gap diag [nnz] -// ./CQRRT_orth_gap diag_gen [nnz] - -#include "RandLAPACK.hh" -#include "rl_blaspp.hh" -#include "rl_lapackpp.hh" -#include "rl_gen.hh" - -#include -#include -#include -#include -#include -#include -#ifdef _OPENMP -#include -#endif - -// Extras utilities for Matrix Market I/O and CholSolver -#include "../../extras/misc/ext_util.hh" -#include "../../extras/linops/ext_cholsolver_linop.hh" -#include "RandLAPACK/testing/rl_test_utils.hh" - -// Linops algorithms -#include "rl_cqrrt_linops.hh" -#include "rl_composite_linop.hh" - -using std::chrono::steady_clock; -using std::chrono::duration_cast; -using std::chrono::microseconds; - -// Compute Q = A * R^{-1} via materialize + trsm (backward stable). -template -static void compute_Q_from_R( - GLO& A_op, T* R, int64_t ldr, - T* Q_out, int64_t m, int64_t n) { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - A_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); - delete[] Eye; - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); -} - -// Compute condition number via SVD. -template -static T compute_condition_number(T* A, int64_t m, int64_t n) { - std::vector A_copy(m * n); - std::copy(A, A + m * n, A_copy.data()); - std::vector sigma(n); - lapack::gesdd(lapack::Job::NoVec, m, n, A_copy.data(), m, sigma.data(), - nullptr, 1, nullptr, 1); - return sigma[0] / sigma[n - 1]; -} - -struct RunResult { - double linop_orth; - double expl_orth; - double gap_ratio; - long linop_time_us; - long expl_time_us; -}; - -// Relative difference between two buffers: ||A - B||_F / ||A||_F -template -static T rel_diff(const T* A, const T* B, int64_t len) { - T norm_diff = 0, norm_A = 0; - for (int64_t i = 0; i < len; ++i) { - T d = A[i] - B[i]; - norm_diff += d * d; - norm_A += A[i] * A[i]; - } - return std::sqrt(norm_diff) / std::sqrt(norm_A); -} - -// Step-by-step diagnostic: manually runs both algorithms and compares intermediates. -template -static void run_diagnostic( - GLO& A_linop, - int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, - RandBLAS::RNGState& state) { - - using RandBLAS::RNGState; - int64_t d = (int64_t)std::ceil(d_factor * n); - - printf("\n=== Step-by-step Diagnostic ===\n"); - printf(" m=%ld, n=%ld, d=%ld, sketch_nnz=%ld\n\n", m, n, d, sketch_nnz); - - // ----- Step 0: Materialize operator ----- - std::vector A_dense(m * n, 0.0); - { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); - delete[] Eye; - } - - // ----- Step 1: Sketch S*A ----- - // Both use same RNG state → same S - auto state_linop = state; - auto state_expl = state; - - // Linop sketch: A_linop(Side::Right, S) → spgemm - RandBLAS::SparseDist Dl(d, m, sketch_nnz, RandBLAS::Axis::Short); - RandBLAS::SparseSkOp S_linop(Dl, state_linop); - std::vector Ahat_linop(d * n, 0.0); - A_linop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, n, m, (T)1.0, S_linop, (T)0.0, Ahat_linop.data(), d); - - // Expl sketch: sketch_general(S, A_dense) → left_spmm - RandBLAS::SparseDist De(d, m, sketch_nnz, RandBLAS::Axis::Short); - RandBLAS::SparseSkOp S_expl(De, state_expl); - std::vector Ahat_expl(d * n, 0.0); - RandBLAS::sketch_general(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, n, m, (T)1.0, S_expl, A_dense.data(), m, - (T)0.0, Ahat_expl.data(), d); - - double rd_sketch = (double)rel_diff(Ahat_linop.data(), Ahat_expl.data(), d * n); - printf(" %-40s %12.3e\n", "Step 1: Sketch ||Ahat_l - Ahat_e||/||Ahat_l||", rd_sketch); - - // ----- Step 2: QR of sketch → R_sk ----- - std::vector R_sk_linop(n * n, 0.0); - std::vector R_sk_expl(n * n, 0.0); - { - // QR on linop sketch - std::vector tau(n); - lapack::geqrf(d, n, Ahat_linop.data(), d, tau.data()); - // Extract R - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_sk_linop[i + j * n] = Ahat_linop[i + j * d]; - } - { - // QR on expl sketch - std::vector tau(n); - lapack::geqrf(d, n, Ahat_expl.data(), d, tau.data()); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_sk_expl[i + j * n] = Ahat_expl[i + j * d]; - } - - double rd_qr = (double)rel_diff(R_sk_linop.data(), R_sk_expl.data(), n * n); - printf(" %-40s %12.3e\n", "Step 2: QR ||R_sk_l - R_sk_e||/||R_sk_l||", rd_qr); - - // ----- Step 3 (linop only): Explicit inverse R_pre = R_sk^{-1} ----- - std::vector R_pre(n * n, 0.0); - RandLAPACK::util::eye(n, n, R_pre.data()); - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, R_sk_linop.data(), n, R_pre.data(), n); - - // ----- Step 3: A_pre = A * R_pre (linop) vs A * R_sk^{-1} via TRSM (expl) ----- - // Linop: A_pre_l = A * R_pre (using the linop's explicit inverse) - std::vector A_pre_linop(m * n, 0.0); - A_linop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, R_pre.data(), n, (T)0.0, A_pre_linop.data(), m); - - // Expl: A_pre_e = A * R_sk^{-1} via in-place TRSM (backward stable, no explicit inverse) - std::vector A_pre_expl(A_dense.begin(), A_dense.end()); // copy - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, m, n, (T)1.0, R_sk_expl.data(), n, A_pre_expl.data(), m); - - double rd_apre = (double)rel_diff(A_pre_linop.data(), A_pre_expl.data(), m * n); - printf(" %-40s %12.3e\n", "Step 3: A_pre ||Apre_l - Apre_e||/||Apre_l||", rd_apre); - - // ----- Step 5a (linop): Gram via A^T * A_pre then R_pre^T * ... ----- - std::vector G_linop(n * n, 0.0); - A_linop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, A_pre_linop.data(), m, (T)0.0, G_linop.data(), n); - // Apply R_pre^T on left: G = R_pre^T * (A^T * A_pre) - blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, - Diag::NonUnit, n, n, (T)1.0, R_pre.data(), n, G_linop.data(), n); - std::vector G_linop_final(G_linop.begin(), G_linop.end()); - - // Compute A^T * A_pre for BOTH paths via dense GEMM (apples-to-apples, no SYRK vs GEMM difference) - std::vector AtApre_linop(n * n, 0.0); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, A_dense.data(), m, A_pre_linop.data(), m, - (T)0.0, AtApre_linop.data(), n); - std::vector AtApre_expl(n * n, 0.0); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, A_dense.data(), m, A_pre_expl.data(), m, - (T)0.0, AtApre_expl.data(), n); - - double rd_atapre = (double)rel_diff(AtApre_linop.data(), AtApre_expl.data(), n * n); - printf(" %-40s %12.3e\n", "Step 4a: A^T*Apre ||l - e||/||l|| (GEMM)", rd_atapre); - - // ----- Step 5b: Full Gram: linop uses GEMM+TRMM, expl uses SYRK ----- - std::vector G_expl(n * n, 0.0); - blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, - n, m, (T)1.0, A_pre_expl.data(), m, (T)0.0, G_expl.data(), n); - // Fill lower triangle - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - G_expl[i + j * n] = G_expl[j + i * n]; - - // Also compare SYRK vs GEMM on the SAME A_pre_expl buffer (isolate SYRK effect) - std::vector G_expl_gemm(n * n, 0.0); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, A_pre_expl.data(), m, A_pre_expl.data(), m, - (T)0.0, G_expl_gemm.data(), n); - - double rd_gram = (double)rel_diff(G_linop_final.data(), G_expl.data(), n * n); - double rd_syrk_gemm = (double)rel_diff(G_expl.data(), G_expl_gemm.data(), n * n); - printf(" %-40s %12.3e\n", "Step 4b: Gram ||G_l - G_e||/||G_l||", rd_gram); - printf(" %-40s %12.3e\n", "Step 4c: SYRK vs GEMM on same Apre_e", rd_syrk_gemm); - - // ----- Step 5: Cholesky ----- - std::vector R_chol_linop(G_linop_final.begin(), G_linop_final.end()); - std::vector R_chol_expl(G_expl.begin(), G_expl.end()); - lapack::potrf(Uplo::Upper, n, R_chol_linop.data(), n); - lapack::potrf(Uplo::Upper, n, R_chol_expl.data(), n); - // Zero below diagonal - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) { - R_chol_linop[i + j * n] = 0; - R_chol_expl[i + j * n] = 0; - } - - double rd_chol = (double)rel_diff(R_chol_linop.data(), R_chol_expl.data(), n * n); - printf(" %-40s %12.3e\n", "Step 5: Cholesky ||Rc_l - Rc_e||/||Rc_l||", rd_chol); - - // ----- Step 6: Final R = R_chol * R_sk ----- - std::vector R_final_linop(n * n, 0.0); - std::vector R_final_expl(n * n, 0.0); - blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - n, n, n, (T)1.0, R_chol_linop.data(), n, R_sk_linop.data(), n, - (T)0.0, R_final_linop.data(), n); - blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - n, n, n, (T)1.0, R_chol_expl.data(), n, R_sk_expl.data(), n, - (T)0.0, R_final_expl.data(), n); - - double rd_finalR = (double)rel_diff(R_final_linop.data(), R_final_expl.data(), n * n); - printf(" %-40s %12.3e\n", "Step 6: Final R ||R_l - R_e||/||R_l||", rd_finalR); - - // ----- Step 7: Orthogonality of Q = A * R^{-1} ----- - std::vector Q(m * n); - compute_Q_from_R(A_linop, R_final_linop.data(), n, Q.data(), m, n); - T orth_linop = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); - - compute_Q_from_R(A_linop, R_final_expl.data(), n, Q.data(), m, n); - T orth_expl = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); - - printf(" %-40s linop=%.3e, expl=%.3e, gap=%.1fx\n", - "Step 7: Orthogonality", - (double)orth_linop, (double)orth_expl, (double)(orth_linop / orth_expl)); - printf("\n"); - - // Write diagnostic CSV - // Filename: diag_.csv in current directory - auto now = std::chrono::system_clock::now(); - auto t = std::chrono::system_clock::to_time_t(now); - char ts[32]; - std::strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", std::localtime(&t)); - - std::string csv_name = std::string("orth_gap_diag_") + ts + ".csv"; - std::ofstream csv(csv_name); - if (csv.is_open()) { - std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; - csv << "# CQRRT orth gap diagnostic\n"; - csv << "# precision=" << prec_name << ", m=" << m << ", n=" << n << ", d=" << d << ", sketch_nnz=" << sketch_nnz << "\n"; - csv << "step,description,rel_diff\n"; - csv << std::scientific << std::setprecision(6); - csv << "1,sketch," << rd_sketch << "\n"; - csv << "2,qr_R_sk," << rd_qr << "\n"; - csv << "3,A_pre," << rd_apre << "\n"; - csv << "4,gram," << rd_gram << "\n"; - csv << "5,cholesky," << rd_chol << "\n"; - csv << "6,final_R," << rd_finalR << "\n"; - csv << "7,orth_linop," << (double)orth_linop << "\n"; - csv << "7,orth_expl," << (double)orth_expl << "\n"; - csv << "7,orth_gap," << (double)(orth_linop / orth_expl) << "\n"; - csv.close(); - printf(" Diagnostic CSV: %s\n\n", csv_name.c_str()); - } -} - -template -static RunResult run_comparison( - GLO& A_linop, - int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, int64_t block_size, - RandBLAS::RNGState& state) { - - T tol = std::pow(std::numeric_limits::epsilon(), 0.85); - std::vector Q(m * n); - RunResult res; - - // --- CQRRT_linop --- - { - std::vector R(n * n, 0.0); - auto state_copy = state; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; - algo.block_size = block_size; - - auto t0 = steady_clock::now(); - algo.call(A_linop, R.data(), n, d_factor, state_copy); - auto t1 = steady_clock::now(); - res.linop_time_us = duration_cast(t1 - t0).count(); - - compute_Q_from_R(A_linop, R.data(), n, Q.data(), m, n); - res.linop_orth = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); - } - - // --- CQRRT_expl (materialize + dense CQRRT) --- - { - // Materialize the operator - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - T* A_dense = new T[m * n](); - A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense, m); - delete[] Eye; - - std::vector R(n * n, 0.0); - auto state_copy = state; - RandLAPACK::CQRRT algo(true, tol); - algo.compute_Q = false; - algo.orthogonalization = false; - algo.nnz = sketch_nnz; - - auto t0 = steady_clock::now(); - algo.call(m, n, A_dense, m, R.data(), n, d_factor, state_copy); - auto t1 = steady_clock::now(); - res.expl_time_us = duration_cast(t1 - t0).count(); - - delete[] A_dense; - - compute_Q_from_R(A_linop, R.data(), n, Q.data(), m, n); - res.expl_orth = RandLAPACK::testing::orthogonality_error(Q.data(), m, n); - } - - res.gap_ratio = res.linop_orth / res.expl_orth; - return res; -} - -template -static int run_generate_mode(int argc, char* argv[]) { - // ./CQRRT_orth_gap generate [sketch_nnz] [block_size] - if (argc < 10) { - std::cerr << "Usage: " << argv[0] - << " generate [sketch_nnz] [block_size]\n"; - return 1; - } - int64_t m = std::stol(argv[3]); - int64_t n = std::stol(argv[4]); - double cond_num = std::stod(argv[5]); - double density = std::stod(argv[6]); - double d_factor = std::stod(argv[7]); - int64_t num_runs = std::stol(argv[8]); - int64_t sketch_nnz = (argc >= 10) ? std::stol(argv[9]) : 4; - int64_t block_size = (argc >= 11) ? std::stol(argv[10]) : 0; - - printf("\n=== CQRRT Orthogonality Gap Benchmark (generate mode) ===\n"); - printf(" Matrix: %ld x %ld, kappa=%.2e, density=%.3f\n", m, n, cond_num, density); - printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", - d_factor, sketch_nnz, block_size, num_runs); -#ifdef _OPENMP - printf(" OpenMP threads: %d\n", omp_get_max_threads()); -#endif - printf("=========================================================\n\n"); - - auto state = RandBLAS::RNGState(); - auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, (T)cond_num, state, (T)density); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - - // Timestamp for CSV filenames - auto now_gen = std::chrono::system_clock::now(); - auto t_gen = std::chrono::system_clock::to_time_t(now_gen); - char ts_gen[32]; - std::strftime(ts_gen, sizeof(ts_gen), "%Y%m%d_%H%M%S", std::localtime(&t_gen)); - std::string csv_name = std::string("orth_gap_gen_") + ts_gen + ".csv"; - std::ofstream csv(csv_name); - if (csv.is_open()) { - std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; - csv << "# CQRRT orth gap benchmark (generate mode)\n"; - csv << "# precision=" << prec_name << ", m=" << m << ", n=" << n << ", kappa=" << cond_num - << ", density=" << density << ", d_factor=" << d_factor - << ", sketch_nnz=" << sketch_nnz << ", block_size=" << block_size << "\n"; - csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; - } - - printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", - "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); - printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", - "---", "-----------", "----------", "-------", "----------", "---------"); - - for (int64_t r = 0; r < num_runs; ++r) { - auto run_state = state; - auto res = run_comparison(A_linop, m, n, (T)d_factor, sketch_nnz, block_size, run_state); - printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", - r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); - if (csv.is_open()) { - csv << r << "," << std::scientific << std::setprecision(6) - << res.linop_orth << "," << res.expl_orth << "," - << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; - } - state = RandBLAS::RNGState(state.key.incr()); - } - if (csv.is_open()) { - csv.close(); - printf(" Results CSV: %s\n", csv_name.c_str()); - } - printf("\n"); - return 0; -} - -template -static int run_file_mode(int argc, char* argv[]) { - // ./CQRRT_orth_gap file [sketch_nnz] [block_size] [compute_cond] - if (argc < 6) { - std::cerr << "Usage: " << argv[0] - << " file [sketch_nnz] [block_size] [compute_cond]\n"; - return 1; - } - std::string mtx_path = argv[3]; - double d_factor = std::stod(argv[4]); - int64_t num_runs = std::stol(argv[5]); - int64_t sketch_nnz = (argc >= 7) ? std::stol(argv[6]) : 4; - int64_t block_size = (argc >= 8) ? std::stol(argv[7]) : 0; - bool compute_cond = (argc >= 9) ? (std::stol(argv[8]) != 0) : false; - - printf("\n=== CQRRT Orthogonality Gap Benchmark (file mode) ===\n"); - printf(" File: %s\n", mtx_path.c_str()); - printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", - d_factor, sketch_nnz, block_size, num_runs); -#ifdef _OPENMP - printf(" OpenMP threads: %d\n", omp_get_max_threads()); -#endif - printf("=====================================================\n\n"); - - // Read Matrix Market file - printf("Loading %s...", mtx_path.c_str()); - fflush(stdout); - auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); - int64_t m = A_coo.n_rows; - int64_t n = A_coo.n_cols; - printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); - - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - - if (compute_cond) { - printf("Computing condition number via SVD (%ld x %ld)...", m, n); - fflush(stdout); - std::vector A_dense(m * n, 0.0); - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); - delete[] Eye; - T kappa = compute_condition_number(A_dense.data(), m, n); - printf(" kappa = %.6e\n", (double)kappa); - } - - auto state = RandBLAS::RNGState(); - - // Extract matrix name from path for CSV - std::string mtx_name = mtx_path; - auto slash = mtx_name.rfind('/'); - if (slash != std::string::npos) mtx_name = mtx_name.substr(slash + 1); - auto dot = mtx_name.rfind('.'); - if (dot != std::string::npos) mtx_name = mtx_name.substr(0, dot); - - // Timestamp for CSV - auto now_file = std::chrono::system_clock::now(); - auto t_file = std::chrono::system_clock::to_time_t(now_file); - char ts_file[32]; - std::strftime(ts_file, sizeof(ts_file), "%Y%m%d_%H%M%S", std::localtime(&t_file)); - std::string csv_name = std::string("orth_gap_") + mtx_name + "_" + ts_file + ".csv"; - std::ofstream csv(csv_name); - if (csv.is_open()) { - std::string prec_name = (sizeof(T) == 8) ? "double" : "float"; - csv << "# CQRRT orth gap benchmark (file mode)\n"; - csv << "# precision=" << prec_name << ", file=" << mtx_path << ", m=" << m << ", n=" << n - << ", d_factor=" << d_factor << ", sketch_nnz=" << sketch_nnz - << ", block_size=" << block_size << "\n"; - csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; - } - - printf("\n %-6s %-14s %-14s %-10s %-12s %-12s\n", - "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); - printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", - "---", "-----------", "----------", "-------", "----------", "---------"); - - for (int64_t r = 0; r < num_runs; ++r) { - auto run_state = state; - auto res = run_comparison(A_linop, m, n, (T)d_factor, sketch_nnz, block_size, run_state); - printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", - r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); - if (csv.is_open()) { - csv << r << "," << std::scientific << std::setprecision(6) - << res.linop_orth << "," << res.expl_orth << "," - << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; - } - state = RandBLAS::RNGState(state.key.incr()); - } - if (csv.is_open()) { - csv.close(); - printf(" Results CSV: %s\n", csv_name.c_str()); - } - printf("\n"); - return 0; -} - -int main(int argc, char* argv[]) { - if (argc < 3) { - std::cerr << "Usage:\n" - << " " << argv[0] << " generate [sketch_nnz] [block_size]\n" - << " " << argv[0] << " file [sketch_nnz] [block_size] [compute_cond]\n" - << " " << argv[0] << " composite [sketch_nnz] [block_size]\n" - << " " << argv[0] << " diag [sketch_nnz]\n" - << " " << argv[0] << " diag_gen [sketch_nnz]\n"; - return 1; - } - - std::string precision = argv[1]; - std::string mode = argv[2]; - - // Diagnostic mode: step-by-step comparison on a file - if (mode == "diag") { - // ./CQRRT_orth_gap diag [sketch_nnz] - if (argc < 5) { - std::cerr << "Usage: " << argv[0] << " diag [sketch_nnz]\n"; - return 1; - } - std::string mtx_path = argv[3]; - double d_factor = std::stod(argv[4]); - int64_t sketch_nnz = (argc >= 6) ? std::stol(argv[5]) : 4; - - printf("Loading %s...", mtx_path.c_str()); fflush(stdout); - if (precision == "double") { - auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); - int64_t m = A_coo.n_rows, n = A_coo.n_cols; - printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - auto state = RandBLAS::RNGState(); - run_diagnostic(A_linop, m, n, d_factor, sketch_nnz, state); - } else { - auto A_coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); - int64_t m = A_coo.n_rows, n = A_coo.n_cols; - printf(" done (%ld x %ld, nnz=%ld)\n", m, n, A_coo.nnz); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - auto state = RandBLAS::RNGState(); - run_diagnostic(A_linop, m, n, (float)d_factor, sketch_nnz, state); - } - return 0; - } - - // Diagnostic mode on synthetic matrix - if (mode == "diag_gen") { - // ./CQRRT_orth_gap diag_gen [sketch_nnz] - if (argc < 8) { - std::cerr << "Usage: " << argv[0] << " diag_gen [sketch_nnz]\n"; - return 1; - } - int64_t m = std::stol(argv[3]); - int64_t n = std::stol(argv[4]); - double cond_num = std::stod(argv[5]); - double density = std::stod(argv[6]); - double d_factor = std::stod(argv[7]); - int64_t sketch_nnz = (argc >= 9) ? std::stol(argv[8]) : 4; - - printf("Generating %ld x %ld matrix, kappa=%.2e, density=%.3f...", m, n, cond_num, density); - fflush(stdout); - auto state = RandBLAS::RNGState(); - if (precision == "double") { - auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, cond_num, state, density); - printf(" done (nnz=%ld)\n", A_coo.nnz); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - run_diagnostic(A_linop, m, n, d_factor, sketch_nnz, state); - } else { - auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, (float)cond_num, state, (float)density); - printf(" done (nnz=%ld)\n", A_coo.nnz); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); - run_diagnostic(A_linop, m, n, (float)d_factor, sketch_nnz, state); - } - return 0; - } - - // Composite mode: K.mtx + V.mtx → CholSolver(K, half_solve) ∘ Sparse(V) = L^{-1}V - if (mode == "composite") { - // ./CQRRT_orth_gap composite [nnz] [block_size] - if (argc < 7) { - std::cerr << "Usage: " << argv[0] - << " composite [sketch_nnz] [block_size]\n"; - return 1; - } - std::string K_file = argv[3]; - std::string V_file = argv[4]; - double d_factor = std::stod(argv[5]); - int64_t num_runs = std::stol(argv[6]); - int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; - int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 256; - - if (precision == "double") { - using T = double; - - printf("\n=== CQRRT Orthogonality Gap Benchmark (composite mode) ===\n"); - printf(" K file: %s\n V file: %s\n", K_file.c_str(), V_file.c_str()); - printf(" d_factor=%.2f, sketch_nnz=%ld, block_size=%ld, runs=%ld\n", - d_factor, sketch_nnz, block_size, num_runs); -#ifdef _OPENMP - printf(" OpenMP threads: %d\n", omp_get_max_threads()); -#endif - printf("==========================================================\n\n"); - - // Load V - printf("Loading V from %s...", V_file.c_str()); fflush(stdout); - auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); - int64_t m = V_coo.n_rows, n = V_coo.n_cols; - printf(" done (%ld x %ld, nnz=%ld)\n", m, n, V_coo.nnz); - - RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); - RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); - - // Build CholSolver for K (half_solve = true → L^{-1}) - printf("Factorizing K = LL^T from %s...", K_file.c_str()); fflush(stdout); - RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); - L_inv_op.factorize(); - printf(" done\n"); - - // Composite operator: L^{-1} * V (CTAD deduces template args) - RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); - - printf("Composite operator L^{-1}V: %ld x %ld\n\n", m, n); - - auto state = RandBLAS::RNGState(); - - // CSV output - auto now = std::chrono::system_clock::now(); - auto t = std::chrono::system_clock::to_time_t(now); - char ts[32]; - std::strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", std::localtime(&t)); - std::string csv_name = std::string("orth_gap_composite_") + ts + ".csv"; - std::ofstream csv(csv_name); - if (csv.is_open()) { - csv << "# CQRRT orth gap benchmark (composite mode)\n"; - csv << "# K_file=" << K_file << ", V_file=" << V_file - << ", m=" << m << ", n=" << n - << ", d_factor=" << d_factor << ", sketch_nnz=" << sketch_nnz - << ", block_size=" << block_size << "\n"; - csv << "run,linop_orth,expl_orth,gap_ratio,linop_time_us,expl_time_us\n"; - } - - printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", - "Run", "CQRRT_linop", "CQRRT_expl", "Gap (x)", "Linop (us)", "Expl (us)"); - printf(" %-6s %-14s %-14s %-10s %-12s %-12s\n", - "---", "-----------", "----------", "-------", "----------", "---------"); - - for (int64_t r = 0; r < num_runs; ++r) { - auto run_state = state; - auto res = run_comparison(LiV_op, m, n, (T)d_factor, sketch_nnz, block_size, run_state); - printf(" %-6ld %-14.6e %-14.6e %-10.1f %-12ld %-12ld\n", - r, res.linop_orth, res.expl_orth, res.gap_ratio, res.linop_time_us, res.expl_time_us); - if (csv.is_open()) { - csv << r << "," << std::scientific << std::setprecision(6) - << res.linop_orth << "," << res.expl_orth << "," - << res.gap_ratio << "," << res.linop_time_us << "," << res.expl_time_us << "\n"; - } - state = RandBLAS::RNGState(state.key.incr()); - } - if (csv.is_open()) { csv.close(); printf(" Results CSV: %s\n", csv_name.c_str()); } - printf("\n"); - } else { - std::cerr << "Composite mode only supports double precision\n"; - return 1; - } - return 0; - } - - if (precision == "double") { - if (mode == "generate") return run_generate_mode(argc, argv); - if (mode == "file") return run_file_mode(argc, argv); - } else if (precision == "float") { - if (mode == "generate") return run_generate_mode(argc, argv); - if (mode == "file") return run_file_mode(argc, argv); - } - - std::cerr << "Unknown precision '" << precision << "' or mode '" << mode << "'\n"; - return 1; -} From 4185e71180181fcc81acf8ebdfad689baa8b2abf Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 7 Apr 2026 16:18:20 -0400 Subject: [PATCH 09/47] Small optimizations --- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 183 ++++++++---------- .../bench_CQRRT_linops/CQRRT_linop_basic.cc | 34 ++-- .../CQRRT_linop_composite_applications.cc | 103 ++++------ .../bench_CQRRT_linops/cqrrt_bench_common.hh | 37 ++++ 4 files changed, 172 insertions(+), 185 deletions(-) create mode 100644 benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index e75a215dd..ad5e966fe 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -7,7 +7,7 @@ // [2] expl_inv_trsm: TRSM(I, R_sk) → R_inv; DGEMM(A, R_inv) (TRSM on identity) // [3] expl_inv_trtri: TRTRI(R_sk) → R_inv; DGEMM(A, R_inv) (LAPACK trtri) // [4] expl_inv_geqp3: GEQP3(R_sk) = Q*R_buf*P^T; -// R_inv = P * TRTRI(R_buf) * Q^T; DGEMM(A, R_inv) +// R_inv = P * TRSM(R_buf, Q^T); DGEMM(A, R_inv) // // Path [1] never forms R_sk^{-1} explicitly (backward stable). // Paths [2]-[4] all form R_sk^{-1} explicitly via different methods. @@ -59,6 +59,7 @@ #include "../../extras/misc/ext_util.hh" #include "RandLAPACK/testing/rl_test_utils.hh" +#include "cqrrt_bench_common.hh" using std::chrono::steady_clock; using std::chrono::duration_cast; @@ -69,6 +70,26 @@ using blas::Side; using blas::Uplo; using blas::Diag; +// ============================================================================ +// Path constants +// ============================================================================ + +static constexpr int N_PATHS = 4; + +static constexpr const char* PATH_NAMES[N_PATHS] = { + "expl_trsm", + "expl_inv_trsm", + "expl_inv_trtri", + "expl_inv_geqp3", +}; + +static constexpr const char* PATH_DESCS[N_PATHS] = { + "DTRSM_R(A, R_sk) in-place <- CQRRT_expl", + "TRSM(I, R_sk)->R_inv; DGEMM(A, R_inv)", + "TRTRI(R_sk)->R_inv; DGEMM(A, R_inv)", + "GEQP3(R_sk)=Q*R_buf*P^T; R_inv=P*TRSM(R_buf,Q^T); DGEMM(A, R_inv)", +}; + // ============================================================================ // Helpers // ============================================================================ @@ -136,9 +157,9 @@ static T cholqr_orth_error(const std::vector& A_pre, const T* A_orig, template struct TrialResult { // Per-path metrics - double cond_Apre[4]; - double cond_G[4]; - double orth_Q[4]; + double cond_Apre[N_PATHS]; + double cond_G[N_PATHS]; + double orth_Q[N_PATHS]; // Cross-path relative differences of A_pre (reference = path [1]) double rd_Apre_12; // TRSM in-place vs TRSM-on-identity double rd_Apre_13; // TRSM in-place vs trtri @@ -186,8 +207,7 @@ static TrialResult run_trial( // ---------------------------------------------------------------- // Method A: TRSM on identity (path [2]) - std::vector R_inv_trsm(n * n, 0.0); - RandLAPACK::util::eye(n, n, R_inv_trsm.data()); + auto R_inv_trsm = make_eye(n); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, R_sk.data(), n, R_inv_trsm.data(), n); @@ -237,53 +257,41 @@ static TrialResult run_trial( } // ---------------------------------------------------------------- - // Compute all four A_pre matrices + // Compute all N_PATHS preconditioned matrices: + // Apre[0]: TRSM in-place (path [1], CQRRT_expl) + // Apre[1]: GEMM + R_inv_trsm (path [2]) + // Apre[2]: GEMM + R_inv_trtri (path [3]) + // Apre[3]: GEMM + R_inv_geqp3 (path [4]) // ---------------------------------------------------------------- + std::array, N_PATHS> Apre; + for (auto& a : Apre) a.resize(m * n, T(0)); - // Path [1]: TRSM in-place on A ← CQRRT_expl - std::vector Apre1(A_dense, A_dense + m*n); + Apre[0].assign(A_dense, A_dense + m*n); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, m, n, (T)1.0, - R_sk.data(), n, Apre1.data(), m); - - // Path [2]: explicit inverse via TRSM-on-I + DGEMM - std::vector Apre2(m * n, 0.0); - blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, - A_dense, m, R_inv_trsm.data(), n, - (T)0.0, Apre2.data(), m); - - // Path [3]: explicit inverse via trtri + DGEMM - std::vector Apre3(m * n, 0.0); - blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, - A_dense, m, R_inv_trtri.data(), n, - (T)0.0, Apre3.data(), m); - - // Path [4]: explicit inverse via geqp3 + trtri + Q^T + permutation + DGEMM - std::vector Apre4(m * n, 0.0); - blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, - A_dense, m, R_inv_geqp3.data(), n, - (T)0.0, Apre4.data(), m); + Diag::NonUnit, m, n, (T)1.0, R_sk.data(), n, Apre[0].data(), m); + + const T* R_invs[N_PATHS - 1] = {R_inv_trsm.data(), R_inv_trtri.data(), R_inv_geqp3.data()}; + for (int p = 1; p < N_PATHS; ++p) + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, A_dense, m, R_invs[p-1], n, (T)0.0, Apre[p].data(), m); // ---------------------------------------------------------------- - // Cross-path relative differences (reference = path [1]) + // Cross-path relative differences (reference = path [1] = Apre[0]) // ---------------------------------------------------------------- - res.rd_Apre_12 = (double)rel_diff(Apre1.data(), Apre2.data(), m*n); - res.rd_Apre_13 = (double)rel_diff(Apre1.data(), Apre3.data(), m*n); - res.rd_Apre_14 = (double)rel_diff(Apre1.data(), Apre4.data(), m*n); + res.rd_Apre_12 = (double)rel_diff(Apre[0].data(), Apre[1].data(), m*n); + res.rd_Apre_13 = (double)rel_diff(Apre[0].data(), Apre[2].data(), m*n); + res.rd_Apre_14 = (double)rel_diff(Apre[0].data(), Apre[3].data(), m*n); // ---------------------------------------------------------------- - // Step-by-step pipeline intermediates: paths [1] vs [2] + // Step-by-step pipeline intermediates: paths [1] vs [2] (Apre[0] vs Apre[1]) // G = A_pre^T A_pre (upper triangle via SYRK) // R_chol = chol(G) (POTRF in-place on upper triangle) // R_final = R_chol * R_sk (TRMM) // ---------------------------------------------------------------- - auto make_G = [&](const std::vector& Apre) { + auto make_G = [&](const std::vector& A) { std::vector G(n*n, 0.0); blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, - n, m, (T)1.0, Apre.data(), m, (T)0.0, G.data(), n); + n, m, (T)1.0, A.data(), m, (T)0.0, G.data(), n); return G; }; auto make_Rchol = [&](std::vector G) { @@ -296,8 +304,8 @@ static TrialResult run_trial( return Rchol; }; - auto G1 = make_G(Apre1); - auto G2 = make_G(Apre2); + auto G1 = make_G(Apre[0]); + auto G2 = make_G(Apre[1]); auto Rchol1 = make_Rchol(G1); auto Rchol2 = make_Rchol(G2); auto Rfinal1 = make_Rfinal(Rchol1); @@ -310,20 +318,11 @@ static TrialResult run_trial( // ---------------------------------------------------------------- // Per-path metrics // ---------------------------------------------------------------- - res.cond_Apre[0] = (double)condition_number(Apre1.data(), m, n); - res.cond_Apre[1] = (double)condition_number(Apre2.data(), m, n); - res.cond_Apre[2] = (double)condition_number(Apre3.data(), m, n); - res.cond_Apre[3] = (double)condition_number(Apre4.data(), m, n); - - res.cond_G[0] = (double)gram_condition_number(Apre1.data(), m, n); - res.cond_G[1] = (double)gram_condition_number(Apre2.data(), m, n); - res.cond_G[2] = (double)gram_condition_number(Apre3.data(), m, n); - res.cond_G[3] = (double)gram_condition_number(Apre4.data(), m, n); - - res.orth_Q[0] = (double)cholqr_orth_error(Apre1, A_dense, m, n, R_sk.data()); - res.orth_Q[1] = (double)cholqr_orth_error(Apre2, A_dense, m, n, R_sk.data()); - res.orth_Q[2] = (double)cholqr_orth_error(Apre3, A_dense, m, n, R_sk.data()); - res.orth_Q[3] = (double)cholqr_orth_error(Apre4, A_dense, m, n, R_sk.data()); + for (int p = 0; p < N_PATHS; ++p) { + res.cond_Apre[p] = (double)condition_number(Apre[p].data(), m, n); + res.cond_G[p] = (double)gram_condition_number(Apre[p].data(), m, n); + res.orth_Q[p] = (double)cholqr_orth_error(Apre[p], A_dense, m, n, R_sk.data()); + } return res; } @@ -349,21 +348,15 @@ int run_benchmark(int argc, char* argv[]) { // ---------------------------------------------------------------- // Load matrix and materialize as dense // ---------------------------------------------------------------- - auto coo = RandLAPACK_extras::coo_from_matrix_market(mtx_path); - int64_t m = coo.n_rows, n = coo.n_cols; - - RandBLAS::sparse_data::csr::CSRMatrix csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(coo, csr); - RandLAPACK::linops::SparseLinOp> - A_linop(m, n, csr); + int64_t m, n, nnz; + auto csr = load_csr(mtx_path, m, n, nnz); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, csr); std::vector A_dense(m * n, 0.0); { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); + auto Eye = make_eye(n); A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_dense.data(), m); - delete[] Eye; + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); } T cond_A = condition_number(A_dense.data(), m, n); @@ -371,7 +364,7 @@ int run_benchmark(int argc, char* argv[]) { std::cout << "\n=== CQRRT Preconditioner Comparison ===\n"; std::cout << " Matrix: " << mtx_path << "\n"; - std::cout << " Size: " << m << " x " << n << " (nnz=" << coo.nnz << ")\n"; + std::cout << " Size: " << m << " x " << n << " (nnz=" << nnz << ")\n"; std::cout << " d_factor: " << d_factor << " (d=" << d << ")\n"; std::cout << " sketch_nnz: " << sketch_nnz << "\n"; std::cout << " runs: " << num_runs << "\n"; @@ -414,24 +407,18 @@ int run_benchmark(int argc, char* argv[]) { auto res = run_trial(A_dense.data(), m, n, d_factor, sketch_nnz, state); printf(" run %ld orth_error(Q = A * R_final^{-1}):\n", r); - printf(" [1] expl_trsm: %12.3e\n", res.orth_Q[0]); - printf(" [2] expl_inv_trsm: %12.3e\n", res.orth_Q[1]); - printf(" [3] expl_inv_trtri: %12.3e\n", res.orth_Q[2]); - printf(" [4] expl_inv_geqp3: %12.3e\n", res.orth_Q[3]); - - printf(" run %ld cond(A_pre):\n", r); - printf(" [1] expl_trsm: %12.3e\n", res.cond_Apre[0]); - printf(" [2] expl_inv_trsm: %12.3e\n", res.cond_Apre[1]); - printf(" [3] expl_inv_trtri: %12.3e\n", res.cond_Apre[2]); - printf(" [4] expl_inv_geqp3: %12.3e\n", res.cond_Apre[3]); - - printf(" run %ld cond(G = A_pre^T A_pre):\n", r); - printf(" [1] expl_trsm: %12.3e\n", res.cond_G[0]); - printf(" [2] expl_inv_trsm: %12.3e\n", res.cond_G[1]); - printf(" [3] expl_inv_trtri: %12.3e\n", res.cond_G[2]); - printf(" [4] expl_inv_geqp3: %12.3e\n", res.cond_G[3]); - - printf(" run %ld rel_diff(A_pre) vs [1]:\n", r); + for (int p = 0; p < N_PATHS; ++p) + printf(" [%d] %-18s %12.3e\n", p+1, PATH_NAMES[p], res.orth_Q[p]); + + printf(" run %ld cond(MR^pre):\n", r); + for (int p = 0; p < N_PATHS; ++p) + printf(" [%d] %-18s %12.3e\n", p+1, PATH_NAMES[p], res.cond_Apre[p]); + + printf(" run %ld cond(G = MR^pre^T MR^pre):\n", r); + for (int p = 0; p < N_PATHS; ++p) + printf(" [%d] %-18s %12.3e\n", p+1, PATH_NAMES[p], res.cond_G[p]); + + printf(" run %ld rel_diff(MR^pre) vs [1]:\n", r); printf(" rd_12 (trsm-on-I): %12.3e\n", res.rd_Apre_12); printf(" rd_13 (trtri): %12.3e\n", res.rd_Apre_13); printf(" rd_14 (geqp3): %12.3e\n", res.rd_Apre_14); @@ -446,27 +433,19 @@ int run_benchmark(int argc, char* argv[]) { printf(" run %ld cond(R_sk): %9.3e\n\n", r, res.cond_Rsk); - csv << r << "," - << std::scientific << std::setprecision(6) - << res.orth_Q[0] << "," << res.orth_Q[1] << "," - << res.orth_Q[2] << "," << res.orth_Q[3] << "," - << res.cond_Apre[0] << "," << res.cond_Apre[1] << "," - << res.cond_Apre[2] << "," << res.cond_Apre[3] << "," - << res.cond_G[0] << "," << res.cond_G[1] << "," - << res.cond_G[2] << "," << res.cond_G[3] << "," - << res.rd_Apre_12 << "," << res.rd_Apre_13 << "," - << res.rd_Apre_14 << "," - << res.rd_G_12 << "," << res.rd_Rchol_12 << "," - << res.rd_Rfinal_12 << "," - << res.cond_Rsk << "\n"; + csv << r << "," << std::scientific << std::setprecision(6); + for (int p = 0; p < N_PATHS; ++p) csv << res.orth_Q[p] << ","; + for (int p = 0; p < N_PATHS; ++p) csv << res.cond_Apre[p] << ","; + for (int p = 0; p < N_PATHS; ++p) csv << res.cond_G[p] << ","; + csv << res.rd_Apre_12 << "," << res.rd_Apre_13 << "," << res.rd_Apre_14 << "," + << res.rd_G_12 << "," << res.rd_Rchol_12 << "," << res.rd_Rfinal_12 << "," + << res.cond_Rsk << "\n"; } csv.close(); std::cout << " Legend:\n"; - std::cout << " [1] expl_trsm: DTRSM_R(A, R_sk) in-place <- CQRRT_expl\n"; - std::cout << " [2] expl_inv_trsm: TRSM(I, R_sk)->R_inv; DGEMM(A, R_inv)\n"; - std::cout << " [3] expl_inv_trtri: TRTRI(R_sk)->R_inv; DGEMM(A, R_inv)\n"; - std::cout << " [4] expl_inv_geqp3: GEQP3(R_sk)=Q*R_buf*P^T; R_inv=P*TRTRI(R_buf)*Q^T; DGEMM(A, R_inv)\n"; + for (int p = 0; p < N_PATHS; ++p) + printf(" [%d] %-18s %s\n", p+1, PATH_NAMES[p], PATH_DESCS[p]); std::cout << "\n CSV written to: " << csv_path << "\n"; return 0; diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc index 7b8eb0145..4f250b5ea 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc @@ -22,6 +22,7 @@ // Extras utilities for Matrix Market I/O #include "../../extras/misc/ext_util.hh" #include "RandLAPACK/testing/rl_test_utils.hh" +#include "cqrrt_bench_common.hh" // Linops algorithms (now in main RandLAPACK) #include "rl_cqrrt_linops.hh" @@ -68,13 +69,9 @@ template static void compute_Q_from_R( GLO& A_op, T* R, int64_t ldr, T* Q_out, int64_t m, int64_t n) { - // Step 1: Materialize A into Q_out: Q_out = A * I - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); + auto Eye = make_eye(n); A_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); - delete[] Eye; - // Step 2: Solve Q * R = A for Q via trsm (backward stable, no explicit inverse) + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, Q_out, m); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); } @@ -203,8 +200,9 @@ static std::vector> run_algorithms( sCholQR3_alg.call(A_linop, R_scholqr3.data(), n); results[run].scholqr3.peak_rss_kb = scholqr3_mem.stop(); - results[run].scholqr3.time = sCholQR3_alg.times[12]; // total - results[run].scholqr3.breakdown.assign(sCholQR3_alg.times.begin(), sCholQR3_alg.times.begin() + 12); + results[run].scholqr3.time = sCholQR3_alg.times[17]; // total + // breakdown (17): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest + results[run].scholqr3.breakdown.assign(sCholQR3_alg.times.begin(), sCholQR3_alg.times.begin() + 17); // Uniform Q computation (same as all other algorithms) compute_Q_from_R(A_linop, R_scholqr3.data(), n, Q_uniform.data(), m, n); @@ -225,18 +223,15 @@ static std::vector> run_algorithms( dense_mem.start(); // Step 1: Materialize the operator by multiplying with identity - T* I_mat = new T[n * n](); - RandLAPACK::util::eye(n, n, I_mat); + auto I_mat = make_eye(n); T* A_materialized = new T[m * n](); auto materialize_start = steady_clock::now(); A_linop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, I_mat, n, (T)0.0, A_materialized, m); + m, n, n, (T)1.0, I_mat.data(), n, (T)0.0, A_materialized, m); auto materialize_stop = steady_clock::now(); long materialize_time = duration_cast(materialize_stop - materialize_start).count(); - delete[] I_mat; - // Step 2: Call rl_cqrrt with timing, Q-factor disabled (computed uniformly below) // Uses same per-run RNG state as CQRRT_linop for fair comparison std::vector R_dense(n * n, 0.0); @@ -354,12 +349,9 @@ static std::vector> run_single_test_from_file( } // Load matrix from Matrix Market file - auto A_coo = RandLAPACK_extras::coo_from_matrix_market(filename); - int64_t m = A_coo.n_rows; - int64_t n = A_coo.n_cols; - T actual_density = static_cast(A_coo.nnz) / (static_cast(m) * n); - RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + int64_t m, n, nnz; + auto A_csr = load_csr(filename, m, n, nnz); + T actual_density = static_cast(nnz) / (static_cast(m) * n); RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); T cond_num = std::numeric_limits::quiet_NaN(); @@ -630,12 +622,12 @@ static void write_csv_headers( breakdown << "# Times are in microseconds\n"; breakdown << "# CQRRT_linop: alloc, saso, qr, trtri, linop_precond, linop_gram, trmm_gram, potrf, finalize, rest, total\n"; breakdown << "# CholQR: alloc, materialize, gram, potrf, rest, total\n"; - breakdown << "# sCholQR3: alloc, materialize, gram1, potrf1, trsm1, syrk2, potrf2, update2, syrk3, potrf3, update3, rest, total\n"; + breakdown << "# sCholQR3: alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total\n"; breakdown << "# CQRRT_expl: materialize, saso, qr, trtri(=0), precond, gram, trmm_gram(=0), potrf, finalize, rest, total\n"; breakdown << "m,n,run," << "cqrrt_alloc,cqrrt_saso,cqrrt_qr,cqrrt_trtri,cqrrt_linop_precond,cqrrt_linop_gram,cqrrt_trmm_gram,cqrrt_potrf,cqrrt_finalize,cqrrt_rest,cqrrt_total," << "cholqr_alloc,cholqr_materialize,cholqr_gram,cholqr_potrf,cholqr_rest,cholqr_total," - << "scholqr3_alloc,scholqr3_materialize,scholqr3_gram1,scholqr3_potrf1,scholqr3_trsm1,scholqr3_syrk2,scholqr3_potrf2,scholqr3_update2,scholqr3_syrk3,scholqr3_potrf3,scholqr3_update3,scholqr3_rest,scholqr3_total," + << "scholqr3_alloc,scholqr3_fwd1,scholqr3_adj1,scholqr3_chol1,scholqr3_upd1,scholqr3_fwd2,scholqr3_adj2,scholqr3_gemm2,scholqr3_chol2,scholqr3_upd2,scholqr3_fwd3,scholqr3_adj3,scholqr3_gemm3,scholqr3_chol3,scholqr3_upd3,scholqr3_q_mat,scholqr3_rest,scholqr3_total," << "dense_materialize,dense_saso,dense_qr,dense_trtri,dense_precond,dense_gram,dense_trmm_gram,dense_potrf,dense_finalize,dense_rest,dense_total," << "cqrrt_peak_rss_kb,cqrrt_analytical_kb," << "cholqr_peak_rss_kb,cholqr_analytical_kb," diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc index 6b7258d0b..f868cda53 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc @@ -34,6 +34,7 @@ #include #include #include +#include #ifdef _OPENMP #include #endif @@ -45,6 +46,7 @@ #include "../../extras/misc/ext_util.hh" #include "../../extras/linops/ext_cholsolver_linop.hh" #include "RandLAPACK/testing/rl_test_utils.hh" +#include "cqrrt_bench_common.hh" // Linops algorithms (now in main RandLAPACK) #include "rl_cqrrt_linops.hh" @@ -111,11 +113,9 @@ template static void compute_Q_from_R( GLO& A_op, T* R, int64_t ldr, T* Q_out, int64_t m, int64_t n) { - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); + auto Eye = make_eye(n); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); - delete[] Eye; + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, Q_out, m); blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); } @@ -146,9 +146,7 @@ static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); // Copy into AtA columns j0..j0+bk - for (int64_t j = 0; j < bk; ++j) - for (int64_t i = 0; i < n; ++i) - AtA[i + (j0 + j) * n] = AtA_block[i + j * n]; + lapack::lacpy(lapack::MatrixType::General, n, bk, AtA_block.data(), n, AtA + j0 * n, n); } } @@ -166,10 +164,7 @@ static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); - #pragma omp parallel for schedule(static) - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - RtR[i + j * n] = RtR[j + i * n]; + fill_lower_from_upper(RtR.data(), n); // ||A||_F^2 = trace(A^T A) T norm_A_sq = 0; @@ -201,11 +196,9 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, A_ptr = A_dense; } else { A_buf.resize(m * n); - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); + auto Eye = make_eye(n); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_buf.data(), m); - delete[] Eye; + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_buf.data(), m); A_ptr = A_buf.data(); } @@ -525,15 +518,13 @@ static int run_benchmark_inner( // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) // ================================================================ T* A_materialized = nullptr; - if (upcast_orth || (method_mask & 16) || (method_mask & 32)) { + if (upcast_orth || (method_mask & 48)) { std::cout << "Materializing " << op_label << " for upcast/expl/stb (" << m << " x " << n << ", " << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; A_materialized = new T[m * n]; - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); + auto Eye = make_eye(n); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye, n, (T)0.0, A_materialized, m); - delete[] Eye; + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_materialized, m); std::cout << "done\n\n"; } @@ -546,9 +537,7 @@ static int run_benchmark_inner( AtA_precomputed.resize(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - AtA_precomputed[i + j * n] = AtA_precomputed[j + i * n]; + fill_lower_from_upper(AtA_precomputed.data(), n); for (int64_t i = 0; i < n; ++i) norm_A_sq_precomputed += AtA_precomputed[i + i * n]; } @@ -572,8 +561,7 @@ static int run_benchmark_inner( // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop if (A_materialized) { - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) Q_buf[i] = A_materialized[i]; + lapack::lacpy(lapack::MatrixType::General, m, n, A_materialized, m, Q_buf.data(), m); blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); } else { @@ -588,10 +576,7 @@ static int run_benchmark_inner( std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); - #pragma omp parallel for schedule(static) - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - RtR[i + j * n] = RtR[j + i * n]; + fill_lower_from_upper(RtR.data(), n); T diff_sq = 0; #pragma omp parallel for reduction(+:diff_sq) schedule(static) @@ -635,13 +620,14 @@ static int run_benchmark_inner( }; // ================================================================ - // CQRRT_linop + // CQRRT_linop variants (standard TRSM_IDENTITY and GEQP3-stabilized) // ================================================================ - if (method_mask & 1) { - run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { + auto run_cqrrt_linop = [&](const std::string& name, RandLAPACK::CQRRTLinopPrecond precond) { + run_algo(name, [&](gsvd_result& res, std::vector& R, int64_t r) { auto state = run_states[r]; RandLAPACK::CQRRT_linops algo(true, tol, false); algo.nnz = sketch_nnz; algo.block_size = block_size; + algo.precond_method = precond; RandLAPACK::PeakRSSTracker mem; mem.start(); algo.call(A_op, R.data(), n, d_factor, state); res.peak_rss_kb = mem.stop(); @@ -650,7 +636,9 @@ static int run_benchmark_inner( res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); }); - } + }; + if (method_mask & 1) + run_cqrrt_linop("CQRRT_linop", RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY); // ================================================================ // CholQR @@ -707,10 +695,13 @@ static int run_benchmark_inner( // ================================================================ if (method_mask & 16) { run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { - // Copy A_materialized since CQRRT modifies it in-place + // Time the copy: CQRRT modifies A in-place, so a fresh copy is needed each run. + // This per-run materialization cost is not captured by algo.times — store in slot [2]. T* A_copy = new T[m * n]; - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) A_copy[i] = A_materialized[i]; + auto copy_start = std::chrono::steady_clock::now(); + lapack::lacpy(lapack::MatrixType::General, m, n, A_materialized, m, A_copy, m); + auto copy_end = std::chrono::steady_clock::now(); + long copy_dur = std::chrono::duration_cast(copy_end - copy_start).count(); auto state = run_states[r]; RandLAPACK::CQRRT algo(true, tol); @@ -721,11 +712,16 @@ static int run_benchmark_inner( RandLAPACK::PeakRSSTracker mem; mem.start(); algo.call(m, n, A_copy, m, R.data(), n, d_factor, state); res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times.back(); // total time is last entry - // CQRRT_expl breakdown matches CQRRT_linop structure approximately + + // CQRRT dense times layout (10 entries, 0-indexed): + // [0]=saso, [1]=qr, [2]=trtri(0), [3]=precond, [4]=gram, + // [5]=trmm_gram(0), [6]=potrf, [7]=finalize, [8]=rest, [9]=total + // Store copy_dur in slot [2] (repurposed trtri placeholder) and include in total. res.qr_breakdown.assign(algo.times.begin(), algo.times.end()); - // Pad to standard length while (res.qr_breakdown.size() < 11) res.qr_breakdown.push_back(0); + res.qr_breakdown[2] = copy_dur; + res.qr_breakdown[9] += copy_dur; // update total to include copy + res.qr_time_us = res.qr_breakdown[9]; res.analytical_kb = (m * n * sizeof(T)) / 1024; // just the dense matrix delete[] A_copy; @@ -735,22 +731,8 @@ static int run_benchmark_inner( // ================================================================ // CQRRT_linop_stb (GEQP3-stabilized preconditioner, uses pre-materialized A for orth check) // ================================================================ - if (method_mask & 32) { - run_algo("CQRRT_linop_stb", [&](gsvd_result& res, std::vector& R, int64_t r) { - auto state = run_states[r]; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; - algo.block_size = block_size; - algo.precond_method = RandLAPACK::CQRRTLinopPrecond::GEQP3; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[10]; - // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); - }); - } + if (method_mask & 32) + run_cqrrt_linop("CQRRT_linop_stb", RandLAPACK::CQRRTLinopPrecond::GEQP3); // Free pre-materialized A delete[] A_materialized; @@ -850,13 +832,10 @@ int run_benchmark(int argc, char* argv[]) { // Step 1: Load A (sparse mode) or V (composite mode) from Matrix Market // ================================================================ std::cout << "Loading " << (sparse_mode ? "A" : "V") << " from " << V_file << "... " << std::flush; - auto V_coo = RandLAPACK_extras::coo_from_matrix_market(V_file); - int64_t m = V_coo.n_rows; - int64_t n = V_coo.n_cols; - RandBLAS::sparse_data::csr::CSRMatrix V_csr(m, n); - RandBLAS::sparse_data::conversions::coo_to_csr(V_coo, V_csr); + int64_t m, n, nnz_V; + auto V_csr = load_csr(V_file, m, n, nnz_V); RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); - std::cout << "done (" << m << " x " << n << ", nnz=" << V_coo.nnz << ")\n"; + std::cout << "done (" << m << " x " << n << ", nnz=" << nnz_V << ")\n"; if (m < n) { std::cerr << "Error: matrix must be overdetermined (m >= n), got " << m << "x" << n << "\n"; @@ -948,7 +927,7 @@ int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth]\n"; + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask]\n"; return 1; } diff --git a/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh b/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh new file mode 100644 index 000000000..bea34a1e8 --- /dev/null +++ b/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh @@ -0,0 +1,37 @@ +// cqrrt_bench_common.hh — shared utilities for CQRRT linop benchmarks +#pragma once + +#include "RandLAPACK.hh" +#include "../../extras/misc/ext_util.hh" +#include + +#include +#include + +// Load a Matrix Market file into a CSRMatrix. Sets m, n, nnz on exit. +template +static RandBLAS::sparse_data::csr::CSRMatrix load_csr( + const std::string& path, int64_t& m, int64_t& n, int64_t& nnz) +{ + auto coo = RandLAPACK_extras::coo_from_matrix_market(path); + m = coo.n_rows; n = coo.n_cols; nnz = coo.nnz; + RandBLAS::sparse_data::csr::CSRMatrix csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(coo, csr); + return csr; +} + +// Return an n x n identity matrix (column-major). +template +static std::vector make_eye(int64_t n) { + std::vector I(n * n, T(0)); + RandLAPACK::util::eye(n, n, I.data()); + return I; +} + +// Fill lower triangle of a column-major symmetric n x n matrix from its upper triangle. +template +static void fill_lower_from_upper(T* M, int64_t n) { + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j + 1; i < n; ++i) + M[i + j * n] = M[j + i * n]; +} From 7a277806e23213228d487f27689da41b566795cf Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 7 Apr 2026 16:38:19 -0400 Subject: [PATCH 10/47] Script name update --- benchmark/CMakeLists.txt | 2 +- ...op_composite_applications.cc => CQRRT_linop_applications.cc} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename benchmark/bench_CQRRT_linops/{CQRRT_linop_composite_applications.cc => CQRRT_linop_applications.cc} (99%) diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 4b0906442..2d5be5054 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -156,7 +156,7 @@ set( Eigen3::Eigen fast_matrix_market ) -add_benchmark(NAME CQRRT_linop_composite_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_composite_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +add_benchmark(NAME CQRRT_linop_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_diagnostic CXX_SOURCES bench_CQRRT_linops/CQRRT_diagnostic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc similarity index 99% rename from benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc rename to benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index f868cda53..f34f4d24b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_composite_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -10,7 +10,7 @@ // 7. Application (c): Generalized singular vectors — full SVD of R // // Usage: -// ./CQRRT_linop_composite_applications +// ./CQRRT_linop_applications // [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask] // // method_mask (integer bitmask, optional, default = 0b001111 or 0b011111 if run_expl=1): From cb1af26300a93754da9abf41062bf6a19ef6a83f Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 7 Apr 2026 21:38:05 -0400 Subject: [PATCH 11/47] Benchmark fix --- benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index f34f4d24b..cb71a2abd 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -518,8 +518,8 @@ static int run_benchmark_inner( // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) // ================================================================ T* A_materialized = nullptr; - if (upcast_orth || (method_mask & 48)) { - std::cout << "Materializing " << op_label << " for upcast/expl/stb (" << m << " x " << n << ", " + if (upcast_orth || (method_mask & 16)) { + std::cout << "Materializing " << op_label << " for upcast/expl (" << m << " x " << n << ", " << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; A_materialized = new T[m * n]; auto Eye = make_eye(n); From 0ceeb73df5197902b47e06ba75a0b3711fac37c1 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 9 Apr 2026 11:43:25 -0400 Subject: [PATCH 12/47] Added BQRRP option to CQRRT --- RandLAPACK/drivers/rl_cqrrt_linops.hh | 44 ++++- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 182 ++++++++++++------ .../CQRRT_linop_applications.cc | 26 ++- 3 files changed, 181 insertions(+), 71 deletions(-) diff --git a/RandLAPACK/drivers/rl_cqrrt_linops.hh b/RandLAPACK/drivers/rl_cqrrt_linops.hh index 928aff9a9..a97275afb 100644 --- a/RandLAPACK/drivers/rl_cqrrt_linops.hh +++ b/RandLAPACK/drivers/rl_cqrrt_linops.hh @@ -4,6 +4,7 @@ #include "rl_blaspp.hh" #include "rl_lapackpp.hh" #include "rl_linops.hh" +#include "rl_bqrrp.hh" #include #include @@ -21,9 +22,14 @@ namespace RandLAPACK { /// GEQP3 — stabilized method: GEQP3(R_sk) = Q*R_buf*P^T, /// then R_sk^{-1} = P * R_buf^{-1} * Q^T via ungqr + TRSM. /// More expensive (O(11n³/6)) but accurate for ill-conditioned R_sk. +/// BQRRP — stabilized method using blocked randomized QRCP instead of GEQP3. +/// Same output format as GEQP3; may be faster for large n due to +/// cache-friendly blocked structure and sketched pivot selection. +/// Block size is chosen adaptively via bqrrp_block_ratio. enum class CQRRTLinopPrecond { TRSM_IDENTITY, - GEQP3 + GEQP3, + BQRRP }; /// Sketch-preconditioned Cholesky QR for abstract linear operators. @@ -70,9 +76,16 @@ class CQRRT_linops { // Method for computing R_sk^{-1}. See CQRRTLinopPrecond enum. // Default: TRSM_IDENTITY (original behavior). - // Set to GEQP3 for the stabilized variant. + // Set to GEQP3 or BQRRP for the stabilized variants. CQRRTLinopPrecond precond_method; + // Block size ratio for BQRRP preconditioner (precond_method == BQRRP only). + // The BQRRP block size is set to max(1, n * bqrrp_block_ratio). + // When precond_method == BQRRP and bqrrp_block_ratio == 1.0 (default), + // the ratio is overridden adaptively inside call() using the same + // heuristic as CQRRPT: 1.0 for n ≤ 2000, 0.5 for n ≤ 8000, 1/32 otherwise. + T bqrrp_block_ratio; + // Column-block size for the precondition + Gram computation. // // When block_size > 0, the two expensive linear operator calls: @@ -110,6 +123,7 @@ class CQRRT_linops { use_dense_sketch = false; block_size = 0; precond_method = CQRRTLinopPrecond::TRSM_IDENTITY; + bqrrp_block_ratio = (T)1.0; test_mode = enable_test_mode; Q = nullptr; Q_rows = 0; @@ -262,10 +276,13 @@ class CQRRT_linops { } R_sk_inv = Eye; } else { - // Stabilized: GEQP3(R_sk) = Q_buf * R_buf * P^T - // R_sk^{-1} = P * R_buf^{-1} * Q_buf^T - // Via: ungqr (explicit Q_buf) + TRSM (W = R_buf^{-1} * Q_buf^T) + scatter by jpiv - // R_sk_inv is dense (not upper triangular). + // Stabilized QRCP-based inversion (GEQP3 or BQRRP). + // Both methods produce the same output format: + // R_sk_copy: Householder reflectors for Q_buf in lower triangle, + // R_buf in upper triangle (1-based jpiv, same as LAPACK GEQP3) + // The ungqr + TRSM + scatter code below is identical for both. + // + // Result: R_sk^{-1} = P * R_buf^{-1} * Q_buf^T (dense, n×n) // Extract R_sk from A_hat upper triangle into n×n buffer T* R_sk_copy = new T[n * n](); @@ -280,9 +297,20 @@ class CQRRT_linops { return 1; } - int64_t* jpiv = new int64_t[n](); + int64_t* jpiv = new int64_t[n](); T* tau_qr = new T[n]; - lapack::geqp3(n, n, R_sk_copy, n, jpiv, tau_qr); + + if (this->precond_method == CQRRTLinopPrecond::GEQP3) { + lapack::geqp3(n, n, R_sk_copy, n, jpiv, tau_qr); + } else { + // BQRRP: blocked randomized QRCP on R_sk (n×n). + // Adaptive block size matches CQRRPT's heuristic. + if (n <= 2000) this->bqrrp_block_ratio = (T)1.0; + else if (n <= 8000) this->bqrrp_block_ratio = (T)0.5; + else this->bqrrp_block_ratio = (T)1.0 / (T)32; + RandLAPACK::BQRRP bqrrp(false, (int64_t)(n * this->bqrrp_block_ratio)); + bqrrp.call(n, n, R_sk_copy, n, (T)1.0, tau_qr, jpiv, state); + } // Extract upper triangular R_buf before overwriting R_sk_copy with Q_buf T* R_buf = new T[n * n](); diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index ad5e966fe..2d886ea7a 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -38,13 +38,17 @@ // Sketch diagnostic: // cond(R_sk) // -// Usage: +// Usage (file mode): // ./CQRRT_diagnostic [sketch_nnz] +// +// Usage (generate mode): +// ./CQRRT_diagnostic gen [sketch_nnz] #include "RandLAPACK.hh" #include "rl_blaspp.hh" #include "rl_lapackpp.hh" #include "rl_gen.hh" +#include "rl_cqrrt_linops.hh" #include #include @@ -328,66 +332,33 @@ static TrialResult run_trial( } // ============================================================================ -// Main benchmark +// Shared: write CSV header and run trials given a dense matrix // ============================================================================ template -int run_benchmark(int argc, char* argv[]) { - if (argc < 6) { - std::cerr << "Usage: " << argv[0] - << " [sketch_nnz]\n"; - return 1; - } - - std::string output_dir = argv[2]; - std::string mtx_path = argv[3]; - T d_factor = (T)std::stod(argv[4]); - int64_t num_runs = std::stol(argv[5]); - int64_t sketch_nnz = (argc >= 7) ? std::stol(argv[6]) : 4; - - // ---------------------------------------------------------------- - // Load matrix and materialize as dense - // ---------------------------------------------------------------- - int64_t m, n, nnz; - auto csr = load_csr(mtx_path, m, n, nnz); - RandLAPACK::linops::SparseLinOp> A_linop(m, n, csr); - - std::vector A_dense(m * n, 0.0); - { - auto Eye = make_eye(n); - A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); - } - - T cond_A = condition_number(A_dense.data(), m, n); - int64_t d = (int64_t)std::ceil(d_factor * n); - - std::cout << "\n=== CQRRT Preconditioner Comparison ===\n"; - std::cout << " Matrix: " << mtx_path << "\n"; - std::cout << " Size: " << m << " x " << n << " (nnz=" << nnz << ")\n"; - std::cout << " d_factor: " << d_factor << " (d=" << d << ")\n"; - std::cout << " sketch_nnz: " << sketch_nnz << "\n"; - std::cout << " runs: " << num_runs << "\n"; - std::cout << " cond(A): " << std::scientific << std::setprecision(3) << cond_A << "\n"; -#ifdef _OPENMP - std::cout << " OMP threads: " << omp_get_max_threads() << "\n"; -#endif - std::cout << "\n"; - - // ---------------------------------------------------------------- - // CSV setup - // ---------------------------------------------------------------- +static void write_csv_and_run( + const std::vector& A_dense, + int64_t m, int64_t n, + T d_factor, int64_t sketch_nnz, int64_t num_runs, + T cond_A, double kappa_target, // kappa_target < 0 means "from file" + const std::string& matrix_label, + const std::string& output_dir) +{ char time_buf[64]; time_t now = time(nullptr); strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); std::string csv_path = output_dir + "/diagnostic_" + time_buf + ".csv"; std::ofstream csv(csv_path); + csv << "# CQRRT Preconditioner Comparison\n"; csv << "# Date: " << ctime(&now); - csv << "# Matrix: " << mtx_path << "\n"; + csv << "# Matrix: " << matrix_label << "\n"; csv << "# m=" << m << " n=" << n << " d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz << "\n"; - csv << "# cond_A=" << cond_A << "\n"; + csv << "# cond_A=" << std::scientific << std::setprecision(6) << cond_A; + if (kappa_target > 0) + csv << " kappa_target=" << std::scientific << std::setprecision(6) << kappa_target; + csv << "\n"; csv << "run," << "orth_Q1,orth_Q2,orth_Q3,orth_Q4," << "cond_Apre1,cond_Apre2,cond_Apre3,cond_Apre4," @@ -396,9 +367,6 @@ int run_benchmark(int argc, char* argv[]) { << "rd_G_12,rd_Rchol_12,rd_Rfinal_12," << "cond_Rsk\n"; - // ---------------------------------------------------------------- - // Runs - // ---------------------------------------------------------------- RandBLAS::RNGState base_state(42); for (int64_t r = 0; r < num_runs; ++r) { auto state = base_state; @@ -447,14 +415,120 @@ int run_benchmark(int argc, char* argv[]) { for (int p = 0; p < N_PATHS; ++p) printf(" [%d] %-18s %s\n", p+1, PATH_NAMES[p], PATH_DESCS[p]); std::cout << "\n CSV written to: " << csv_path << "\n"; +} + +// ============================================================================ +// Main benchmark +// ============================================================================ + +template +int run_benchmark(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage (file mode): " << argv[0] + << " [sketch_nnz]\n" + << "Usage (generate mode): " << argv[0] + << " gen [sketch_nnz]\n"; + return 1; + } + + std::string output_dir = argv[2]; + bool is_generate = (argc >= 4 && std::string(argv[3]) == "gen"); + + if (is_generate) { + // generate mode: prec output_dir gen m n kappa density d_factor runs [sketch_nnz] + if (argc < 11) { + std::cerr << "Usage (generate mode): " << argv[0] + << " gen [sketch_nnz]\n"; + return 1; + } + int64_t m = std::stol(argv[4]); + int64_t n = std::stol(argv[5]); + T kappa = (T)std::stod(argv[6]); + T density = (T)std::stod(argv[7]); + T d_factor = (T)std::stod(argv[8]); + int64_t num_runs = std::stol(argv[9]); + int64_t sketch_nnz = (argc >= 11) ? std::stol(argv[10]) : 4; + + std::cout << "\n=== CQRRT Preconditioner Comparison (generate mode) ===\n"; + std::cout << " Size: " << m << " x " << n << "\n"; + std::cout << " kappa: " << std::scientific << std::setprecision(3) << (double)kappa << "\n"; + std::cout << " density: " << density << "\n"; + std::cout << " d_factor: " << d_factor << "\n"; + std::cout << " sketch_nnz: " << sketch_nnz << "\n"; + std::cout << " runs: " << num_runs << "\n"; +#ifdef _OPENMP + std::cout << " OMP threads: " << omp_get_max_threads() << "\n"; +#endif + + RandBLAS::RNGState gen_state(0); + auto A_coo = RandLAPACK::gen::gen_sparse_cond_coo(m, n, kappa, gen_state, density); + RandBLAS::sparse_data::csr::CSRMatrix A_csr(m, n); + RandBLAS::sparse_data::conversions::coo_to_csr(A_coo, A_csr); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + + std::vector A_dense(m * n, 0.0); + { + auto Eye = make_eye(n); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); + } + T cond_A = condition_number(A_dense.data(), m, n); + std::cout << " cond(A): " << std::scientific << std::setprecision(3) << (double)cond_A << "\n\n"; + + std::string label = "gen_" + std::to_string(m) + "x" + std::to_string(n) + + "_kappa" + std::to_string((int)std::round(std::log10((double)kappa))); + write_csv_and_run(A_dense, m, n, d_factor, sketch_nnz, num_runs, + cond_A, (double)kappa, label, output_dir); + } else { + // file mode: prec output_dir mtx_path d_factor runs [sketch_nnz] + if (argc < 6) { + std::cerr << "Usage (file mode): " << argv[0] + << " [sketch_nnz]\n"; + return 1; + } + std::string mtx_path = argv[3]; + T d_factor = (T)std::stod(argv[4]); + int64_t num_runs = std::stol(argv[5]); + int64_t sketch_nnz = (argc >= 7) ? std::stol(argv[6]) : 4; + + int64_t m, n, nnz; + auto csr = load_csr(mtx_path, m, n, nnz); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, csr); + + std::vector A_dense(m * n, 0.0); + { + auto Eye = make_eye(n); + A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); + } + T cond_A = condition_number(A_dense.data(), m, n); + int64_t d = (int64_t)std::ceil(d_factor * n); + + std::cout << "\n=== CQRRT Preconditioner Comparison ===\n"; + std::cout << " Matrix: " << mtx_path << "\n"; + std::cout << " Size: " << m << " x " << n << " (nnz=" << nnz << ")\n"; + std::cout << " d_factor: " << d_factor << " (d=" << d << ")\n"; + std::cout << " sketch_nnz: " << sketch_nnz << "\n"; + std::cout << " runs: " << num_runs << "\n"; + std::cout << " cond(A): " << std::scientific << std::setprecision(3) << (double)cond_A << "\n"; +#ifdef _OPENMP + std::cout << " OMP threads: " << omp_get_max_threads() << "\n"; +#endif + std::cout << "\n"; + + write_csv_and_run(A_dense, m, n, d_factor, sketch_nnz, num_runs, + cond_A, -1.0, mtx_path, output_dir); + } return 0; } int main(int argc, char* argv[]) { if (argc < 2) { - std::cerr << "Usage: " << argv[0] - << " [sketch_nnz]\n"; + std::cerr << "Usage (file mode): " << argv[0] + << " [sketch_nnz]\n" + << "Usage (generate mode): " << argv[0] + << " gen [sketch_nnz]\n"; return 1; } std::string prec = argv[1]; diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index cb71a2abd..4ad717911 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -14,12 +14,13 @@ // [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask] // // method_mask (integer bitmask, optional, default = 0b001111 or 0b011111 if run_expl=1): -// bit 0 ( 1): CQRRT_linop -// bit 1 ( 2): CholQR -// bit 2 ( 4): sCholQR3 -// bit 3 ( 8): sCholQR3_basic -// bit 4 (16): CQRRT_expl (requires A materialization) -// bit 5 (32): CQRRT_linop_stb (GEQP3-stabilized preconditioner) +// bit 0 ( 1): CQRRT_linop +// bit 1 ( 2): CholQR +// bit 2 ( 4): sCholQR3 +// bit 3 ( 8): sCholQR3_basic +// bit 4 ( 16): CQRRT_expl (requires A materialization) +// bit 5 ( 32): CQRRT_linop_stb (GEQP3-stabilized preconditioner) +// bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner) #include "RandLAPACK.hh" #include "rl_blaspp.hh" @@ -734,6 +735,12 @@ static int run_benchmark_inner( if (method_mask & 32) run_cqrrt_linop("CQRRT_linop_stb", RandLAPACK::CQRRTLinopPrecond::GEQP3); + // ================================================================ + // CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner) + // ================================================================ + if (method_mask & 64) + run_cqrrt_linop("CQRRT_linop_stb_bqrrp", RandLAPACK::CQRRTLinopPrecond::BQRRP); + // Free pre-materialized A delete[] A_materialized; @@ -778,8 +785,9 @@ int run_benchmark(int argc, char* argv[]) { << " bit 1 ( 2): CholQR\n" << " bit 2 ( 4): sCholQR3\n" << " bit 3 ( 8): sCholQR3_basic\n" - << " bit 4 (16): CQRRT_expl (requires A materialization)\n" - << " bit 5 (32): CQRRT_linop_stb (GEQP3-stabilized preconditioner)\n"; + << " bit 4 ( 16): CQRRT_expl (requires A materialization)\n" + << " bit 5 ( 32): CQRRT_linop_stb (GEQP3-stabilized preconditioner)\n" + << " bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner)\n"; return 1; } @@ -820,7 +828,7 @@ int run_benchmark(int argc, char* argv[]) { std::cout << " method_mask: " << method_mask << " (bits: linop=" << (method_mask&1) << " CholQR=" << ((method_mask>>1)&1) << " sCholQR3=" << ((method_mask>>2)&1) << " sCholQR3_basic=" << ((method_mask>>3)&1) << " expl=" << ((method_mask>>4)&1) - << " linop_stb=" << ((method_mask>>5)&1) << ")\n"; + << " linop_stb=" << ((method_mask>>5)&1) << " linop_stb_bqrrp=" << ((method_mask>>6)&1) << ")\n"; std::cout << " num_runs: " << num_runs << "\n"; #ifdef _OPENMP std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; From 118545d0a815023277e1d472002bb37a7e5d4b9b Mon Sep 17 00:00:00 2001 From: mmelnich Date: Fri, 24 Apr 2026 11:41:10 -0400 Subject: [PATCH 13/47] CQRRT_stb analytical est --- RandLAPACK/drivers/rl_cqrrt_linops.hh | 42 ++- RandLAPACK/testing/rl_memory_tracker.hh | 21 +- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 277 ++++++++++++++---- .../CQRRT_linop_applications.cc | 11 +- 4 files changed, 257 insertions(+), 94 deletions(-) diff --git a/RandLAPACK/drivers/rl_cqrrt_linops.hh b/RandLAPACK/drivers/rl_cqrrt_linops.hh index a97275afb..69402ccce 100644 --- a/RandLAPACK/drivers/rl_cqrrt_linops.hh +++ b/RandLAPACK/drivers/rl_cqrrt_linops.hh @@ -60,10 +60,10 @@ class CQRRT_linops { int64_t Q_rows; int64_t Q_cols; - // 11 entries: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - // fwd = LinOp NoTrans: A * R_sk_inv (accumulated over blocks) - // adj = LinOp Trans: A^T * buf (accumulated over blocks) - // trmm = R_sk_inv^T * G (dense trmm, completes Gram) + // 11 entries: alloc, sketch, qr, tri_inv, fwd, adj, trsm_gram, chol, finalize, rest, total + // fwd = LinOp NoTrans: A * R_sk_inv (accumulated over blocks) + // adj = LinOp Trans: A^T * buf (accumulated over blocks) + // trsm_gram = (R^sk)^{-T} * G via TRSM on original R_sk (backward-stable left factor) std::vector times; // tuning SASOS @@ -187,7 +187,7 @@ class CQRRT_linops { steady_clock::time_point saso_t_start, saso_t_stop; steady_clock::time_point qr_t_start, qr_t_stop; steady_clock::time_point trtri_t_start, trtri_t_stop; - steady_clock::time_point trmm_gram_t_start, trmm_gram_t_stop; + steady_clock::time_point trsm_gram_t_start, trsm_gram_t_stop; steady_clock::time_point potrf_t_start, potrf_t_stop; steady_clock::time_point finalize_t_start, finalize_t_stop; steady_clock::time_point total_t_start, total_t_stop; @@ -198,7 +198,7 @@ class CQRRT_linops { long trtri_t_dur = 0; long fwd_t_dur = 0; long adj_t_dur = 0; - long trmm_gram_t_dur = 0; + long trsm_gram_t_dur = 0; long potrf_t_dur = 0; long finalize_t_dur = 0; long total_t_dur = 0; @@ -424,26 +424,18 @@ class CQRRT_linops { } if(this -> timing) { - trmm_gram_t_start = steady_clock::now(); + trsm_gram_t_start = steady_clock::now(); } - // Complete Gram: R := R_sk_inv^T * R (= R_sk_inv^T * A^T * A * R_sk_inv = A_pre^T * A_pre) - // TRSM_IDENTITY: R_sk_inv is upper triangular — use TRMM (O(n³/2)) - // GEQP3: R_sk_inv is dense — use GEMM (O(n³)) - if (this->precond_method == CQRRTLinopPrecond::TRSM_IDENTITY) { - blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, n, n, (T)1.0, R_sk_inv, n, R, ldr); - } else { - T* tmp = new T[n * n]; - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, n, n, n, - (T)1.0, R_sk_inv, n, R, ldr, (T)0.0, tmp, n); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i < n; ++i) - R[i + j*ldr] = tmp[i + j*n]; - delete[] tmp; - } + // Complete Gram: R := (R^sk)^{-T} * R (backward-stable TRSM on original sketch R) + // Applying the left factor via TRSM on A_hat (which holds R^sk in its upper n×n triangle) + // is more stable than TRMM/GEMM with the explicit R_sk_inv^T, regardless of which + // precond_method was used to form R_sk_inv. A_hat is still alive here (deleted below). + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, + n, n, (T)1.0, A_hat, d, R, ldr); if(this -> timing) { - trmm_gram_t_stop = steady_clock::now(); + trsm_gram_t_stop = steady_clock::now(); potrf_t_start = steady_clock::now(); } @@ -520,7 +512,7 @@ class CQRRT_linops { qr_t_dur = duration_cast(qr_t_stop - qr_t_start).count(); trtri_t_dur = duration_cast(trtri_t_stop - trtri_t_start).count(); // fwd_t_dur and adj_t_dur already set (in both full and blocked paths) - trmm_gram_t_dur = duration_cast(trmm_gram_t_stop - trmm_gram_t_start).count(); + trsm_gram_t_dur = duration_cast(trsm_gram_t_stop - trsm_gram_t_start).count(); potrf_t_dur = duration_cast(potrf_t_stop - potrf_t_start).count(); finalize_t_dur = duration_cast(finalize_t_stop - finalize_t_start).count(); @@ -533,12 +525,12 @@ class CQRRT_linops { } long t_rest = total_t_dur - (alloc_t_dur + saso_t_dur + qr_t_dur + trtri_t_dur + fwd_t_dur + - adj_t_dur + trmm_gram_t_dur + potrf_t_dur + finalize_t_dur); + adj_t_dur + trsm_gram_t_dur + potrf_t_dur + finalize_t_dur); // Fill the data vector (11 entries) // Index: 0=alloc, 1=sketch, 2=qr, 3=tri_inv, 4=fwd, 5=adj, 6=trmm, 7=chol, 8=finalize, 9=rest, 10=total this -> times = {alloc_t_dur, saso_t_dur, qr_t_dur, trtri_t_dur, fwd_t_dur, - adj_t_dur, trmm_gram_t_dur, potrf_t_dur, finalize_t_dur, + adj_t_dur, trsm_gram_t_dur, potrf_t_dur, finalize_t_dur, t_rest, total_t_dur}; } diff --git a/RandLAPACK/testing/rl_memory_tracker.hh b/RandLAPACK/testing/rl_memory_tracker.hh index 30a80c531..7ddfd3c69 100644 --- a/RandLAPACK/testing/rl_memory_tracker.hh +++ b/RandLAPACK/testing/rl_memory_tracker.hh @@ -83,7 +83,8 @@ private: // All return memory in KB. // --------------------------------------------------------------------------- -// CQRRT_linops: A_hat(d*n) + tau(n) + R_sk_inv(n*n) + A_pre(m*b_eff) +// CQRRT_linops (TRSM_IDENTITY / GEQP3): A_hat(d*n) + tau(n) + R_sk_inv(n*n) + A_pre(m*b_eff) +// Peak is during the blocked Gram loop where A_hat, R_sk_inv, and A_pre are all live. template static inline long cqrrt_linops_analytical_kb(int64_t m, int64_t n, double d_factor, int64_t block_size) { int64_t d = static_cast(std::ceil(d_factor * n)); @@ -92,6 +93,16 @@ static inline long cqrrt_linops_analytical_kb(int64_t m, int64_t n, double d_fac return bytes / 1024; } +// CQRRT_linops (BQRRP): A_hat(d*n) + R_sk_copy(n*n) + R_buf(n*n) + W(n*n) + R_sk_inv(n*n) +// Peak is in the BQRRP preconditioner block (lines 288-342 of rl_cqrrt_linops.hh) where all +// five buffers are simultaneously live. A_pre (m*b) is not yet allocated at this point. +template +static inline long cqrrt_linops_bqrrp_analytical_kb(int64_t n, double d_factor) { + int64_t d = static_cast(std::ceil(d_factor * n)); + long bytes = static_cast(sizeof(T)) * ((long)d * n + 4L * n * n); + return bytes / 1024; +} + // CholQR_linops: I_mat(n*n) + A_temp(m*b_eff) template static inline long cholqr_linops_analytical_kb(int64_t m, int64_t n, int64_t block_size = 0) { @@ -100,12 +111,14 @@ static inline long cholqr_linops_analytical_kb(int64_t m, int64_t n, int64_t blo return bytes / 1024; } -// sCholQR3_linops (fully-blocked): G(n*n) + R_temp(n*n) + M(n*n) + A_temp(m*b) + Z_buf(n*b) -// No m x n buffer during QR iterations; all Gram matrices computed through blocked linop calls. +// sCholQR3_linops (fully-blocked): +// local: G(n*n) + R_temp(n*n) + M(n*n) + A_temp(m*b) + Z_buf(n*b) +// member: G1_factor(n*n) + G2_factor(n*n) (both live during iteration 3) +// Peak is during the iteration-3 Gram loop where all 5 n*n buffers and the block buffers are live. template static inline long scholqr3_linops_analytical_kb(int64_t m, int64_t n, int64_t block_size = 0) { int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; - long bytes = static_cast(sizeof(T)) * (3L * n * n + (long)(m + n) * b_eff); + long bytes = static_cast(sizeof(T)) * (5L * n * n + (long)(m + n) * b_eff); return bytes / 1024; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index 2d886ea7a..ea43db81b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -8,11 +8,17 @@ // [3] expl_inv_trtri: TRTRI(R_sk) → R_inv; DGEMM(A, R_inv) (LAPACK trtri) // [4] expl_inv_geqp3: GEQP3(R_sk) = Q*R_buf*P^T; // R_inv = P * TRSM(R_buf, Q^T); DGEMM(A, R_inv) +// [5] expl_inv_svd: GESDD(R_sk) = U*S*Vt; +// R_inv = V * diag(1/S) * U^T; DGEMM(A, R_inv) +// [6] expl_inv_getri: GETRF(R_sk) then GETRI; DGEMM(A, R_inv) // // Path [1] never forms R_sk^{-1} explicitly (backward stable). -// Paths [2]-[4] all form R_sk^{-1} explicitly via different methods. +// Paths [2]-[6] all form R_sk^{-1} explicitly via different methods. // Path [4] uses a rank-revealing QR to invert R_sk; the Q factor makes // the inversion well-conditioned even when R_sk itself is ill-conditioned. +// Path [5] uses the SVD (gold standard for stability). +// Path [6] uses general LU with partial pivoting; for upper-triangular R_sk +// this is expected to behave similarly to paths [2]-[3]. // // All four paths use the same sketch (same RNG state). // @@ -26,13 +32,18 @@ // rd_12 = ||A_pre[1] - A_pre[2]|| / ||A_pre[1]|| // rd_13 = ||A_pre[1] - A_pre[3]|| / ||A_pre[1]|| // rd_14 = ||A_pre[1] - A_pre[4]|| / ||A_pre[1]|| +// rd_15 = ||A_pre[1] - A_pre[5]|| / ||A_pre[1]|| +// rd_16 = ||A_pre[1] - A_pre[6]|| / ||A_pre[1]|| // // Step-by-step pipeline divergence between paths [1] and [2] (CQRRT_expl vs CQRRT_linop): -// Reproduces the "step-by-step divergence" plot from CQRRT_orth_gap diag mode. -// Since all paths share the same sketch, M^sk and R^sk diffs are 0 by construction -// (the shared-sketch design cleanly eliminates sketch method as a variable). -// rd_G_12 = ||G1 - G2|| / ||G1|| (Gram matrix: G = A_pre^T A_pre) -// rd_Rchol_12 = ||Rchol1 - Rchol2|| / ||Rchol1|| (Cholesky factor) +// Each path uses the same RNG seed but a different sketch code path — mirrors actual impls. +// Path [1] (CQRRT_expl): sketch via sketch_general(S, A_dense); TRSM in-place; SYRK +// Path [2] (CQRRT_linop): sketch via A_linop(Side::Right, S) [SpGEMM]; +// TRSM_IDENTITY → R_inv; A_linop fwd/adj + TRMM +// rd_Msk_12 = ||Ahat1 - Ahat2|| / ||Ahat1|| (raw sketch S*A, different code paths) +// rd_Rsk_12 = ||R_sk1 - R_sk2|| / ||R_sk1|| (QR of above) +// rd_G_12 = ||G1 - G2|| / ||G1|| (Gram matrix) +// rd_Rchol_12 = ||Rchol1 - Rchol2|| / ||Rchol1|| (Cholesky factor) // rd_Rfinal_12= ||Rfinal1 - Rfinal2|| / ||Rfinal1|| (final R = R_chol * R_sk) // // Sketch diagnostic: @@ -78,13 +89,15 @@ using blas::Diag; // Path constants // ============================================================================ -static constexpr int N_PATHS = 4; +static constexpr int N_PATHS = 6; static constexpr const char* PATH_NAMES[N_PATHS] = { "expl_trsm", "expl_inv_trsm", "expl_inv_trtri", "expl_inv_geqp3", + "expl_inv_svd", + "expl_inv_getri", }; static constexpr const char* PATH_DESCS[N_PATHS] = { @@ -92,6 +105,8 @@ static constexpr const char* PATH_DESCS[N_PATHS] = { "TRSM(I, R_sk)->R_inv; DGEMM(A, R_inv)", "TRTRI(R_sk)->R_inv; DGEMM(A, R_inv)", "GEQP3(R_sk)=Q*R_buf*P^T; R_inv=P*TRSM(R_buf,Q^T); DGEMM(A, R_inv)", + "GESDD(R_sk)=U*S*Vt; R_inv=V*diag(1/S)*U^T; DGEMM(A, R_inv)", + "GETRF+GETRI(R_sk)->R_inv; DGEMM(A, R_inv)", }; // ============================================================================ @@ -131,9 +146,11 @@ static T gram_condition_number(const T* A_pre, int64_t m, int64_t n) { return condition_number(G.data(), n, n); } -// Full CQRRT pipeline (matching V2 benchmark orth_error computation): -// G = A_pre^T A_pre (SYRK) -// R_chol = chol(G) (POTRF, overwrites G with upper triangular factor) +// Full CQRRT pipeline (matching CQRRT_linops Gram computation): +// G = A_orig^T * A_pre (GEMM — full n×n, not exploiting symmetry) +// G = (R_sketch)^{-T} * G (TRSM Left — backward-stable left factor on original R^sk) +// Zero lower triangle of G (POTRF/TRMM only use upper triangle; lower has TRSM output) +// R_chol = chol(G) (POTRF, upper triangle only) // R_final = R_chol * R_sketch (TRMM) // Q = A_orig * R_final^{-1} (TRSM on copy of A_orig) // return orth_error(Q) @@ -142,8 +159,15 @@ template static T cholqr_orth_error(const std::vector& A_pre, const T* A_orig, int64_t m, int64_t n, const T* R_sketch) { std::vector G(n * n, 0.0); - blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, - n, m, (T)1.0, A_pre.data(), m, (T)0.0, G.data(), n); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, A_orig, m, A_pre.data(), m, (T)0.0, G.data(), n); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, + n, n, (T)1.0, R_sketch, n, G.data(), n); + // Zero strictly lower triangle: TRSM fills the full n×n matrix; the subsequent + // TRMM(Right,Upper) reads lower-triangle entries of G when computing upper-triangle + // output entries and would produce corrupted R_chol if left non-zero. + if (n > 1) + lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G.data()[1], n); if (lapack::potrf(Uplo::Upper, n, G.data(), n)) return std::numeric_limits::infinity(); blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, @@ -155,29 +179,39 @@ static T cholqr_orth_error(const std::vector& A_pre, const T* A_orig, } // ============================================================================ -// One trial: three paths, same sketch +// One trial: 4-path orth comparison (shared sketch) + +// independent path [1] vs [2] step-by-step divergence // ============================================================================ template struct TrialResult { - // Per-path metrics + // Per-path metrics (shared sketch, 4 paths) double cond_Apre[N_PATHS]; double cond_G[N_PATHS]; double orth_Q[N_PATHS]; - // Cross-path relative differences of A_pre (reference = path [1]) + // Cross-path relative differences of A_pre (reference = path [1], shared sketch) double rd_Apre_12; // TRSM in-place vs TRSM-on-identity double rd_Apre_13; // TRSM in-place vs trtri double rd_Apre_14; // TRSM in-place vs geqp3 - // Step-by-step pipeline divergence: paths [1] vs [2] (CQRRT_expl vs CQRRT_linop) - double rd_G_12; // Gram matrix: G = A_pre^T A_pre - double rd_Rchol_12; // Cholesky factor: R_chol = chol(G) - double rd_Rfinal_12; // Final R: R_final = R_chol * R_sk + double rd_Apre_15; // TRSM in-place vs svd + double rd_Apre_16; // TRSM in-place vs getri + // Step-by-step pipeline divergence: paths [1] vs [2], faithful to actual implementations + // Path [1] (CQRRT_expl): sketch via sketch_general(S, A_dense); TRSM in-place; SYRK + // Path [2] (CQRRT_linop): sketch via A_linop(Side::Right, S) [SpGEMM]; + // TRSM_IDENTITY → R_inv; A_linop fwd/adj + TRMM + double rd_Msk_12; // M^sk: raw sketch Ahat = S*A (different code paths) + double rd_Rsk_12; // R^sk: QR factor of the above + double rd_Apre_12_step; // MR^pre: TRSM in-place vs A_linop(fwd, R_inv) + double rd_G_12; // Gram: SYRK(A_pre) vs A_linop(adj, A_pre)+TRMM + double rd_Rchol_12; // R^chol: Cholesky factor + double rd_Rfinal_12; // R: R_final = R_chol * R_sk // Sketch diagnostic double cond_Rsk; }; -template +template static TrialResult run_trial( + LinOpT& A_linop, const T* A_dense, int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, @@ -186,8 +220,12 @@ static TrialResult run_trial( TrialResult res{}; int64_t d = (int64_t)std::ceil(d_factor * n); + // Save RNG state before any sketching; Part B (step-by-step) uses this + // to compute independent sketches for paths [1] and [2]. + auto initial_state = state; + // ---------------------------------------------------------------- - // Sketch A → Ahat (d × n), then QR → R_sk + // Part A: Shared sketch → R_sk, 4-path orth comparison // ---------------------------------------------------------------- RandBLAS::SparseDist Ds(d, m, sketch_nnz, RandBLAS::Axis::Short); RandBLAS::SparseSkOp S(Ds, state); @@ -260,6 +298,33 @@ static TrialResult run_trial( R_inv_geqp3[(jpiv[k]-1) + j*n] = W[k + j*n]; } + // Method D: SVD of R_sk (path [5]) + // R_sk = U * diag(s) * Vt + // R_sk^{-1} = V * diag(1/s) * U^T = Vt^T * diag(1/s) * U^T + std::vector R_inv_svd(n * n, 0.0); + { + std::vector R_copy(R_sk.begin(), R_sk.end()); + std::vector U(n * n, 0.0), Vt(n * n, 0.0), s(n); + lapack::gesdd(lapack::Job::AllVec, n, n, R_copy.data(), n, + s.data(), U.data(), n, Vt.data(), n); + // Scale row k of Vt by 1/s[k]: Vt[k + j*n] is row k, col j (col-major) + for (int64_t k = 0; k < n; ++k) + for (int64_t j = 0; j < n; ++j) + Vt[k + j*n] /= s[k]; + // R_inv = scaled_Vt^T * U^T + blas::gemm(Layout::ColMajor, Op::Trans, Op::Trans, + n, n, n, (T)1.0, Vt.data(), n, U.data(), n, + (T)0.0, R_inv_svd.data(), n); + } + + // Method E: GETRF + GETRI (general LU with partial pivoting) (path [6]) + std::vector R_inv_getri(R_sk.begin(), R_sk.end()); + { + std::vector ipiv(n); + lapack::getrf(n, n, R_inv_getri.data(), n, ipiv.data()); + lapack::getri(n, R_inv_getri.data(), n, ipiv.data()); + } + // ---------------------------------------------------------------- // Compute all N_PATHS preconditioned matrices: // Apre[0]: TRSM in-place (path [1], CQRRT_expl) @@ -274,7 +339,10 @@ static TrialResult run_trial( blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, R_sk.data(), n, Apre[0].data(), m); - const T* R_invs[N_PATHS - 1] = {R_inv_trsm.data(), R_inv_trtri.data(), R_inv_geqp3.data()}; + const T* R_invs[N_PATHS - 1] = { + R_inv_trsm.data(), R_inv_trtri.data(), R_inv_geqp3.data(), + R_inv_svd.data(), R_inv_getri.data() + }; for (int p = 1; p < N_PATHS; ++p) blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, A_dense, m, R_invs[p-1], n, (T)0.0, Apre[p].data(), m); @@ -285,39 +353,121 @@ static TrialResult run_trial( res.rd_Apre_12 = (double)rel_diff(Apre[0].data(), Apre[1].data(), m*n); res.rd_Apre_13 = (double)rel_diff(Apre[0].data(), Apre[2].data(), m*n); res.rd_Apre_14 = (double)rel_diff(Apre[0].data(), Apre[3].data(), m*n); + res.rd_Apre_15 = (double)rel_diff(Apre[0].data(), Apre[4].data(), m*n); + res.rd_Apre_16 = (double)rel_diff(Apre[0].data(), Apre[5].data(), m*n); // ---------------------------------------------------------------- - // Step-by-step pipeline intermediates: paths [1] vs [2] (Apre[0] vs Apre[1]) - // G = A_pre^T A_pre (upper triangle via SYRK) - // R_chol = chol(G) (POTRF in-place on upper triangle) - // R_final = R_chol * R_sk (TRMM) + // Part B: Independent step-by-step divergence, paths [1] vs [2] + // + // Both paths compute their own sketch and R_sk from initial_state + // (same seed → same result, but as separate objects). + // + // Path [1] (CQRRT_expl): TRSM in-place on A; Gram via SYRK. + // Path [2] (CQRRT_linop, block_size=0): + // R_inv = TRSM_IDENTITY(R_sk); + // A_pre = GEMM(A, R_inv) [fwd linop call] + // G = GEMM(A^T, A_pre) [adj linop call] + // G = TRMM(R_inv^T, G) [complete Gram: R_inv^T * A^T * A * R_inv] // ---------------------------------------------------------------- - auto make_G = [&](const std::vector& A) { - std::vector G(n*n, 0.0); + + // Run Part B as a lambda so early returns on Cholesky failure are clean. + [&]() { + // ---- Step 1: Sketch ---- + // Path [1] (CQRRT_expl): sketch_general(S, A_dense) — left SPMM on dense copy + RandBLAS::SparseDist Ds_1(d, m, sketch_nnz, RandBLAS::Axis::Short); + RandBLAS::SparseSkOp S_1(Ds_1, initial_state); + std::vector Ahat_1(d * n, 0.0); + RandBLAS::sketch_general(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S_1, A_dense, m, + (T)0.0, Ahat_1.data(), d); + + // Path [2] (CQRRT_linop): A_linop(Side::Right, S) — SpGEMM on sparse CSR matrix + RandBLAS::SparseDist Ds_2(d, m, sketch_nnz, RandBLAS::Axis::Short); + RandBLAS::SparseSkOp S_2(Ds_2, initial_state); // same seed → same S + std::vector Ahat_2(d * n, 0.0); + A_linop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S_2, (T)0.0, Ahat_2.data(), d); + + // M^sk diff: before geqrf overwrites the sketch buffers + res.rd_Msk_12 = (double)rel_diff(Ahat_1.data(), Ahat_2.data(), d*n); + + // ---- Step 2: QR → R_sk ---- + std::vector R_sk_1(n * n, 0.0); + { + std::vector tau_1(n); + lapack::geqrf(d, n, Ahat_1.data(), d, tau_1.data()); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk_1[i + j*n] = Ahat_1[i + j*d]; + } + std::vector R_sk_2(n * n, 0.0); + { + std::vector tau_2(n); + lapack::geqrf(d, n, Ahat_2.data(), d, tau_2.data()); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_sk_2[i + j*n] = Ahat_2[i + j*d]; + } + res.rd_Rsk_12 = (double)rel_diff(R_sk_1.data(), R_sk_2.data(), n*n); + + // ---- Path [1]: CQRRT_expl — TRSM in-place, SYRK ---- + std::vector Apre_1(A_dense, A_dense + m*n); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, R_sk_1.data(), n, Apre_1.data(), m); + + std::vector G_1(n*n, 0.0); blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, - n, m, (T)1.0, A.data(), m, (T)0.0, G.data(), n); - return G; - }; - auto make_Rchol = [&](std::vector G) { - lapack::potrf(Uplo::Upper, n, G.data(), n); - return G; - }; - auto make_Rfinal = [&](std::vector Rchol) { + n, m, (T)1.0, Apre_1.data(), m, (T)0.0, G_1.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j+1; i < n; ++i) + G_1[i + j*n] = G_1[j + i*n]; + + std::vector Rchol_1(G_1); + if (lapack::potrf(Uplo::Upper, n, Rchol_1.data(), n)) return; + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j+1; i < n; ++i) + Rchol_1[i + j*n] = (T)0.0; + + std::vector Rfinal_1(Rchol_1); blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, R_sk.data(), n, Rchol.data(), n); - return Rchol; - }; + Diag::NonUnit, n, n, (T)1.0, R_sk_1.data(), n, Rfinal_1.data(), n); - auto G1 = make_G(Apre[0]); - auto G2 = make_G(Apre[1]); - auto Rchol1 = make_Rchol(G1); - auto Rchol2 = make_Rchol(G2); - auto Rfinal1 = make_Rfinal(Rchol1); - auto Rfinal2 = make_Rfinal(Rchol2); + // ---- Path [2]: CQRRT_linop — TRSM_IDENTITY, linop fwd/adj, TRMM ---- + // R_inv via TRSM_IDENTITY: solve X * R_sk_2 = I (upper triangular result) + auto R_inv_2 = make_eye(n); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_sk_2.data(), n, R_inv_2.data(), n); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j+1; i < n; ++i) + R_inv_2[i + j*n] = (T)0.0; + + // fwd: Apre_2 = A * R_inv_2 via A_linop(Side::Left, NoTrans) + std::vector Apre_2(m * n, 0.0); + A_linop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, n, (T)1.0, R_inv_2.data(), n, (T)0.0, Apre_2.data(), m); + res.rd_Apre_12_step = (double)rel_diff(Apre_1.data(), Apre_2.data(), m*n); + + // adj: G_2 = A^T * Apre_2 via A_linop(Side::Left, Trans) + std::vector G_2(n * n, 0.0); + A_linop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, n, m, (T)1.0, Apre_2.data(), m, (T)0.0, G_2.data(), n); + // Complete Gram: G_2 = (R_sk_2)^{-T} * G_2 (backward-stable TRSM on original R_sk) + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, + n, n, (T)1.0, R_sk_2.data(), n, G_2.data(), n); + res.rd_G_12 = (double)rel_diff(G_1.data(), G_2.data(), n*n); + + std::vector Rchol_2(G_2); + if (lapack::potrf(Uplo::Upper, n, Rchol_2.data(), n)) return; + for (int64_t j = 0; j < n; ++j) + for (int64_t i = j+1; i < n; ++i) + Rchol_2[i + j*n] = (T)0.0; + res.rd_Rchol_12 = (double)rel_diff(Rchol_1.data(), Rchol_2.data(), n*n); - res.rd_G_12 = (double)rel_diff(G1.data(), G2.data(), n*n); - res.rd_Rchol_12 = (double)rel_diff(Rchol1.data(), Rchol2.data(), n*n); - res.rd_Rfinal_12 = (double)rel_diff(Rfinal1.data(), Rfinal2.data(), n*n); + std::vector Rfinal_2(Rchol_2); + blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, R_sk_2.data(), n, Rfinal_2.data(), n); + res.rd_Rfinal_12 = (double)rel_diff(Rfinal_1.data(), Rfinal_2.data(), n*n); + }(); // ---------------------------------------------------------------- // Per-path metrics @@ -335,8 +485,9 @@ static TrialResult run_trial( // Shared: write CSV header and run trials given a dense matrix // ============================================================================ -template +template static void write_csv_and_run( + LinOpT& A_linop, const std::vector& A_dense, int64_t m, int64_t n, T d_factor, int64_t sketch_nnz, int64_t num_runs, @@ -360,11 +511,11 @@ static void write_csv_and_run( csv << " kappa_target=" << std::scientific << std::setprecision(6) << kappa_target; csv << "\n"; csv << "run," - << "orth_Q1,orth_Q2,orth_Q3,orth_Q4," - << "cond_Apre1,cond_Apre2,cond_Apre3,cond_Apre4," - << "cond_G1,cond_G2,cond_G3,cond_G4," - << "rd_Apre_12,rd_Apre_13,rd_Apre_14," - << "rd_G_12,rd_Rchol_12,rd_Rfinal_12," + << "orth_Q1,orth_Q2,orth_Q3,orth_Q4,orth_Q5,orth_Q6," + << "cond_Apre1,cond_Apre2,cond_Apre3,cond_Apre4,cond_Apre5,cond_Apre6," + << "cond_G1,cond_G2,cond_G3,cond_G4,cond_G5,cond_G6," + << "rd_Apre_12,rd_Apre_13,rd_Apre_14,rd_Apre_15,rd_Apre_16," + << "rd_Msk_12,rd_Rsk_12,rd_Apre_12_step,rd_G_12,rd_Rchol_12,rd_Rfinal_12," << "cond_Rsk\n"; RandBLAS::RNGState base_state(42); @@ -372,7 +523,7 @@ static void write_csv_and_run( auto state = base_state; if (r > 0) state.key.incr(r); - auto res = run_trial(A_dense.data(), m, n, d_factor, sketch_nnz, state); + auto res = run_trial(A_linop, A_dense.data(), m, n, d_factor, sketch_nnz, state); printf(" run %ld orth_error(Q = A * R_final^{-1}):\n", r); for (int p = 0; p < N_PATHS; ++p) @@ -390,11 +541,13 @@ static void write_csv_and_run( printf(" rd_12 (trsm-on-I): %12.3e\n", res.rd_Apre_12); printf(" rd_13 (trtri): %12.3e\n", res.rd_Apre_13); printf(" rd_14 (geqp3): %12.3e\n", res.rd_Apre_14); + printf(" rd_15 (svd): %12.3e\n", res.rd_Apre_15); + printf(" rd_16 (getri): %12.3e\n", res.rd_Apre_16); - printf(" run %ld step-by-step divergence [1] vs [2] (expl vs linop):\n", r); - printf(" M^sk: %12.3e (0 by construction — shared sketch)\n", 0.0); - printf(" R^sk: %12.3e (0 by construction — shared sketch)\n", 0.0); - printf(" MR^pre: %12.3e\n", res.rd_Apre_12); + printf(" run %ld step-by-step divergence [1] vs [2] (expl: sketch_general; linop: SpGEMM):\n", r); + printf(" M^sk: %12.3e\n", res.rd_Msk_12); + printf(" R^sk: %12.3e\n", res.rd_Rsk_12); + printf(" MR^pre: %12.3e\n", res.rd_Apre_12_step); printf(" G: %12.3e\n", res.rd_G_12); printf(" R^chol: %12.3e\n", res.rd_Rchol_12); printf(" R: %12.3e\n", res.rd_Rfinal_12); @@ -406,6 +559,8 @@ static void write_csv_and_run( for (int p = 0; p < N_PATHS; ++p) csv << res.cond_Apre[p] << ","; for (int p = 0; p < N_PATHS; ++p) csv << res.cond_G[p] << ","; csv << res.rd_Apre_12 << "," << res.rd_Apre_13 << "," << res.rd_Apre_14 << "," + << res.rd_Apre_15 << "," << res.rd_Apre_16 << "," + << res.rd_Msk_12 << "," << res.rd_Rsk_12 << "," << res.rd_Apre_12_step << "," << res.rd_G_12 << "," << res.rd_Rchol_12 << "," << res.rd_Rfinal_12 << "," << res.cond_Rsk << "\n"; } @@ -477,7 +632,7 @@ int run_benchmark(int argc, char* argv[]) { std::string label = "gen_" + std::to_string(m) + "x" + std::to_string(n) + "_kappa" + std::to_string((int)std::round(std::log10((double)kappa))); - write_csv_and_run(A_dense, m, n, d_factor, sketch_nnz, num_runs, + write_csv_and_run(A_linop, A_dense, m, n, d_factor, sketch_nnz, num_runs, cond_A, (double)kappa, label, output_dir); } else { // file mode: prec output_dir mtx_path d_factor runs [sketch_nnz] @@ -516,7 +671,7 @@ int run_benchmark(int argc, char* argv[]) { #endif std::cout << "\n"; - write_csv_and_run(A_dense, m, n, d_factor, sketch_nnz, num_runs, + write_csv_and_run(A_linop, A_dense, m, n, d_factor, sketch_nnz, num_runs, cond_A, -1.0, mtx_path, output_dir); } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 4ad717911..8c621d198 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -623,8 +623,8 @@ static int run_benchmark_inner( // ================================================================ // CQRRT_linop variants (standard TRSM_IDENTITY and GEQP3-stabilized) // ================================================================ - auto run_cqrrt_linop = [&](const std::string& name, RandLAPACK::CQRRTLinopPrecond precond) { - run_algo(name, [&](gsvd_result& res, std::vector& R, int64_t r) { + auto run_cqrrt_linop = [&](const std::string& name, RandLAPACK::CQRRTLinopPrecond precond, long analytical_kb_override = -1L) { + run_algo(name, [&, precond, analytical_kb_override](gsvd_result& res, std::vector& R, int64_t r) { auto state = run_states[r]; RandLAPACK::CQRRT_linops algo(true, tol, false); algo.nnz = sketch_nnz; algo.block_size = block_size; @@ -635,7 +635,9 @@ static int run_benchmark_inner( res.qr_time_us = algo.times[10]; // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + res.analytical_kb = (analytical_kb_override >= 0) + ? analytical_kb_override + : RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); }); }; if (method_mask & 1) @@ -739,7 +741,8 @@ static int run_benchmark_inner( // CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner) // ================================================================ if (method_mask & 64) - run_cqrrt_linop("CQRRT_linop_stb_bqrrp", RandLAPACK::CQRRTLinopPrecond::BQRRP); + run_cqrrt_linop("CQRRT_linop_stb_bqrrp", RandLAPACK::CQRRTLinopPrecond::BQRRP, + RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(n, d_factor)); // Free pre-materialized A delete[] A_materialized; From a4f1c3999e47379fa1f3c9a29c2e64a3bd20b75c Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 7 May 2026 16:43:16 -0400 Subject: [PATCH 14/47] New benchmark for Oleg --- RandLAPACK.hh | 1 + RandLAPACK/drivers/rl_iter_refine_lsq.hh | 340 ++++++++++ RandLAPACK/linops/rl_kronecker_linop.hh | 334 ++++++++++ RandLAPACK/linops/rl_linops.hh | 2 + RandLAPACK/linops/rl_regularized_linop.hh | 269 ++++++++ benchmark/CMakeLists.txt | 2 + .../CQRRT_linop_applications.cc | 160 +---- .../bench_CQRRT_linops/CQRRT_linop_nmr.cc | 584 ++++++++++++++++++ test/CMakeLists.txt | 3 + test/drivers/test_iter_refine_lsq.cc | 223 +++++++ test/linops/test_kronecker_linop.cc | 185 ++++++ test/linops/test_regularized_linop.cc | 221 +++++++ 12 files changed, 2197 insertions(+), 127 deletions(-) create mode 100644 RandLAPACK/drivers/rl_iter_refine_lsq.hh create mode 100644 RandLAPACK/linops/rl_kronecker_linop.hh create mode 100644 RandLAPACK/linops/rl_regularized_linop.hh create mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc create mode 100644 test/drivers/test_iter_refine_lsq.cc create mode 100644 test/linops/test_kronecker_linop.cc create mode 100644 test/linops/test_regularized_linop.cc diff --git a/RandLAPACK.hh b/RandLAPACK.hh index 7e88f5cf2..b840d73ee 100644 --- a/RandLAPACK.hh +++ b/RandLAPACK.hh @@ -34,6 +34,7 @@ #include "RandLAPACK/drivers/rl_cqrrt.hh" #include "RandLAPACK/drivers/rl_cholqr_linops.hh" #include "RandLAPACK/drivers/rl_cqrrt_linops.hh" +#include "RandLAPACK/drivers/rl_iter_refine_lsq.hh" #include "RandLAPACK/drivers/rl_scholqr3_linops.hh" #include "RandLAPACK/drivers/rl_cqrrpt.hh" #include "RandLAPACK/drivers/rl_bqrrp.hh" diff --git a/RandLAPACK/drivers/rl_iter_refine_lsq.hh b/RandLAPACK/drivers/rl_iter_refine_lsq.hh new file mode 100644 index 000000000..a1c3b67c9 --- /dev/null +++ b/RandLAPACK/drivers/rl_iter_refine_lsq.hh @@ -0,0 +1,340 @@ +#pragma once + +// Public API: IterRefineLSQ — Q-less, sketch-and-precondition iterative-refinement +// least-squares solver. +// +// Solves min_x ||b - J x||_2 for a tall LinearOperator J using a precomputed +// triangular preconditioner R (e.g., the R-factor from CQRRT_linops on J or +// on a sketch SJ). R is treated as a right preconditioner on the normal +// equations, and two iterative-refinement steps are performed; under standard +// hypotheses two steps suffice for backward stability. The inner solver is +// CG on the symmetric-positive-definite preconditioned normal-equation matrix +// +// M = R^{-T} J^T J R^{-1}. +// +// Reference: E. N. Epperly, M. Meier, and Y. Nakatsukasa, +// "Fast randomized least-squares solvers can be just as accurate and stable +// as classical direct solvers," arXiv:2406.03468v3 (2025), Algorithm 1 +// + Theorem 6.1 (master theorem on backward stability of two-step IR). + +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "../linops/rl_concepts.hh" + +#include +#include +#include +#include +#include + + +namespace RandLAPACK { + + +/*********************************************************/ +/* */ +/* IterRefineLSQ */ +/* */ +/*********************************************************/ + +/// @brief Iterative-refinement least-squares solver with right preconditioner R. +/// +/// Solves min_x ||b - J x||_2 (or, with `lambda > 0`, the Tikhonov-regularized +/// problem min_x ||b - J x||_2^2 + lambda^2 ||x||_2^2) by performing +/// `n_refine_steps` outer steps of +/// +/// r_i ← b - J x_i +/// g_i ← J^T r_i − lambda^2 x_i (regularized gradient; lambda=0 → unregularized) +/// c_i ← R^{-T} g_i +/// z_i ← inner_solve(M_reg, c_i) with M_reg = R^{-T} (J^T J + lambda^2 I) R^{-1} +/// x_{i+1} ← x_i + R^{-1} z_i +/// +/// where R is upper triangular (n × n, ColMajor) and is held constant. The +/// inner solver is preconditioner-free conjugate gradients on the SPD matrix +/// M_reg; each inner matvec costs two TRSMs, one J apply (forward), one J^T apply +/// (adjoint), plus a single axpy when `lambda > 0`. The default +/// `n_refine_steps = 2` and inner CG stopping rule (relative residual +/// `< inner_tol`) suffice for backward stability under the conditions of +/// Epperly et al. (2025) Theorem 6.1. +/// +/// Tikhonov is useful for ill-posed inverse problems (e.g., NMR relaxometry) +/// where J has tiny singular values and the unregularized normal-equation +/// matrix loses positive definiteness in finite precision. +template +struct IterRefineLSQ { + // ------------- Configuration ------------- + /// Inner-CG residual tolerance: stop when ||M z - c|| <= inner_tol * ||c||. + T inner_tol; + /// Hard cap on inner CG iterations per outer refinement step. + int max_inner_iters; + /// Outer refinement steps (Algorithm 1 of Epperly et al. uses 2). + int n_refine_steps; + /// Tikhonov regularization parameter. Solves min ||Jx-b||² + lambda²||x||². + /// Set to 0 (default) for unregularized LS. + T lambda; + /// Enable per-step / per-substep timing breakdown. + bool timing; + /// Print convergence info to stdout. + bool verbose; + + // ------------- Outputs (filled by call) ------------- + /// Number of outer refinement steps actually executed. + int outer_iters_done; + /// CG iteration counts for each outer step. + std::vector inner_iters_per_step; + /// Final relative residual ||b - J x|| / ||b|| (or ||b - J x|| if ||b|| == 0). + T final_residual_norm; + /// Per-substep wall-clock breakdown (microseconds), populated when timing == true. + /// Entries: [0]=outer_total, [1]=inner_cg_total, [2]=trsm_total, + /// [3]=fwd_total, [4]=adj_total, [5]=other. + std::vector times; + + IterRefineLSQ(T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85), + int max_inner = 200, + int n_steps = 2, + bool timing_on = false, + bool verbose_on = false, + T lambda_in = (T)0) + : inner_tol(tol), + max_inner_iters(max_inner), + n_refine_steps(n_steps), + lambda(lambda_in), + timing(timing_on), + verbose(verbose_on), + outer_iters_done(0), + final_residual_norm((T)0) + {} + + /// @brief Solve min ||b - J x||_2 with right preconditioner R. + /// + /// @tparam J_LO A LinearOperator type (must satisfy linops::LinearOperator). + /// + /// @param J Forward operator (m × n, m >= n; n_rows == m, n_cols == n). + /// @param R n × n upper triangular ColMajor; leading dim ldr >= n. + /// @param ldr Leading dimension of R. + /// @param b Right-hand side, length m. + /// @param m Number of rows of J / length of b. + /// @param x Solution buffer, length n. On entry: initial guess (use zeros + /// for cold start). On exit: the refined LS solution. + /// @param n Number of columns of J / length of x. + /// + /// @returns 0 on success; nonzero on inner-CG breakdown. + template + int call(J_LO& J, const T* R, int64_t ldr, + const T* b, int64_t m, T* x, int64_t n) + { + using clock = std::chrono::steady_clock; + auto outer_start = clock::now(); + + long t_inner_total = 0, t_trsm_total = 0, t_fwd_total = 0, t_adj_total = 0; + + inner_iters_per_step.clear(); + outer_iters_done = 0; + + std::vector r(m); // residual + std::vector g(n); // J^T r + std::vector c(n); // R^{-T} g + std::vector z(n); // inner-solve output + std::vector dx(n); // R^{-1} z + + // CG workspaces (allocated once, reused across outer steps) + std::vector cg_r(n), cg_p(n), cg_Mp(n); + // Inside-M-apply workspaces + std::vector tmp_n(n), tmp_m(m); + + T b_norm = blas::nrm2(m, b, 1); + if (b_norm == (T)0) b_norm = (T)1; // avoid div-by-zero in residual reporting + + for (int step = 0; step < n_refine_steps; ++step) { + // r = b - J*x + // tmp_m = J*x + auto t0 = clock::now(); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); + t_fwd_total += std::chrono::duration_cast(clock::now() - t0).count(); + + // r = b - tmp_m + for (int64_t i = 0; i < m; ++i) r[i] = b[i] - tmp_m[i]; + + T r_norm = blas::nrm2(m, r.data(), 1); + if (verbose) { + std::printf("[IR-LSQ] step %d: ||r||/||b|| = %.4e\n", step, (double)(r_norm / b_norm)); + } + + // g = J^T r (regularized gradient: g = J^T r - lambda^2 x) + t0 = clock::now(); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, 1, m, (T)1.0, r.data(), m, (T)0.0, g.data(), n); + t_adj_total += std::chrono::duration_cast(clock::now() - t0).count(); + if (lambda != (T)0) { + T lambda_sq = lambda * lambda; + blas::axpy(n, -lambda_sq, x, 1, g.data(), 1); + } + + // c = R^{-T} g (in-place TRSM on a copy of g) + std::copy(g.begin(), g.end(), c.begin()); + t0 = clock::now(); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, + n, 1, (T)1.0, R, ldr, c.data(), n); + t_trsm_total += std::chrono::duration_cast(clock::now() - t0).count(); + + // Inner CG on M*z = c + int inner_iters = 0; + auto t_in0 = clock::now(); + int cg_status = inner_cg(J, R, ldr, c.data(), n, m, + z.data(), + cg_r.data(), cg_p.data(), cg_Mp.data(), + tmp_n.data(), tmp_m.data(), + inner_iters, t_trsm_total, t_fwd_total, t_adj_total); + t_inner_total += std::chrono::duration_cast(clock::now() - t_in0).count(); + inner_iters_per_step.push_back(inner_iters); + if (cg_status != 0) { + outer_iters_done = step; + final_residual_norm = r_norm / b_norm; + if (timing) populate_times(outer_start, t_inner_total, t_trsm_total, + t_fwd_total, t_adj_total); + return cg_status; + } + + // dx = R^{-1} z + std::copy(z.begin(), z.end(), dx.begin()); + t0 = clock::now(); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + n, 1, (T)1.0, R, ldr, dx.data(), n); + t_trsm_total += std::chrono::duration_cast(clock::now() - t0).count(); + + // x ← x + dx + blas::axpy(n, (T)1.0, dx.data(), 1, x, 1); + outer_iters_done = step + 1; + } + + // Final residual report + { + auto t0 = clock::now(); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); + t_fwd_total += std::chrono::duration_cast(clock::now() - t0).count(); + for (int64_t i = 0; i < m; ++i) tmp_m[i] = b[i] - tmp_m[i]; + final_residual_norm = blas::nrm2(m, tmp_m.data(), 1) / b_norm; + } + + if (timing) populate_times(outer_start, t_inner_total, t_trsm_total, + t_fwd_total, t_adj_total); + return 0; + } + +private: + // Inner CG: solve M z = c, where M = R^{-T} J^T J R^{-1}, on ℝ^n. + // Workspaces (caller-allocated, length n unless noted): cg_r, cg_p, cg_Mp, + // tmp_n (for R^{-1} v), tmp_m (m-length, for J v_pre). + template + int inner_cg(J_LO& J, const T* R, int64_t ldr, + const T* c, int64_t n, int64_t m, + T* z, + T* cg_r, T* cg_p, T* cg_Mp, + T* tmp_n, T* tmp_m, + int& iters_out, + long& t_trsm, long& t_fwd, long& t_adj) + { + using clock = std::chrono::steady_clock; + // Initial guess z = 0. + std::fill(z, z + n, (T)0); + + // r = c - M*z = c (since z=0) + std::copy(c, c + n, cg_r); + std::copy(c, c + n, cg_p); + + T c_norm = blas::nrm2(n, c, 1); + T tol_abs = inner_tol * c_norm; + if (c_norm == (T)0) { + iters_out = 0; + return 0; + } + + T rs_old = blas::dot(n, cg_r, 1, cg_r, 1); + + for (int it = 0; it < max_inner_iters; ++it) { + // Mp = M * p = R^{-T} J^T J R^{-1} p + // tmp_n = R^{-1} p + std::copy(cg_p, cg_p + n, tmp_n); + auto t0 = clock::now(); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + n, 1, (T)1.0, R, ldr, tmp_n, n); + t_trsm += std::chrono::duration_cast(clock::now() - t0).count(); + + // tmp_m = J * tmp_n + t0 = clock::now(); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, tmp_n, n, (T)0.0, tmp_m, m); + t_fwd += std::chrono::duration_cast(clock::now() - t0).count(); + + // cg_Mp = J^T * tmp_m + t0 = clock::now(); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, 1, m, (T)1.0, tmp_m, m, (T)0.0, cg_Mp, n); + t_adj += std::chrono::duration_cast(clock::now() - t0).count(); + + // Add Tikhonov term: M_reg v = R^{-T} (J^T J + lambda^2 I) R^{-1} v. + // We have JtJv ≡ cg_Mp = J^T J R^{-1} v, and tmp_n ≡ R^{-1} v. + // So (J^T J + lambda^2 I) R^{-1} v = JtJv + lambda^2 * tmp_n. + if (lambda != (T)0) { + T lambda_sq = lambda * lambda; + blas::axpy(n, lambda_sq, tmp_n, 1, cg_Mp, 1); + } + + // cg_Mp ← R^{-T} cg_Mp (in-place TRSM) + t0 = clock::now(); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, + n, 1, (T)1.0, R, ldr, cg_Mp, n); + t_trsm += std::chrono::duration_cast(clock::now() - t0).count(); + + T pMp = blas::dot(n, cg_p, 1, cg_Mp, 1); + if (!(pMp > 0)) { + iters_out = it; + return 1; // CG breakdown (loss of orthogonality / non-SPD M) + } + T alpha = rs_old / pMp; + + // z ← z + alpha p + blas::axpy(n, alpha, cg_p, 1, z, 1); + // r ← r - alpha Mp + blas::axpy(n, -alpha, cg_Mp, 1, cg_r, 1); + + T rs_new = blas::dot(n, cg_r, 1, cg_r, 1); + T r_norm = std::sqrt(rs_new); + + if (verbose) { + std::printf("[IR-LSQ] inner CG iter %d: ||r||/||c|| = %.4e\n", + it + 1, (double)(r_norm / c_norm)); + } + if (r_norm <= tol_abs) { + iters_out = it + 1; + return 0; + } + + T beta = rs_new / rs_old; + // p ← r + beta p + for (int64_t i = 0; i < n; ++i) cg_p[i] = cg_r[i] + beta * cg_p[i]; + rs_old = rs_new; + } + iters_out = max_inner_iters; + return 0; // hit cap; not necessarily an error — caller can inspect inner_iters + } + + void populate_times(std::chrono::steady_clock::time_point outer_start, + long inner, long trsm, long fwd, long adj) + { + using clock = std::chrono::steady_clock; + long outer_total = std::chrono::duration_cast( + clock::now() - outer_start).count(); + long other = outer_total - inner - trsm - fwd - adj; + times = { outer_total, inner, trsm, fwd, adj, other }; + } +}; + + +} // namespace RandLAPACK diff --git a/RandLAPACK/linops/rl_kronecker_linop.hh b/RandLAPACK/linops/rl_kronecker_linop.hh new file mode 100644 index 000000000..a337c1f96 --- /dev/null +++ b/RandLAPACK/linops/rl_kronecker_linop.hh @@ -0,0 +1,334 @@ +#pragma once + +// Public API: KroneckerOperator — implicit Kronecker-product linear operator +// A = A2 ⊗ A1 with two small dense factors. + +#include "rl_concepts.hh" +#include "rl_blaspp.hh" + +#include +#include +#include + + +namespace RandLAPACK::linops { + +/*********************************************************/ +/* */ +/* KroneckerOperator */ +/* */ +/*********************************************************/ + +/// @brief Linear operator representing A = A2 ⊗ A1, with small dense factors. +/// +/// @details Applies the Kronecker product implicitly via the identity +/// (A2 ⊗ A1) vec(X) = vec(A1 X A2^T), with X = reshape(x, n1, n2). +/// Each matvec is two GEMM calls (one with A1, one with A2). The full +/// operator A is m1*m2 × n1*n2 and is never materialized. Both factors are +/// stored in column-major layout with leading dimensions m1 (for A1) and +/// m2 (for A2). +/// +/// Use case: tall, structured least-squares problems whose forward operator +/// has Kronecker structure — most notably 2D NMR relaxometry (PRnmr in the +/// IR Tools toolbox) and any separable-kernel Fredholm integral equation. +/// +/// Storage: this operator owns its A1 and A2 buffers (allocated in the +/// constructor, freed by the destructor). The factors are typically tiny +/// (e.g., 256×128 each at default NMR scale) compared to the implicit +/// m1*m2 × n1*n2 operator. +/// +/// Supported call patterns: +/// - Side::Left, NoTrans/NoTrans, dense B — forward apply (per-col loop) +/// - Side::Left, Trans/NoTrans, dense B — adjoint apply (per-col loop) +/// - Side::Right, NoTrans/NoTrans, dense B — used by Side::Right SkOp path +/// - Side::Right, NoTrans/NoTrans, sparse SkOp — sketch step (nnz-loop) +/// +/// Limitations: Side::Right with general dense B uses a row-loop dispatch; +/// other configurations (trans_B != NoTrans, sketch with dense SkOp, etc.) +/// will throw via randblas_require. +template +struct KroneckerOperator { + using scalar_t = T; + const int64_t n_rows; ///< m1 * m2 + const int64_t n_cols; ///< n1 * n2 + const int64_t m1, n1, m2, n2; + T* A1; ///< m1 × n1, ColMajor, lda = m1, owned + T* A2; ///< m2 × n2, ColMajor, lda = m2, owned + + /// Construct a Kronecker operator. The constructor copies A1 and A2 into + /// internally-owned buffers (the source pointers may be freed afterwards). + KroneckerOperator(int64_t m1_, int64_t n1_, int64_t m2_, int64_t n2_, + const T* A1_buff, const T* A2_buff) + : n_rows(m1_ * m2_), n_cols(n1_ * n2_), + m1(m1_), n1(n1_), m2(m2_), n2(n2_) + { + A1 = new T[m1 * n1]; + A2 = new T[m2 * n2]; + std::copy(A1_buff, A1_buff + m1 * n1, A1); + std::copy(A2_buff, A2_buff + m2 * n2, A2); + } + + ~KroneckerOperator() { + delete[] A1; + delete[] A2; + } + + KroneckerOperator(const KroneckerOperator&) = delete; + KroneckerOperator& operator=(const KroneckerOperator&) = delete; + + /// @brief Frobenius norm: ||A2 ⊗ A1||_F = ||A1||_F * ||A2||_F. + T fro_nrm() { + T n1_fro = blas::nrm2(m1 * n1, A1, 1); + T n2_fro = blas::nrm2(m2 * n2, A2, 1); + return n1_fro * n2_fro; + } + + // ----------------------------------------------------------------- + // Concept-required overload (no Side; defaults to Side::Left) + // ----------------------------------------------------------------- + void operator()( + Layout layout, Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) + { + (*this)(Side::Left, layout, trans_A, trans_B, m, n, k, alpha, B, ldb, beta, C, ldc); + } + + // ----------------------------------------------------------------- + // Dense matmul with explicit Side + // Side::Left: C = alpha * op(A) * B + beta * C + // Side::Right: C = alpha * B * op(A) + beta * C + // ----------------------------------------------------------------- + void operator()( + Side side, Layout layout, Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) + { + randblas_require(layout == Layout::ColMajor); + randblas_require(trans_B == Op::NoTrans); + + if (side == Side::Left) { + apply_left_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); + } else { + apply_right_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); + } + } + + // ----------------------------------------------------------------- + // Sketching-operator overloads + // ----------------------------------------------------------------- + template + void operator()( + Layout layout, Op trans_A, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, T beta, T* C, int64_t ldc) + { + (*this)(Side::Right, layout, trans_A, trans_S, m, n, k, alpha, S, beta, C, ldc); + } + + template + void operator()( + Side side, Layout layout, Op trans_A, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, T beta, T* C, int64_t ldc) + { + randblas_require(layout == Layout::ColMajor); + randblas_require(side == Side::Right); + randblas_require(trans_A == Op::NoTrans); + randblas_require(trans_S == Op::NoTrans); + // Side::Right, NoTrans/NoTrans: C = alpha * S * A + beta * C + // m = rows of C = rows of S = d + // n = cols of C = cols of A = n1*n2 + // k = inner = cols of S = rows of A = m1*m2 + randblas_require(k == m1 * m2); + randblas_require(n == n1 * n2); + randblas_require(ldc >= m); + + if constexpr (requires { S.buff; S.layout; S.dist; }) { + // Dense SkOp: forward to the dense Side::Right path using S.buff. + if (S.buff == nullptr) RandBLAS::fill_dense(S); + int64_t lds = S.dist.dim_major; + // RandBLAS DenseSkOp may be RowMajor — densely-stored SkOps store data + // with a single layout. If S.layout != ColMajor, treat the buffer as a + // ColMajor view of S^T (which is m × d), and apply with trans_S = Trans. + // Currently we only need NoTrans so require matching layout. + randblas_require(S.layout == Layout::ColMajor); + apply_right_dense(Op::NoTrans, m, n, k, alpha, S.buff, lds, beta, C, ldc); + } else { + // Sparse SkOp (SASO): iterate nnz directly. No materialization. + if (S.nnz < 0) RandBLAS::fill_sparse(S); + auto S_coo = RandBLAS::coo_view_of_skop(S); + apply_right_sparse_coo(m, n, alpha, beta, + S_coo.nnz, S_coo.rows, S_coo.cols, S_coo.vals, + C, ldc); + } + } + +private: + // ----------------------------------------------------------------- + // Side::Left dense apply: C = alpha * op(A) * B + beta * C + // + // Per-column loop. For each column j of B (length n1*n2 in NoTrans case + // or m1*m2 in Trans case), reshape into a small matrix, apply two GEMMs. + // Math: + // NoTrans: A x = vec(A1 X A2^T), X = reshape(x, n1, n2), output m1×m2 + // Trans: A^T y = vec(A1^T Y A2), Y = reshape(y, m1, m2), output n1×n2 + // ----------------------------------------------------------------- + void apply_left_dense(Op trans_A, int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + int64_t rows_in, cols_in, rows_out, cols_out; + Op trans_first, trans_second; + const T* fac1; int64_t lda_fac1; + const T* fac2; int64_t lda_fac2; + + if (trans_A == Op::NoTrans) { + // dims-before-op for op(A): (m, k) = (n_rows, n_cols) + randblas_require(m == n_rows); + randblas_require(k == n_cols); + rows_in = n1; cols_in = n2; + rows_out = m1; cols_out = m2; + // GEMM 1: Tmp = A1 * X (m1 × n2) <- (m1 × n1) * (n1 × n2) + // GEMM 2: Y = Tmp * A2^T (m1 × m2) <- (m1 × n2) * (n2 × m2) + trans_first = Op::NoTrans; fac1 = A1; lda_fac1 = m1; + trans_second = Op::Trans; fac2 = A2; lda_fac2 = m2; + } else { + // dims-before-op for op(A) = A^T: (m, k) = (n_cols, n_rows) + randblas_require(m == n_cols); + randblas_require(k == n_rows); + rows_in = m1; cols_in = m2; + rows_out = n1; cols_out = n2; + // GEMM 1: Tmp = A1^T * Y (n1 × m2) <- (n1 × m1) * (m1 × m2) + // GEMM 2: Z = Tmp * A2 (n1 × n2) <- (n1 × m2) * (m2 × n2) + trans_first = Op::Trans; fac1 = A1; lda_fac1 = m1; + trans_second = Op::NoTrans; fac2 = A2; lda_fac2 = m2; + } + + randblas_require(ldb >= rows_in * cols_in); // B columns are length k = rows_in*cols_in + randblas_require(ldc >= rows_out * cols_out); + + std::vector Tmp(static_cast(rows_out) * cols_in); + + for (int64_t j = 0; j < n; ++j) { + const T* Bj = B + j * ldb; // n1*n2 (or m1*m2) vector + T* Cj = C + j * ldc; // m1*m2 (or n1*n2) vector + + // X = reshape(Bj, rows_in, cols_in) ← view, no copy (lda = rows_in) + // Tmp = op(fac1) * X (rows_out × cols_in) + blas::gemm( + Layout::ColMajor, + trans_first, Op::NoTrans, + rows_out, cols_in, rows_in, + (T)1.0, + fac1, lda_fac1, + Bj, rows_in, + (T)0.0, + Tmp.data(), rows_out + ); + // Y = alpha * Tmp * op(fac2)^T (rows_out × cols_out) + // (where the "T" depends on direction: Trans for forward, NoTrans for adjoint) + blas::gemm( + Layout::ColMajor, + Op::NoTrans, trans_second, + rows_out, cols_out, cols_in, + alpha, + Tmp.data(), rows_out, + fac2, lda_fac2, + beta, + Cj, rows_out + ); + } + } + + // ----------------------------------------------------------------- + // Side::Right dense apply: C = alpha * B * op(A) + beta * C + // Implementation: row-by-row of B, delegate to Side::Left with flipped trans_A. + // C[i, :] = (B[i, :]) * op(A) = (op(A)^T * B[i, :]^T)^T + // ----------------------------------------------------------------- + void apply_right_dense(Op trans_A, int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + int64_t rows_op_A, cols_op_A; + if (trans_A == Op::NoTrans) { + rows_op_A = n_rows; cols_op_A = n_cols; + } else { + rows_op_A = n_cols; cols_op_A = n_rows; + } + randblas_require(k == rows_op_A); + randblas_require(n == cols_op_A); + + Op flipped = (trans_A == Op::NoTrans) ? Op::Trans : Op::NoTrans; + + std::vector b_row(k); + std::vector c_row(n); + + for (int64_t i = 0; i < m; ++i) { + // b_row = B[i, :] (gather across the i-th row in ColMajor B) + for (int64_t j = 0; j < k; ++j) b_row[j] = B[i + j * ldb]; + + // c_row = op(A)^T * b_row (Side::Left with flipped trans_A, single column) + apply_left_dense(flipped, n, /*ncols*/1, k, + (T)1.0, b_row.data(), k, + (T)0.0, c_row.data(), n); + + // C[i, :] = alpha * c_row + beta * C[i, :] + for (int64_t j = 0; j < n; ++j) { + T& ce = C[i + j * ldc]; + ce = alpha * c_row[j] + beta * ce; + } + } + } + +public: + // ----------------------------------------------------------------- + // Sparse-COO sketch apply: C = alpha * S * A + beta * C + // S is given as a COO view (rows[l], cols[l], vals[l]) with `nnz` entries. + // For each nonzero (i, p, val): C[i, :] += alpha * val * A[p, :], where + // A[p, j] = A1[p_inner, j_inner] * A2[p_outer, j_outer], with the standard + // Kronecker convention p = p_outer*m1 + p_inner, j = j_outer*n1 + j_inner. + // + // Public so wrapper operators (e.g., RegularizedLinOp) can dispatch a + // pre-filtered COO directly without re-materializing through SparseSkOp. + // ----------------------------------------------------------------- + template + void apply_right_sparse_coo(int64_t m, int64_t n, + T alpha, T beta, + int64_t nnz, const SInt* rows, const SInt* cols, const T* vals, + T* C, int64_t ldc) + { + // Scale C by beta first. + if (beta == (T)0) { + for (int64_t j = 0; j < n; ++j) + std::fill(C + j * ldc, C + j * ldc + m, (T)0); + } else if (beta != (T)1) { + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i < m; ++i) + C[i + j * ldc] *= beta; + } + + // For each nonzero, accumulate alpha * val * outer_prod(A1[p_inner,:], A2[p_outer,:]) + // into C[i, :] viewed as n1 × n2. + for (int64_t l = 0; l < nnz; ++l) { + int64_t i = rows[l]; + int64_t p = cols[l]; + T av = alpha * vals[l]; + + int64_t p_outer = p / m1; + int64_t p_inner = p % m1; + + // C[i, j_outer*n1 + j_inner] += av * A1[p_inner, j_inner] * A2[p_outer, j_outer] + for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { + T scaled = av * A2[p_outer + j_outer * m2]; + if (scaled == (T)0) continue; + T* C_col_block = C + (j_outer * n1) * ldc + i; // start of i-th row, j_outer*n1-th col block + for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { + C_col_block[j_inner * ldc] += scaled * A1[p_inner + j_inner * m1]; + } + } + } + } +}; + +} // end namespace RandLAPACK::linops diff --git a/RandLAPACK/linops/rl_linops.hh b/RandLAPACK/linops/rl_linops.hh index b981f2c35..a76d426e5 100644 --- a/RandLAPACK/linops/rl_linops.hh +++ b/RandLAPACK/linops/rl_linops.hh @@ -16,3 +16,5 @@ #include "rl_composite_linop.hh" #include "rl_sym_linops.hh" #include "rl_materialize.hh" +#include "rl_kronecker_linop.hh" +#include "rl_regularized_linop.hh" diff --git a/RandLAPACK/linops/rl_regularized_linop.hh b/RandLAPACK/linops/rl_regularized_linop.hh new file mode 100644 index 000000000..c3c6b6cc6 --- /dev/null +++ b/RandLAPACK/linops/rl_regularized_linop.hh @@ -0,0 +1,269 @@ +#pragma once + +// Public API: RegularizedLinOp — wraps a tall LinearOperator J as the +// augmented (m+n) × n operator +// +// ⎡ J ⎤ +// A_aug = ⎣ λI ⎦ +// +// so that QR(A_aug) succeeds even when J is highly ill-conditioned (σ_min(J) +// can be anything, but σ_min(A_aug) ≥ λ by construction). Solving +// min ||A_aug x − [b; 0]||₂² is mathematically equivalent to the Tikhonov- +// regularized LS problem min ||J x − b||₂² + λ²||x||₂². This is the +// standard augmentation trick for handling ill-posed inverse problems. + +#include "rl_concepts.hh" +#include "rl_blaspp.hh" + +#include +#include +#include + + +namespace RandLAPACK::linops { + +/*********************************************************/ +/* */ +/* RegularizedLinOp */ +/* */ +/*********************************************************/ + +/// @brief Tikhonov-augmented LinearOperator wrapper: A_aug = [J; λI]. +/// +/// @tparam JLO Underlying tall LinearOperator type. Must satisfy +/// `LinearOperator` and provide both forward + adjoint apply +/// and a Side::Right + SkOp overload (used by the sketch step +/// of CQRRT_linops). +template +struct RegularizedLinOp { + using scalar_t = typename JLO::scalar_t; + using T = scalar_t; + + JLO& J; + const T lambda; + const int64_t m_J; ///< J.n_rows + const int64_t n_J; ///< J.n_cols + const int64_t n_rows; ///< m_J + n_J + const int64_t n_cols; ///< n_J + + RegularizedLinOp(JLO& J_in, T lambda_in) + : J(J_in), lambda(lambda_in), + m_J(J_in.n_rows), n_J(J_in.n_cols), + n_rows(J_in.n_rows + J_in.n_cols), + n_cols(J_in.n_cols) {} + + // ----------------------------------------------------------------- + // Concept-required overload (no Side; defaults to Side::Left) + // ----------------------------------------------------------------- + void operator()( + Layout layout, Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) + { + (*this)(Side::Left, layout, trans_A, trans_B, m, n, k, alpha, B, ldb, beta, C, ldc); + } + + // ----------------------------------------------------------------- + // Dense matmul with Side + // Side::Left: C = alpha * op(A_aug) * B + beta * C + // Side::Right: C = alpha * B * op(A_aug) + beta * C + // ----------------------------------------------------------------- + void operator()( + Side side, Layout layout, Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) + { + randblas_require(layout == Layout::ColMajor); + randblas_require(trans_B == Op::NoTrans); + + if (side == Side::Left) { + apply_left_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); + } else { + apply_right_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); + } + } + + // ----------------------------------------------------------------- + // Sketching-operator overloads + // + // Side::Right, NoTrans/NoTrans corresponds to the CQRRT_linops sketch + // step `A_hat = S * A_aug` where S is d × (m_J+n_J). + // ----------------------------------------------------------------- + template + void operator()( + Layout layout, Op trans_A, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, T beta, T* C, int64_t ldc) + { + (*this)(Side::Right, layout, trans_A, trans_S, m, n, k, alpha, S, beta, C, ldc); + } + + template + void operator()( + Side side, Layout layout, Op trans_A, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, T beta, T* C, int64_t ldc) + { + randblas_require(layout == Layout::ColMajor); + randblas_require(side == Side::Right); + randblas_require(trans_A == Op::NoTrans); + randblas_require(trans_S == Op::NoTrans); + randblas_require(k == m_J + n_J); + randblas_require(n == n_J); + randblas_require(ldc >= m); + + if constexpr (requires { S.buff; S.layout; S.dist; }) { + // Dense SkOp: split S.buff column-wise. Forwards to dense Side::Right. + if (S.buff == nullptr) RandBLAS::fill_dense(S); + int64_t lds = S.dist.dim_major; + randblas_require(S.layout == Layout::ColMajor); + apply_right_dense(Op::NoTrans, m, n, k, alpha, S.buff, lds, beta, C, ldc); + } else { + // Sparse SASO path. Partition S's COO into J-touching nonzeros + // (col < m_J) and I-touching nonzeros (col ≥ m_J), then dispatch + // each subset in bulk: + // J-touching → one call to J.apply_right_sparse_coo (assumes + // JLO exposes it; KroneckerOperator does) + // I-touching → scalar updates C[i, p − m_J] += α·val·λ + // This avoids the per-nonzero J-adjoint apply that scales as + // O(nnz_J × cost(J adj)) and dominated runtime at moderate n. + if (S.nnz < 0) RandBLAS::fill_sparse(S); + auto S_coo = RandBLAS::coo_view_of_skop(S); + using sint_t = std::remove_pointer_t; + + // First pass: count J-touching nonzeros. + int64_t nnz_J = 0; + for (int64_t l = 0; l < S_coo.nnz; ++l) { + if (static_cast(S_coo.cols[l]) < m_J) ++nnz_J; + } + int64_t nnz_I = S_coo.nnz - nnz_J; + + // Second pass: copy J-touching into local arrays. + std::vector J_rows; J_rows.reserve(nnz_J); + std::vector J_cols; J_cols.reserve(nnz_J); + std::vector J_vals; J_vals.reserve(nnz_J); + // I-touching: collected into parallel arrays for the scalar pass. + std::vector I_rows; I_rows.reserve(nnz_I); + std::vector I_cols; I_cols.reserve(nnz_I); + std::vector I_vals; I_vals.reserve(nnz_I); + for (int64_t l = 0; l < S_coo.nnz; ++l) { + int64_t p = static_cast(S_coo.cols[l]); + if (p < m_J) { + J_rows.push_back(S_coo.rows[l]); + J_cols.push_back(S_coo.cols[l]); + J_vals.push_back(S_coo.vals[l]); + } else { + I_rows.push_back(S_coo.rows[l]); + I_cols.push_back(p - m_J); + I_vals.push_back(S_coo.vals[l]); + } + } + + // J-touching: bulk dispatch via J's COO entry point. + // The call writes C ← α·(S_J · J) + β·C in one pass. + J.apply_right_sparse_coo(m, n_J, alpha, beta, + nnz_J, J_rows.data(), J_cols.data(), J_vals.data(), + C, ldc); + + // I-touching: scalar updates C[i, col] += α·val·λ. + // Note: the β scaling above already applied to the entire C, so + // we just accumulate (no need to re-apply β). + T scale = alpha * lambda; + for (int64_t l = 0; l < nnz_I; ++l) { + C[I_rows[l] + I_cols[l] * ldc] += scale * I_vals[l]; + } + } + } + +private: + // ----------------------------------------------------------------- + // Side::Left dense apply: C = alpha * op(A_aug) * B + beta * C + // + // NoTrans: C is (m_J + n_J) × n_blas. + // C_top (first m_J rows) ← alpha * J * B + beta * C_top + // C_bot (last n_J rows) ← alpha * λ * B + beta * C_bot + // + // Trans: C is n_J × n_blas, B is (m_J + n_J) × n_blas. + // C ← alpha * (J^T * B_top + λ * B_bot) + beta * C + // ----------------------------------------------------------------- + void apply_left_dense(Op trans_A, int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + if (trans_A == Op::NoTrans) { + randblas_require(m == n_rows); + randblas_require(k == n_cols); + randblas_require(ldc >= n_rows); + + // Top: C[0:m_J, :] = alpha * J * B + beta * C[0:m_J, :] + J(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m_J, n, n_J, alpha, B, ldb, beta, C, ldc); + + // Bottom: C[m_J:m_J+n_J, :] = alpha * λ * B + beta * C[m_J:..., :] + for (int64_t j = 0; j < n; ++j) { + T* Cj_bot = C + j * ldc + m_J; + const T* Bj = B + j * ldb; + T scale = alpha * lambda; + if (beta == (T)0) { + for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] = scale * Bj[i]; + } else if (beta == (T)1) { + for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] += scale * Bj[i]; + } else { + for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] = scale * Bj[i] + beta * Cj_bot[i]; + } + } + } else { + // Trans: m == n_cols == n_J, k == n_rows == m_J + n_J + randblas_require(m == n_cols); + randblas_require(k == n_rows); + randblas_require(ldb >= n_rows); + + // First: C = alpha * J^T * B_top + beta * C + // B_top is the top m_J rows of B (length-m_J vectors), stride ldb. + J(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n_J, n, m_J, alpha, B, ldb, beta, C, ldc); + + // Then: C += alpha * λ * B_bot, where B_bot starts at row m_J of B. + for (int64_t j = 0; j < n; ++j) { + T* Cj = C + j * ldc; + const T* Bj_bot = B + j * ldb + m_J; + T scale = alpha * lambda; + for (int64_t i = 0; i < n_J; ++i) Cj[i] += scale * Bj_bot[i]; + } + } + } + + // ----------------------------------------------------------------- + // Side::Right dense apply: C = alpha * B * op(A_aug) + beta * C + // + // NoTrans on A_aug: B is p × (m_J + n_J), output C is p × n_J. + // B = [B1 | B2], B1 = p × m_J, B2 = p × n_J. + // C = alpha * (B1 * J + λ * B2) + beta * C. + // ----------------------------------------------------------------- + void apply_right_dense(Op trans_A, int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + randblas_require(trans_A == Op::NoTrans); // Trans on Side::Right is uncommon + randblas_require(k == n_rows); + randblas_require(n == n_cols); + randblas_require(ldb >= n_rows); + randblas_require(ldc >= m); + + // First: C = alpha * B1 * J + beta * C (B1 is the leftmost m_J cols of B) + // J's Side::Right takes a (p × m_J) dense block and returns p × n_J. + J(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, n, m_J, alpha, B, ldb, beta, C, ldc); + + // Then: C += alpha * λ * B2 where B2 starts at column m_J of B. + T scale = alpha * lambda; + for (int64_t j = 0; j < n; ++j) { + T* Cj = C + j * ldc; + const T* Bj_2 = B + (m_J + j) * ldb; + // B2 is (m × n_J), col-major. B2[:, j] starts at B + (m_J + j)*ldb. + for (int64_t i = 0; i < m; ++i) Cj[i] += scale * Bj_2[i]; + } + } +}; + +} // end namespace RandLAPACK::linops diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 2d5be5054..fa6da7377 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -159,6 +159,8 @@ set( add_benchmark(NAME CQRRT_linop_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_diagnostic CXX_SOURCES bench_CQRRT_linops/CQRRT_diagnostic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) +# NMR / Kronecker-operator benchmark — no Eigen / fast_matrix_market dependency +add_benchmark(NAME CQRRT_linop_nmr CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_nmr.cc LINK_LIBS ${Benchmark_libs}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 8c621d198..91f7d6560 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -1,17 +1,19 @@ -// Generalized SVD / Generalized LS benchmark +// Generalized SVD benchmark // // Pipeline: -// 1. Load K.mtx (m x m SPD) and V.mtx (m x n sparse) -// 2. Cholesky factorize K = LL^T, create L^{-1} operator (half_solve=true) -// 3. Create composite operator: CompositeOperator(L_inv_op, V_op) = L^{-1}V -// 4. Run Q-less QR on L^{-1}V via CQRRT_linops, CholQR_linops, sCholQR3_linops -// 5. Application (a): Generalized LS — solve min_x ||Vx - b||_{K^{-1}} -// 6. Application (b): Generalized singular values — SVD of R -// 7. Application (c): Generalized singular vectors — full SVD of R +// 1. Load K.mtx (m x m SPD) and V.mtx (m x n sparse) (composite mode) +// OR a single A.mtx as a SparseLinOp (sparse mode) +// 2. (Composite only) Cholesky-factorize K = LL^T, create L^{-1} operator +// 3. (Composite only) Form CompositeOperator(L_inv_op, V_op) = L^{-1}V +// 4. Run Q-less QR on the operator via CQRRT_linops, CholQR_linops, sCholQR3_linops +// 5. SVD post-processing: gesdd of R for generalized singular values + vectors +// +// (For LS post-processing on Kronecker-structured operators see CQRRT_linop_nmr.) // // Usage: // ./CQRRT_linop_applications -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask] +// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] +// [upcast_orth] [method_mask] // // method_mask (integer bitmask, optional, default = 0b001111 or 0b011111 if run_expl=1): // bit 0 ( 1): CQRRT_linop @@ -85,19 +87,14 @@ struct gsvd_result { // Orthogonality computed with upcast reconstruction (float->double or double->long double) double orth_error_upcast; - // Application (a): Generalized LS - long app_a_time_us; // Post-processing time only - T ls_rel_error; // ||x - x_true|| / ||x_true|| - - // Application (b): Generalized singular values + // SVD post-processing (b): generalized singular values long app_b_time_us; // SVD of R time - // Application (c): Generalized singular vectors + // SVD post-processing (c): generalized singular vectors long app_c_time_us; // Full SVD of R + V_R orthogonality check T right_svec_orth_error; // ||V_R^T V_R - I||_F / sqrt(n) // Totals (QR + application post-processing) - long total_a_time_us; long total_b_time_us; long total_c_time_us; @@ -239,49 +236,7 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, } // ============================================================================ -// Application (a): Generalized Least Squares -// min_x ||Vx - b||_{K^{-1}} via R from QR of L^{-1}V -// -// Solution: x = R^{-1} R^{-T} V^T K^{-1} b -// Steps: c = K^{-1}b, d = V^T c, solve R^T y = d, solve Rx = y -// ============================================================================ - -template -static void app_generalized_ls( - RandLAPACK_extras::linops::CholSolverLinOp& K_inv_op, - VLinOp& V_op, - const T* R, int64_t ldr, int64_t n, - const T* b, int64_t m, - T* x, - long& app_time_us) -{ - auto start = steady_clock::now(); - - // Step 1: c = K^{-1} b (m x 1) - std::vector c(m, 0.0); - K_inv_op(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, m, (T)1.0, b, m, (T)0.0, c.data(), m); - - // Step 2: d = V^T c (n x 1) - std::vector d(n, 0.0); - V_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, 1, m, (T)1.0, c.data(), m, (T)0.0, d.data(), n); - - // Step 3: solve R^T y = d (n x 1) - std::copy(d.begin(), d.end(), x); - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, - blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); - - // Step 4: solve R x = y (n x 1) - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, x, n); - - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); -} - -// ============================================================================ -// Application (b): Generalized Singular Values +// SVD post-processing (b): Generalized Singular Values // SVD of R gives the generalized singular values of (V, K) // ============================================================================ @@ -307,7 +262,7 @@ static void app_generalized_svals( } // ============================================================================ -// Application (c): Generalized Singular Vectors +// SVD post-processing (c): Generalized Singular Vectors // Full SVD of R: R = U_R * Sigma * V_R^T // Right generalized singular vectors = columns of V_R // ============================================================================ @@ -375,10 +330,9 @@ static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_r out << "# GSVD Benchmark results\n"; write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," - << "app_a_time_us,ls_rel_error," << "app_b_time_us," << "app_c_time_us,right_svec_orth_error," - << "total_a_time_us,total_b_time_us,total_c_time_us," + << "total_b_time_us,total_c_time_us," << "peak_rss_kb,analytical_kb\n"; } @@ -390,12 +344,10 @@ static void write_csv_row(std::ofstream& out, const gsvd_result& r) { << std::scientific << std::setprecision(6) << r.orth_error << "," << std::scientific << std::setprecision(6) << r.r_backward_error << "," << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," - << r.app_a_time_us << "," - << std::scientific << std::setprecision(6) << r.ls_rel_error << "," << r.app_b_time_us << "," << r.app_c_time_us << "," << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," - << r.total_a_time_us << "," << r.total_b_time_us << "," << r.total_c_time_us << "," + << r.total_b_time_us << "," << r.total_c_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "\n"; } @@ -453,9 +405,9 @@ static void print_summary(const std::string& alg_name, const std::vector 0) printf(" orth_upcast=%.2e\n", r.orth_error_upcast); - if (r.app_a_time_us > 0 || r.ls_rel_error > 0) { - printf(" LS_err=%.2e, App(a)=%ld us, App(b)=%ld us, App(c)=%ld us\n", - (double)r.ls_rel_error, r.app_a_time_us, r.app_b_time_us, r.app_c_time_us); + if (r.app_b_time_us > 0 || r.app_c_time_us > 0) { + printf(" SVD: App(b)=%ld us, App(c)=%ld us\n", + r.app_b_time_us, r.app_c_time_us); } printf(" Memory: peak_RSS=%ld KB, predicted=%ld KB\n", r.peak_rss_kb, r.analytical_kb); @@ -476,8 +428,7 @@ static int run_benchmark_inner( bool skip_apps, bool compute_cond, bool upcast_orth, int64_t method_mask, long chol_time_us, const std::string& K_file, const std::string& V_file, - const std::string& op_label, - std::function&, gsvd_result&)> app_a_fn) + const std::string& op_label) { // Condition number diagnostic (materializes A_op, runs two SVDs) if (compute_cond) { @@ -598,12 +549,11 @@ static int run_benchmark_inner( res.orth_error_upcast = 0.0; } - if (!skip_apps) { - // App (a): generalized LS — only in composite mode (app_a_fn is null in sparse mode) - if (app_a_fn) { - app_a_fn(R, res); - } + res.app_b_time_us = 0; + res.app_c_time_us = 0; + res.right_svec_orth_error = (T)-1.0; + if (!skip_apps) { std::vector sigma_b(n, 0.0); app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); @@ -612,7 +562,6 @@ static int run_benchmark_inner( res.right_svec_orth_error, res.app_c_time_us); } - res.total_a_time_us = res.qr_time_us + res.app_a_time_us; res.total_b_time_us = res.qr_time_us + res.app_b_time_us; res.total_c_time_us = res.qr_time_us + res.app_c_time_us; all_results.push_back(res); @@ -781,7 +730,8 @@ int run_benchmark(int argc, char* argv[]) { if (argc < 7) { std::cerr << "Usage: " << argv[0] << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask]\n" + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl]" + << " [upcast_orth] [method_mask]\n" << " sparse mode: pass 'sparse' as K_file and a single A.mtx as V_file\n" << " method_mask: bitmask selecting algorithms (default = 0b001111 or 0b011111 if run_expl=1)\n" << " bit 0 ( 1): CQRRT_linop\n" @@ -857,24 +807,17 @@ int run_benchmark(int argc, char* argv[]) { // Sparse mode: wrap A directly as SparseLinOp, skip Cholesky // ================================================================ if (sparse_mode) { - if (!skip_apps) { - std::cout << " Note: in sparse mode, app (a) (Gen. LS) is unavailable (requires K).\n" - << " Apps (b) and (c) will still run.\n"; - } - std::function&, gsvd_result&)> app_a_fn = nullptr; return run_benchmark_inner( V_linop, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, skip_apps, compute_cond, upcast_orth, method_mask, 0L /*chol_time_us*/, "sparse", V_file, - "A (" + V_file + ")", app_a_fn); + "A (" + V_file + ")"); } // ================================================================ - // Composite mode: Steps 2-4 + // Composite mode: Cholesky-factorize K, build L^{-1}V composite operator // ================================================================ - - // Step 2: Create L^{-1} operator (half_solve=true) and K^{-1} operator std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); @@ -882,63 +825,26 @@ int run_benchmark(int argc, char* argv[]) { L_inv_op.factorize(); auto chol_stop = steady_clock::now(); long chol_time_us = duration_cast(chol_stop - chol_start).count(); - - // Also create full K^{-1} for App (a) — only needed when running apps - std::unique_ptr> K_inv_op_ptr; - if (!skip_apps) { - K_inv_op_ptr = std::make_unique>(K_file, /*half_solve=*/false); - K_inv_op_ptr->factorize(); - } std::cout << "done (" << chol_time_us << " us)\n"; - // Step 3: Form composite operator L^{-1} * V RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); LiV_op.block_size = block_size; - std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n"; - - // Step 4: Generate synthetic RHS: b = V * x_true (only when running apps) - RandBLAS::RNGState rng_state(42); - std::vector x_true(n); - std::vector b(m, 0.0); - T x_true_norm = 0.0; - if (!skip_apps) { - RandBLAS::DenseDist D(n, 1); - auto next_state = RandBLAS::fill_dense(D, x_true.data(), rng_state); - rng_state = next_state; - - V_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b.data(), m); - - x_true_norm = blas::nrm2(n, x_true.data(), 1); - std::cout << "Generated b = V * x_true (||x_true|| = " << x_true_norm << ")\n"; - } - std::cout << "\n"; - - // App (a) callback — captures K_inv_op_ptr, V_linop, b, x_true, x_true_norm - std::function&, gsvd_result&)> app_a_fn = nullptr; - if (!skip_apps) { - app_a_fn = [&](const std::vector& R, gsvd_result& res) { - std::vector x_computed(n, 0.0); - app_generalized_ls(*K_inv_op_ptr, V_linop, R.data(), n, n, - b.data(), m, x_computed.data(), res.app_a_time_us); - blas::axpy(n, (T)-1.0, x_true.data(), 1, x_computed.data(), 1); - res.ls_rel_error = blas::nrm2(n, x_computed.data(), 1) / x_true_norm; - }; - } + std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n\n"; return run_benchmark_inner( LiV_op, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, skip_apps, compute_cond, upcast_orth, method_mask, chol_time_us, K_file, V_file, - "L^{-1}V", app_a_fn); + "L^{-1}V"); } int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] [upcast_orth] [method_mask]\n"; + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl]" + << " [upcast_orth] [method_mask]\n"; return 1; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc new file mode 100644 index 000000000..85964b52e --- /dev/null +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc @@ -0,0 +1,584 @@ +// 2D NMR Relaxometry Benchmark — Q-less QR + iterative-refinement LSQ. +// +// Test problem: PRnmr from the IR Tools toolbox (Gazzola, Hansen, Nagy 2018). +// Forward operator A = A2 ⊗ A1 with separable Laplace-type kernel: +// +// (A1)_{l,k} = 1 - 2 * exp(-tau1[l] / T1[k]) (m1 × n1) +// (A2)_{l,k} = exp(-tau2[l] / T2[k]) (m2 × n2) +// +// Time grids are logspace(taulogleft, taulogright). Default ranges follow +// the paper (T, tau ∈ [10^{-4}, 10^1]). Operator dimensions: +// M = m1*m2 (rows), N = n1*n2 (cols), default m_i = 2*n_i. +// +// Pipeline: +// 1. Generate A1, A2, phantom x_true ∈ ℝ^{n1*n2}, b = A x_true + noise. +// 2. Wrap A as KroneckerOperator (no materialization). +// 3. Run CQRRT_linops on A → R (n × n upper triangular). +// 4. Run IterRefineLSQ(A, R, b) → x. +// 5. Report ||x - x_true||/||x_true||, ||b - Ax||/||b||, IR iter counts, +// timing, and analytical-memory predictions. +// +// Usage: +// ./CQRRT_linop_nmr +// [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size] +// [log_lo] [log_hi] [method_mask] [lambda] +// where: +// prec = "double" | "float" +// n = scalar; sets n1 = n2 = n, m1 = m2 = 2*n +// phantom = "two_blob" (default) | "one_blob" +// noise_level = ||noise||/||b|| (default 0.05) +// method_mask = bitmask selecting Q-less QR variants to run, ALL piped +// through the IR-LSQ pipeline. Default 0b1101111 (all +// linop variants; CQRRT_expl excluded since materializing +// the Kronecker would defeat the purpose). +// bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) +// bit 1 ( 2): CholQR +// bit 2 ( 4): sCholQR3 +// bit 3 ( 8): sCholQR3_basic +// bit 5 ( 32): CQRRT_linop_stb (GEQP3) +// bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP) +// lambda = Tikhonov regularization. min ||Jx-b||² + λ²||x||². +// When λ>0 the QR is run on the augmented operator +// A_aug = [J; λI] so it succeeds even when J alone is +// too ill-conditioned for Q-less QR. + +#include "RandLAPACK.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "RandLAPACK/testing/rl_memory_tracker.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif + + +using std::chrono::steady_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + + +// ============================================================================= +// Grid generation +// ============================================================================= +// logspace(a, b, n) -> n points evenly spaced in log10 from 10^a to 10^b. +template +static std::vector logspace(T a, T b, int64_t n) { + std::vector v(n); + if (n == 1) { v[0] = std::pow((T)10, a); return v; } + T step = (b - a) / (T)(n - 1); + for (int64_t i = 0; i < n; ++i) v[i] = std::pow((T)10, a + step * (T)i); + return v; +} + + +// ============================================================================= +// Build A1 and A2 per the PRnmr formulas. +// ============================================================================= +template +static void build_A_factors(int64_t m1, int64_t n1, int64_t m2, int64_t n2, + T log_lo, T log_hi, + std::vector& A1, std::vector& A2) +{ + auto T1 = logspace(log_lo, log_hi, n1); + auto T2 = logspace(log_lo, log_hi, n2); + auto tau1 = logspace(log_lo, log_hi, m1); + auto tau2 = logspace(log_lo, log_hi, m2); + + A1.assign(m1 * n1, (T)0); + A2.assign(m2 * n2, (T)0); + // ColMajor: A1[i + j*m1] = (A1)_{i,j} = 1 - 2*exp(-tau1[i] / T1[j]) + for (int64_t j = 0; j < n1; ++j) + for (int64_t i = 0; i < m1; ++i) + A1[i + j * m1] = (T)1.0 - (T)2.0 * std::exp(-tau1[i] / T1[j]); + for (int64_t j = 0; j < n2; ++j) + for (int64_t i = 0; i < m2; ++i) + A2[i + j * m2] = std::exp(-tau2[i] / T2[j]); +} + + +// ============================================================================= +// Phantom generation: synthetic 2D T1-T2 distribution. +// ============================================================================= +// "two_blob": sum of two 2D Gaussians in (log T1, log T2) space — coarse +// approximation of multi-population NMR phantoms (carbonate / methane). +// "one_blob": single Gaussian (simpler reference). +template +static std::vector make_phantom(int64_t n1, int64_t n2, + T log_lo, T log_hi, + const std::string& kind) +{ + std::vector x(n1 * n2, (T)0); + auto T1 = logspace(log_lo, log_hi, n1); + auto T2 = logspace(log_lo, log_hi, n2); + + auto add_gaussian = [&](T mu1, T mu2, T sigma1, T sigma2, T amp) { + for (int64_t j2 = 0; j2 < n2; ++j2) { + T lt2 = std::log10(T2[j2]); + for (int64_t j1 = 0; j1 < n1; ++j1) { + T lt1 = std::log10(T1[j1]); + T d1 = (lt1 - mu1) / sigma1; + T d2 = (lt2 - mu2) / sigma2; + x[j1 + j2 * n1] += amp * std::exp(-(T)0.5 * (d1 * d1 + d2 * d2)); + } + } + }; + + if (kind == "one_blob") { + add_gaussian((T)-1.0, (T)-1.0, (T)0.5, (T)0.5, (T)1.0); + } else { + // "two_blob" (default) + add_gaussian((T)-2.5, (T)-2.5, (T)0.4, (T)0.4, (T)1.0); // short relaxation + add_gaussian((T) 0.0, (T)-0.5, (T)0.5, (T)0.5, (T)0.7); // long relaxation + } + return x; +} + + +// ============================================================================= +// Result struct +// ============================================================================= +template +struct nmr_result { + int64_t m, n, m1, n1, m2, n2; + int64_t run_idx; + std::string alg_name; // "CQRRT_linop", "CholQR", "sCholQR3", "sCholQR3_basic", "CQRRT_linop_stb", "CQRRT_linop_stb_bqrrp" + std::string phantom; + T noise_level; + int qr_status; // 0 on success; nonzero indicates QR breakdown (no IR-LSQ run) + + // Q-less QR (CQRRT_linop) + long qr_time_us; + T orth_error; // ||Q^T Q - I||_F / sqrt(n) (only if cheap to check) + long peak_rss_kb; + long analytical_kb; + + // IR LSQ + long ir_total_us; + int ir_outer_iters; + int ir_inner_iters_total; + T ls_residual_norm; // ||b - A x|| / ||b|| + T ls_solution_error; // ||x - x_true|| / ||x_true|| + + // QR breakdown (CQRRT_linop times: 11 entries) + std::vector qr_breakdown; + // IR breakdown (6 entries: outer, inner, trsm, fwd, adj, other) + std::vector ir_breakdown; +}; + + +template +static void print_summary(const nmr_result& r) { + std::printf("\n [%s] Run %ld (phantom=%s, noise=%.3f):\n", + r.alg_name.c_str(), (long)r.run_idx, r.phantom.c_str(), (double)r.noise_level); + if (r.qr_status != 0) { + std::printf(" QR returned status %d — IR-LSQ skipped.\n", r.qr_status); + return; + } + std::printf(" QR: %ld us, peak_RSS=%ld KB, predicted=%ld KB\n", + r.qr_time_us, r.peak_rss_kb, r.analytical_kb); + if (r.orth_error >= 0) std::printf(" orth_err = %.3e\n", (double)r.orth_error); + std::printf(" IR-LSQ: total=%ld us, outer=%d, inner_total=%d\n", + r.ir_total_us, r.ir_outer_iters, r.ir_inner_iters_total); + std::printf(" ||r||/||b|| = %.3e\n", (double)r.ls_residual_norm); + std::printf(" ||x-x_true||/||x_true|| = %.3e\n", (double)r.ls_solution_error); +} + + +// ============================================================================= +// Core templated runner +// ============================================================================= +template +int run_benchmark(int argc, char* argv[]) +{ + if (argc < 5) { + std::cerr << "Usage: " << argv[0] + << " " + << " [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size]" + << " [log_lo] [log_hi] [method_mask] [lambda]\n" + << " method_mask: bitmask of Q-less QR variants (default = 0b1101111)\n" + << " bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY)\n" + << " bit 1 ( 2): CholQR\n" + << " bit 2 ( 4): sCholQR3\n" + << " bit 3 ( 8): sCholQR3_basic\n" + << " bit 5 ( 32): CQRRT_linop_stb (GEQP3)\n" + << " bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP)\n" + << " lambda: Tikhonov regularization. min ||Jx-b||² + λ²||x||²; default 0.0\n" + << " log_lo, log_hi: τ and T grids on logspace(log_lo, log_hi).\n" + << " Default (-2, 1) = 3 decades. The IR Tools paper uses (-4, 1) = 5 decades,\n" + << " but unregularized Q-less QR (any preconditioner) cannot handle that conditioning.\n"; + return 1; + } + + std::string output_dir = argv[2]; + int64_t num_runs = std::stol(argv[3]); + int64_t n = std::stol(argv[4]); + std::string phantom = (argc >= 6) ? std::string(argv[5]) : std::string("two_blob"); + T noise_level = (argc >= 7) ? (T)std::stod(argv[6]) : (T)0.05; + T d_factor = (argc >= 8) ? (T)std::stod(argv[7]) : (T)2.0; + int64_t sketch_nnz = (argc >= 9) ? std::stol(argv[8]) : 4; + int64_t block_size = (argc >= 10) ? std::stol(argv[9]) : 0; + T log_lo = (argc >= 11) ? (T)std::stod(argv[10]) : (T)-2.0; + T log_hi = (argc >= 12) ? (T)std::stod(argv[11]) : (T)1.0; + int64_t method_mask = (argc >= 13) ? std::stol(argv[12]) : 0b1101111; + T lambda_reg = (argc >= 14) ? (T)std::stod(argv[13]) : (T)0.0; + + int64_t n1 = n, n2 = n; + int64_t m1 = 2 * n1, m2 = 2 * n2; + int64_t M = m1 * m2; + int64_t N = n1 * n2; + + std::cout << "=== NMR Relaxometry Benchmark (Kronecker + CQRRT + IR-LSQ) ===\n" + << " m1, n1, m2, n2 = " << m1 << ", " << n1 << ", " << m2 << ", " << n2 << "\n" + << " M (=m1*m2) = " << M << ", N (=n1*n2) = " << N + << " (aspect M:N = " << (double)M / (double)N << ":1)\n" + << " phantom = " << phantom << "\n" + << " noise_lvl = " << noise_level << "\n" + << " method_mask= " << method_mask + << " (linop=" << (method_mask&1) + << " CholQR=" << ((method_mask>>1)&1) + << " sCholQR3=" << ((method_mask>>2)&1) + << " sCholQR3_basic=" << ((method_mask>>3)&1) + << " linop_stb=" << ((method_mask>>5)&1) + << " linop_stb_bqrrp=" << ((method_mask>>6)&1) << ")\n" + << " lambda = " << lambda_reg << "\n" + << " d_factor = " << d_factor << "\n" + << " sketch_nnz = " << sketch_nnz << "\n" + << " block_size = " << block_size << "\n" + << " num_runs = " << num_runs << "\n" +#ifdef _OPENMP + << " OpenMP threads: " << omp_get_max_threads() << "\n"; +#else + << " OpenMP threads: 1\n"; +#endif + + if (M < N) { + std::cerr << "Error: NMR setup requires M >= N (got M=" << M << ", N=" << N << ")\n"; + return 1; + } + + std::cout << " log range = [10^" << log_lo << ", 10^" << log_hi << "] (" + << (log_hi - log_lo) << " decades)\n"; + + // -------- Build A1, A2, KroneckerOperator -------- + std::vector A1, A2; + std::cout << "Building A1, A2 (logspace formulas)... " << std::flush; + auto build_t0 = steady_clock::now(); + build_A_factors(m1, n1, m2, n2, log_lo, log_hi, A1, A2); + auto build_t1 = steady_clock::now(); + std::cout << "done (" << duration_cast(build_t1 - build_t0).count() << " us)\n"; + + RandLAPACK::linops::KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); + std::cout << "Kronecker operator J = A2 ⊗ A1: " << M << " × " << N << "\n"; + + // When lambda > 0, the QR step runs on the augmented operator + // A_aug = [J; λI] (M+N rows × N cols), + // which is positive-definite-Gram by construction (σ_min ≥ λ). This + // bypasses the Cholesky-breakdown problem that hits unregularized Q-less + // QR on ill-conditioned operators (e.g., NMR Fredholm kernels). + // Solving min ||A_aug x − [b; 0]||² is mathematically the Tikhonov + // regularized problem min ||Jx − b||² + λ²||x||². + const bool use_augmented = (lambda_reg > (T)0); + RandLAPACK::linops::RegularizedLinOp> + J_aug(J, lambda_reg); + if (use_augmented) { + std::cout << "Augmented operator A_aug = [J; λI]: " << (M + N) << " × " << N + << " (λ = " << lambda_reg << ")\n"; + } + + // -------- Phantom + RHS -------- + std::vector x_true = make_phantom(n1, n2, log_lo, log_hi, phantom); + T x_true_norm = blas::nrm2(N, x_true.data(), 1); + if (x_true_norm == 0) { std::cerr << "Phantom is zero — aborting\n"; return 1; } + + // b = A * x_true (clean) + std::vector b_clean(M, (T)0), b(M, (T)0), noise_vec(M, (T)0); + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + M, 1, N, (T)1.0, x_true.data(), N, (T)0.0, b_clean.data(), M); + T b_clean_norm = blas::nrm2(M, b_clean.data(), 1); + + // Add Gaussian noise scaled to give ||noise||/||b_clean|| = noise_level. + { + std::mt19937 noise_rng(13); + std::normal_distribution N01(0, 1); + for (auto& v : noise_vec) v = N01(noise_rng); + T raw_noise_norm = blas::nrm2(M, noise_vec.data(), 1); + T scale = noise_level * b_clean_norm / raw_noise_norm; + for (int64_t i = 0; i < M; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; + } + T b_norm = blas::nrm2(M, b.data(), 1); + std::cout << "Synthetic LS problem: ||x_true|| = " << x_true_norm + << ", ||b|| = " << b_norm << "\n\n"; + + // Augmented RHS b_aug = [b; 0] (length M+N), used when lambda > 0. + std::vector b_aug; + if (use_augmented) { + b_aug.assign(M + N, (T)0); + std::copy(b.begin(), b.end(), b_aug.begin()); + } + + // -------- RNG states for runs -------- + RandBLAS::RNGState main_state(123); + std::vector> run_states(num_runs); + for (int64_t r = 0; r < num_runs; ++r) { + run_states[r] = main_state; + if (r > 0) run_states[r].key.incr(r); + } + T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); + + // -------- Warmup -------- + std::cout << "Running warmup... " << std::flush; + { + auto warm_state = run_states[0]; + std::vector R_warm(N * N, (T)0); + RandLAPACK::CQRRT_linops warm_algo(false, tol, false); + warm_algo.nnz = sketch_nnz; + warm_algo.block_size = block_size; + warm_algo.call(J, R_warm.data(), N, d_factor, warm_state); + } + std::cout << "done\n\n"; + + // -------- Per-(algorithm, run) lambda -------- + // Same body for J (unregularized) or J_aug (regularized) via the JLO template. + // m_eff = number of rows of the operator (M for J, M+N for J_aug). + // b_eff = synthetic RHS of length m_eff. + // ir_lambda = regularization strength to pass to IterRefineLSQ. When the QR + // ran on J_aug we set this to 0 (the regularization is built into A_aug); + // when the QR ran on J alone we pass lambda_reg through. + auto run_one = [&](const std::string& alg_name, + JLO& J_eff, const T* b_eff, int64_t m_eff, + T ir_lambda, int64_t r) -> nmr_result + { + nmr_result res{}; + res.m = M; res.n = N; + res.m1 = m1; res.n1 = n1; res.m2 = m2; res.n2 = n2; + res.run_idx = r; + res.alg_name = alg_name; + res.phantom = phantom; + res.noise_level = noise_level; + res.qr_status = 0; + + // ---- Q-less QR on J_eff: dispatch on alg_name ---- + std::cout << "[Run " << r << ", " << alg_name << "] QR ... " << std::flush; + std::vector R(N * N, (T)0); + auto state = run_states[r]; + + RandLAPACK::PeakRSSTracker mem; mem.start(); + if (alg_name == "sCholQR3" || alg_name == "sCholQR3_basic") { + // Both sCholQR3 variants share the same call signature; we use the + // fully-blocked one. (sCholQR3_basic has identical numerics here.) + RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(J_eff, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[17]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m_eff, N, block_size); + } + } else if (alg_name == "CholQR") { + RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(J_eff, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[5]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 6); + res.qr_breakdown.resize(11, 0); // pad to 11 for breakdown CSV uniformity + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m_eff, N, block_size); + } + } else { + // CQRRT_linops with selectable preconditioner. + RandLAPACK::CQRRT_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.nnz = sketch_nnz; + qr_algo.block_size = block_size; + if (alg_name == "CQRRT_linop") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; + else if (alg_name == "CQRRT_linop_stb") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::GEQP3; + else /* CQRRT_linop_stb_bqrrp */ qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; + res.qr_status = qr_algo.call(J_eff, R.data(), N, d_factor, state); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[10]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = (alg_name == "CQRRT_linop_stb_bqrrp") + ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(N, d_factor) + : RandLAPACK::cqrrt_linops_analytical_kb(m_eff, N, d_factor, block_size); + } + } + + if (res.qr_status != 0) { + std::cerr << "\n [" << alg_name << "] Run " << r + << ": QR returned status " << res.qr_status + << " (likely Cholesky breakdown).\n" + << " Try a different algorithm, larger d_factor, or increase lambda.\n"; + res.qr_time_us = -1; + res.qr_breakdown.assign(11, 0); + res.analytical_kb = 0; + res.orth_error = (T)-1.0; + res.ir_total_us = 0; + res.ir_outer_iters = 0; + res.ir_inner_iters_total = 0; + res.ls_residual_norm = (T)-1.0; + res.ls_solution_error = (T)-1.0; + print_summary(res); + return res; + } + res.orth_error = (T)-1.0; + std::cout << "done (" << res.qr_time_us << " us). IR-LSQ ... " << std::flush; + + // ---- IterRefineLSQ ---- + std::vector x_ls(N, (T)0); + RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, + /*max_inner=*/200, + /*n_steps=*/2, + /*timing=*/true, + /*verbose=*/false, + /*lambda=*/ir_lambda); + auto ls_t0 = steady_clock::now(); + int ir_status = ir.call(J_eff, R.data(), N, b_eff, m_eff, x_ls.data(), N); + auto ls_t1 = steady_clock::now(); + if (ir_status != 0) { + std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; + } + + res.ir_total_us = duration_cast(ls_t1 - ls_t0).count(); + res.ir_outer_iters = ir.outer_iters_done; + res.ir_inner_iters_total = 0; + for (int v : ir.inner_iters_per_step) res.ir_inner_iters_total += v; + res.ls_residual_norm = ir.final_residual_norm; + if (!ir.times.empty()) res.ir_breakdown = ir.times; + + // Solution error: ||x_ls - x_true|| / ||x_true|| + T err_sq = 0; + for (int64_t i = 0; i < N; ++i) { + T d = x_ls[i] - x_true[i]; + err_sq += d * d; + } + res.ls_solution_error = std::sqrt(err_sq) / x_true_norm; + std::cout << "done (" << res.ir_total_us << " us)\n"; + + print_summary(res); + return res; + }; + + // Build the ordered list of selected algorithm names from the bitmask. + // Bit assignments match CQRRT_linop_applications.cc (CQRRT_expl, bit 4, is + // intentionally excluded — materializing the Kronecker would defeat the + // purpose). sCholQR3_basic shares the implementation with sCholQR3 here. + std::vector selected_algs; + if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); + if (method_mask & 2) selected_algs.push_back("CholQR"); + if (method_mask & 4) selected_algs.push_back("sCholQR3"); + if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); + if (method_mask & 32) selected_algs.push_back("CQRRT_linop_stb"); + if (method_mask & 64) selected_algs.push_back("CQRRT_linop_stb_bqrrp"); + + if (selected_algs.empty()) { + std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; + return 1; + } + + std::vector> all_results; + for (const auto& alg_name : selected_algs) { + std::cout << "\n=== Algorithm: " << alg_name << " ===\n"; + for (int64_t r = 0; r < num_runs; ++r) { + nmr_result res; + if (use_augmented) { + res = run_one(alg_name, J_aug, b_aug.data(), M + N, (T)0, r); + } else { + res = run_one(alg_name, J, b.data(), M, lambda_reg, r); + } + all_results.push_back(std::move(res)); + } + } + + // -------- CSV output -------- + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + + std::string results_file = output_dir + "/" + time_buf + "_nmr_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_nmr_breakdown.csv"; + + { + std::ofstream out(results_file); + out << "# NMR Relaxometry Benchmark results\n" + << "# Date: " << ctime(&now) + << "# m1=" << m1 << " n1=" << n1 << " m2=" << m2 << " n2=" << n2 << "\n" + << "# M=" << M << " N=" << N << "\n" + << "# phantom=" << phantom << " noise_level=" << noise_level << "\n" + << "# d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz + << " block_size=" << block_size << "\n" + << "# method_mask=" << method_mask << " lambda=" << lambda_reg << "\n" + << "# log_lo=" << log_lo << " log_hi=" << log_hi << "\n" +#ifdef _OPENMP + << "# OpenMP threads: " << omp_get_max_threads() << "\n" +#else + << "# OpenMP threads: 1\n" +#endif + ; + out << "algorithm,run,m,n,qr_status,qr_time_us,peak_rss_kb,analytical_kb," + "ir_total_us,ir_outer_iters,ir_inner_iters_total," + "ls_residual_norm,ls_solution_error\n"; + for (const auto& r : all_results) { + out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," + << r.qr_status << "," << r.qr_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "," + << r.ir_total_us << "," << r.ir_outer_iters << "," << r.ir_inner_iters_total << "," + << std::scientific << std::setprecision(6) << r.ls_residual_norm << "," + << std::scientific << std::setprecision(6) << r.ls_solution_error + << "\n"; + } + std::cout << "\nResults written to " << results_file << "\n"; + } + + { + std::ofstream out(breakdown_file); + out << "# NMR Benchmark runtime breakdown (microseconds)\n" + << "# QR breakdown layout depends on algorithm:\n" + << "# CQRRT_linop[_stb*] (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n" + << "# sCholQR3 / sCholQR3_basic — full 18 entries truncated to first 11 (alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3)\n" + << "# CholQR (6, padded to 11): alloc, fwd, adj, chol, rest, total\n" + << "# IR-LSQ breakdown (6): outer_total, inner_cg_total, trsm_total, fwd_total, adj_total, other\n" + << "algorithm,run,phase,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10\n"; + for (const auto& r : all_results) { + out << r.alg_name << "," << r.run_idx << ",QR"; + for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; + for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + out << r.alg_name << "," << r.run_idx << ",IR"; + for (size_t i = 0; i < r.ir_breakdown.size(); ++i) out << "," << r.ir_breakdown[i]; + for (size_t i = r.ir_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + } + std::cout << "Breakdown written to " << breakdown_file << "\n"; + } + + return 0; +} + + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] + << " " + << " [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size]\n"; + return 1; + } + std::string prec = argv[1]; + if (prec == "double") return run_benchmark(argc, argv); + if (prec == "float") return run_benchmark(argc, argv); + std::cerr << "Unknown precision: " << prec << " (use 'double' or 'float')\n"; + return 1; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 815ae3586..7a1e7155f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,10 +22,13 @@ if (GTest_FOUND) drivers/test_hqrrp.cc drivers/test_abrik.cc drivers/test_orth_linop.cc + drivers/test_iter_refine_lsq.cc misc/test_util.cc misc/test_pdkernels.cc linops/test_linops.cc linops/test_linop_unified.cc + linops/test_kronecker_linop.cc + linops/test_regularized_linop.cc misc/test_gen.cc misc/test_memory_tracker.cc linops/test_linop_block_views.cc diff --git a/test/drivers/test_iter_refine_lsq.cc b/test/drivers/test_iter_refine_lsq.cc new file mode 100644 index 000000000..575854adb --- /dev/null +++ b/test/drivers/test_iter_refine_lsq.cc @@ -0,0 +1,223 @@ +// Unit tests for RandLAPACK::IterRefineLSQ — iterative-refinement LSQ +// using R as a right preconditioner. +// +// Strategy: wrap a small dense tall A as a DenseLinOp, build R from +// lapack::geqrf on a copy of A, then verify the IR-LSQ solution matches +// the gels reference within O(eps) on a synthetic well-conditioned LS +// problem. + +#include +#include +#include + +#include +#include + + +using RandLAPACK::IterRefineLSQ; +using RandLAPACK::linops::DenseLinOp; +using blas::Layout; + + +template +static void fill_random(std::vector& v, uint32_t seed, T scale = 1.0) { + std::mt19937 rng(seed); + std::uniform_real_distribution dist(-1.0, 1.0); + for (auto& x : v) x = scale * dist(rng); +} + + +// Build R from QR of A_copy (in-place destructive QR), zeroing the strictly +// lower triangle and copying the upper-triangular n × n into R. +template +static void build_R_from_A(const T* A, int64_t m, int64_t n, T* R, int64_t ldr) { + std::vector A_copy(m * n); + std::copy(A, A + m * n, A_copy.begin()); + std::vector tau(n); + lapack::geqrf(m, n, A_copy.data(), m, tau.data()); + // Copy upper-triangular n × n into R + for (int64_t j = 0; j < n; ++j) { + for (int64_t i = 0; i <= j; ++i) { + R[i + j * ldr] = A_copy[i + j * m]; + } + for (int64_t i = j + 1; i < n; ++i) { + R[i + j * ldr] = (T)0; + } + } +} + + +class TestIterRefineLSQ : public ::testing::Test {}; + + +// Well-conditioned synthetic problem; exact R from QR(A). M should be +// numerically identity, so CG converges in ~1 iteration. +TEST_F(TestIterRefineLSQ, dense_well_conditioned) { + using T = double; + int64_t m = 80, n = 12; + + std::vector A(m * n), b(m), x_true(n); + fill_random(A, 42); + fill_random(x_true, 99); + + // b = A * x_true (exact RHS, in the column space of A). + blas::gemm(Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, A.data(), m, x_true.data(), n, (T)0.0, b.data(), m); + + // Reference: gels destroys both A and b. + std::vector A_ref(A.begin(), A.end()), b_ref(b.begin(), b.end()); + lapack::gels(blas::Op::NoTrans, m, n, 1, A_ref.data(), m, b_ref.data(), m); + std::vector x_ref(b_ref.begin(), b_ref.begin() + n); + + // Build R from QR(A) — perfect preconditioner. + std::vector R(n * n, 0); + build_R_from_A(A.data(), m, n, R.data(), n); + + // Solve via IR-LSQ. + DenseLinOp J(m, n, A.data(), m, Layout::ColMajor); + IterRefineLSQ ir(/*tol=*/1e-12, /*max_inner=*/50, /*n_steps=*/2); + std::vector x_ir(n, 0); + int status = ir.call(J, R.data(), n, b.data(), m, x_ir.data(), n); + EXPECT_EQ(status, 0); + + // x_ir should match x_ref (within accumulated rounding error). + T diff_norm = 0; + for (int64_t i = 0; i < n; ++i) { + T d = x_ir[i] - x_ref[i]; + diff_norm += d * d; + } + diff_norm = std::sqrt(diff_norm); + T xref_norm = blas::nrm2(n, x_ref.data(), 1); + EXPECT_LT(diff_norm / xref_norm, 1e-10); + + // CG should converge fast with a perfect preconditioner. + EXPECT_LE(ir.inner_iters_per_step.front(), 5); +} + + +// LS with a real residual (b not exactly in range(A)). +TEST_F(TestIterRefineLSQ, dense_with_residual) { + using T = double; + int64_t m = 100, n = 7; + + std::vector A(m * n), b(m), noise(m), x_true(n); + fill_random(A, 7); + fill_random(x_true, 17); + fill_random(noise, 27, 0.05); + + // b = A * x_true + noise + blas::gemm(Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, A.data(), m, x_true.data(), n, (T)0.0, b.data(), m); + for (int64_t i = 0; i < m; ++i) b[i] += noise[i]; + + std::vector A_ref(A.begin(), A.end()), b_ref(b.begin(), b.end()); + lapack::gels(blas::Op::NoTrans, m, n, 1, A_ref.data(), m, b_ref.data(), m); + std::vector x_ref(b_ref.begin(), b_ref.begin() + n); + + std::vector R(n * n, 0); + build_R_from_A(A.data(), m, n, R.data(), n); + + DenseLinOp J(m, n, A.data(), m, Layout::ColMajor); + IterRefineLSQ ir(1e-12, 50, 2); + std::vector x_ir(n, 0); + int status = ir.call(J, R.data(), n, b.data(), m, x_ir.data(), n); + EXPECT_EQ(status, 0); + + T diff_norm = 0; + for (int64_t i = 0; i < n; ++i) { + T d = x_ir[i] - x_ref[i]; + diff_norm += d * d; + } + diff_norm = std::sqrt(diff_norm); + T xref_norm = blas::nrm2(n, x_ref.data(), 1); + EXPECT_LT(diff_norm / xref_norm, 1e-10); +} + + +// Tikhonov regularization: solving min ||Jx-b||² + λ²||x||² should match +// the closed-form solution x = (J^T J + λ² I)^{-1} J^T b. +TEST_F(TestIterRefineLSQ, tikhonov_regularization) { + using T = double; + int64_t m = 80, n = 12; + T lambda = 0.3; + + std::vector A(m * n), b(m); + fill_random(A, 101); + fill_random(b, 102); + + // Reference: solve (A^T A + λ² I) x = A^T b directly via lapack::posv. + std::vector Gram(n * n, 0.0), AtB(n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, m, (T)1.0, A.data(), m, (T)0.0, Gram.data(), n); + for (int64_t i = 0; i < n; ++i) Gram[i + i * n] += lambda * lambda; + blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, m, n, (T)1.0, A.data(), m, + b.data(), 1, (T)0.0, AtB.data(), 1); + std::vector x_ref(AtB); + lapack::posv(blas::Uplo::Upper, n, 1, Gram.data(), n, x_ref.data(), n); + + // Build R from QR(A) — perfect preconditioner for J^T J alone, plus the + // Tikhonov shift gets added inside the inner CG. + std::vector R(n * n, 0); + build_R_from_A(A.data(), m, n, R.data(), n); + + DenseLinOp J(m, n, A.data(), m, Layout::ColMajor); + IterRefineLSQ ir(/*tol=*/1e-12, /*max_inner=*/200, /*n_steps=*/2, + /*timing=*/false, /*verbose=*/false, /*lambda=*/lambda); + std::vector x_ir(n, 0); + int status = ir.call(J, R.data(), n, b.data(), m, x_ir.data(), n); + EXPECT_EQ(status, 0); + + // Compare: x_ir vs x_ref, both solving the same regularized normal equations. + T diff_norm = 0; + for (int64_t i = 0; i < n; ++i) { + T d = x_ir[i] - x_ref[i]; + diff_norm += d * d; + } + diff_norm = std::sqrt(diff_norm); + T xref_norm = blas::nrm2(n, x_ref.data(), 1); + EXPECT_LT(diff_norm / xref_norm, 1e-10); +} + + +// Imperfect R: from QR of a slightly perturbed A. CG should take more iters +// but the final solution must still match gels. +TEST_F(TestIterRefineLSQ, imperfect_preconditioner) { + using T = double; + int64_t m = 60, n = 10; + + std::vector A(m * n), b(m), x_true(n); + fill_random(A, 3); + fill_random(x_true, 13); + + // b in col space (zero residual). + blas::gemm(Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, A.data(), m, x_true.data(), n, (T)0.0, b.data(), m); + + std::vector A_ref(A.begin(), A.end()), b_ref(b.begin(), b.end()); + lapack::gels(blas::Op::NoTrans, m, n, 1, A_ref.data(), m, b_ref.data(), m); + std::vector x_ref(b_ref.begin(), b_ref.begin() + n); + + // Perturbed A for R-construction (simulates a sketch-based R). + std::vector A_pert = A, perturb(m * n); + fill_random(perturb, 99, 0.1); + for (size_t i = 0; i < A_pert.size(); ++i) A_pert[i] += perturb[i]; + std::vector R(n * n, 0); + build_R_from_A(A_pert.data(), m, n, R.data(), n); + + DenseLinOp J(m, n, A.data(), m, Layout::ColMajor); + IterRefineLSQ ir(1e-12, 100, 2); + std::vector x_ir(n, 0); + int status = ir.call(J, R.data(), n, b.data(), m, x_ir.data(), n); + EXPECT_EQ(status, 0); + + T diff_norm = 0; + for (int64_t i = 0; i < n; ++i) { + T d = x_ir[i] - x_ref[i]; + diff_norm += d * d; + } + diff_norm = std::sqrt(diff_norm); + T xref_norm = blas::nrm2(n, x_ref.data(), 1); + EXPECT_LT(diff_norm / xref_norm, 1e-9); + // Imperfect R: CG should take more than 1 iter but well under the cap. + EXPECT_LT(ir.inner_iters_per_step.front(), 100); +} diff --git a/test/linops/test_kronecker_linop.cc b/test/linops/test_kronecker_linop.cc new file mode 100644 index 000000000..46753e0c6 --- /dev/null +++ b/test/linops/test_kronecker_linop.cc @@ -0,0 +1,185 @@ +// Unit tests for KroneckerOperator: +// forward / adjoint dense apply (single column, multi-column), +// sparse SASO sketching (S * A) via the SkOp overload. +// All tests verify against an explicitly-materialized A = kron(A2, A1). + +#include +#include +#include + +#include +#include + + +using RandLAPACK::linops::KroneckerOperator; +using RandLAPACK::linops::DenseLinOp; +using blas::Layout; +using blas::Op; +using blas::Side; + + +// Build A = kron(A2, A1) explicitly in ColMajor as an (m1*m2) × (n1*n2) buffer. +template +static void materialize_kron(int64_t m1, int64_t n1, int64_t m2, int64_t n2, + const T* A1, const T* A2, T* A_out) +{ + int64_t M = m1 * m2; + int64_t N = n1 * n2; + for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { + for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { + int64_t j = j_outer * n1 + j_inner; + for (int64_t i_outer = 0; i_outer < m2; ++i_outer) { + T a2 = A2[i_outer + j_outer * m2]; + for (int64_t i_inner = 0; i_inner < m1; ++i_inner) { + int64_t i = i_outer * m1 + i_inner; + A_out[i + j * M] = a2 * A1[i_inner + j_inner * m1]; + } + } + } + } + (void)M; (void)N; +} + + +template +static void fill_random(std::vector& v, uint32_t seed) { + std::mt19937 rng(seed); + std::uniform_real_distribution dist(-1.0, 1.0); + for (auto& x : v) x = dist(rng); +} + + +template +static T frobenius_diff(const std::vector& a, const std::vector& b) { + T sum = 0; + for (size_t i = 0; i < a.size(); ++i) { + T d = a[i] - b[i]; + sum += d * d; + } + return std::sqrt(sum); +} + + +class TestKroneckerOperator : public ::testing::Test {}; + + +// Test forward apply (NoTrans) and adjoint (Trans) for single-column input. +TEST_F(TestKroneckerOperator, forward_and_adjoint_single_column) { + using T = double; + int64_t m1 = 4, n1 = 3, m2 = 5, n2 = 2; + int64_t M = m1 * m2; // 20 + int64_t N = n1 * n2; // 6 + + std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); + fill_random(A1, 11); + fill_random(A2, 22); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + + KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); + DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); + + // forward: y = A x + { + std::vector x(N), y_kop(M, 0), y_dense(M, 0); + fill_random(x, 33); + kop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M, 1, N, (T)1.0, x.data(), N, (T)0.0, y_kop.data(), M); + dop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M, 1, N, (T)1.0, x.data(), N, (T)0.0, y_dense.data(), M); + EXPECT_LT(frobenius_diff(y_kop, y_dense), 1e-12); + } + // adjoint: z = A^T y + { + std::vector y(M), z_kop(N, 0), z_dense(N, 0); + fill_random(y, 44); + kop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, 1, M, (T)1.0, y.data(), M, (T)0.0, z_kop.data(), N); + dop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, 1, M, (T)1.0, y.data(), M, (T)0.0, z_dense.data(), N); + EXPECT_LT(frobenius_diff(z_kop, z_dense), 1e-12); + } +} + + +// Test alpha and beta correctness on a multi-column input. +TEST_F(TestKroneckerOperator, alpha_beta_multi_column) { + using T = double; + int64_t m1 = 3, n1 = 2, m2 = 4, n2 = 3; + int64_t M = m1 * m2, N = n1 * n2; + int64_t k = 5; + + std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); + fill_random(A1, 51); + fill_random(A2, 52); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + + KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); + DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); + + // forward: B is N × k, C accumulator nonzero + { + std::vector B(N * k), C_kop(M * k), C_dense(M * k); + fill_random(B, 61); + fill_random(C_kop, 71); + C_dense = C_kop; + T alpha = 0.7, beta = -0.3; + kop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M, k, N, alpha, B.data(), N, beta, C_kop.data(), M); + dop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M, k, N, alpha, B.data(), N, beta, C_dense.data(), M); + EXPECT_LT(frobenius_diff(C_kop, C_dense), 1e-12); + } + // adjoint: B is M × k, C accumulator nonzero + { + std::vector B(M * k), C_kop(N * k), C_dense(N * k); + fill_random(B, 81); + fill_random(C_kop, 91); + C_dense = C_kop; + T alpha = 1.3, beta = 0.2; + kop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, k, M, alpha, B.data(), M, beta, C_kop.data(), N); + dop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, k, M, alpha, B.data(), M, beta, C_dense.data(), N); + EXPECT_LT(frobenius_diff(C_kop, C_dense), 1e-12); + } +} + + +// Test sparse-SASO sketch: A_hat = S * A. +// The SkOp overload should match what DenseLinOp produces on the materialized A. +TEST_F(TestKroneckerOperator, sparse_sketch_apply) { + using T = double; + using RNG = r123::Philox4x32; + int64_t m1 = 6, n1 = 4, m2 = 5, n2 = 3; + int64_t M = m1 * m2; // 30 + int64_t N = n1 * n2; // 12 + int64_t d = 8; + int64_t saso_nnz = 3; + + std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); + fill_random(A1, 101); + fill_random(A2, 102); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + + KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); + DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); + + // Build a sparse SASO sketch and apply via both linops. + RandBLAS::RNGState state(42); + RandBLAS::SparseDist DS(d, M, saso_nnz); + RandBLAS::SparseSkOp S(DS, state); + RandBLAS::fill_sparse(S); + auto state2 = state; + RandBLAS::SparseSkOp S2(DS, state2); + RandBLAS::fill_sparse(S2); + + std::vector A_hat_kop(d * N, 0); + std::vector A_hat_dense(d * N, 0); + + kop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, N, M, (T)1.0, S, (T)0.0, A_hat_kop.data(), d); + dop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, N, M, (T)1.0, S2, (T)0.0, A_hat_dense.data(), d); + + EXPECT_LT(frobenius_diff(A_hat_kop, A_hat_dense), 1e-12); +} diff --git a/test/linops/test_regularized_linop.cc b/test/linops/test_regularized_linop.cc new file mode 100644 index 000000000..fe89c2323 --- /dev/null +++ b/test/linops/test_regularized_linop.cc @@ -0,0 +1,221 @@ +// Unit tests for RegularizedLinOp: +// verifies that A_aug = [J; λI] applies correctly for forward, adjoint, +// and sparse-SASO sketch paths, by comparing against an explicitly-built +// (m+n) × n dense matrix. + +#include +#include +#include + +#include +#include + + +using RandLAPACK::linops::KroneckerOperator; +using RandLAPACK::linops::RegularizedLinOp; +using RandLAPACK::linops::DenseLinOp; +using blas::Layout; +using blas::Op; +using blas::Side; + + +// Build A = kron(A2, A1) explicitly in ColMajor. +template +static void materialize_kron(int64_t m1, int64_t n1, int64_t m2, int64_t n2, + const T* A1, const T* A2, T* A_out) +{ + int64_t M = m1 * m2; + for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { + for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { + int64_t j = j_outer * n1 + j_inner; + for (int64_t i_outer = 0; i_outer < m2; ++i_outer) { + T a2 = A2[i_outer + j_outer * m2]; + for (int64_t i_inner = 0; i_inner < m1; ++i_inner) { + int64_t i = i_outer * m1 + i_inner; + A_out[i + j * M] = a2 * A1[i_inner + j_inner * m1]; + } + } + } + } +} + + +// Build A_aug = [A; λI] explicitly in ColMajor. +template +static void materialize_augmented(int64_t M, int64_t N, T lambda, + const T* A, T* A_aug) +{ + int64_t M_aug = M + N; + // Top: copy A into rows [0..M). + for (int64_t j = 0; j < N; ++j) { + for (int64_t i = 0; i < M; ++i) { + A_aug[i + j * M_aug] = A[i + j * M]; + } + // Bottom: λI in rows [M..M+N). + for (int64_t i = 0; i < N; ++i) { + A_aug[(M + i) + j * M_aug] = (i == j) ? lambda : (T)0; + } + } +} + + +template +static void fill_random(std::vector& v, uint32_t seed) { + std::mt19937 rng(seed); + std::uniform_real_distribution dist(-1.0, 1.0); + for (auto& x : v) x = dist(rng); +} + + +template +static T frobenius_diff(const std::vector& a, const std::vector& b) { + T sum = 0; + for (size_t i = 0; i < a.size(); ++i) { + T d = a[i] - b[i]; + sum += d * d; + } + return std::sqrt(sum); +} + + +class TestRegularizedLinOp : public ::testing::Test {}; + + +// Forward + adjoint single column on a Kronecker-backed RegularizedLinOp +// must match the explicitly-materialized A_aug = [kron(A2,A1); λI]. +TEST_F(TestRegularizedLinOp, kron_forward_adjoint_single_column) { + using T = double; + int64_t m1 = 4, n1 = 3, m2 = 5, n2 = 2; + int64_t M = m1 * m2; + int64_t N = n1 * n2; + int64_t M_aug = M + N; + T lambda = 0.7; + + std::vector A1(m1 * n1), A2(m2 * n2); + fill_random(A1, 11); + fill_random(A2, 22); + + std::vector A_full(M * N), A_aug_full(M_aug * N); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); + + KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); + RegularizedLinOp> J_aug(J, lambda); + DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); + + // Forward: y = A_aug x + { + std::vector x(N), y_aug(M_aug, 0), y_dense(M_aug, 0); + fill_random(x, 33); + J_aug(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M_aug, 1, N, (T)1.0, x.data(), N, (T)0.0, y_aug.data(), M_aug); + A_aug_dense(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M_aug, 1, N, (T)1.0, x.data(), N, (T)0.0, y_dense.data(), M_aug); + EXPECT_LT(frobenius_diff(y_aug, y_dense), 1e-12); + } + // Adjoint: z = A_aug^T y + { + std::vector y(M_aug), z_aug(N, 0), z_dense(N, 0); + fill_random(y, 44); + J_aug(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, 1, M_aug, (T)1.0, y.data(), M_aug, (T)0.0, z_aug.data(), N); + A_aug_dense(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, 1, M_aug, (T)1.0, y.data(), M_aug, (T)0.0, z_dense.data(), N); + EXPECT_LT(frobenius_diff(z_aug, z_dense), 1e-12); + } +} + + +// alpha/beta + multi-column on forward and adjoint. +TEST_F(TestRegularizedLinOp, kron_alpha_beta_multi_column) { + using T = double; + int64_t m1 = 3, n1 = 2, m2 = 4, n2 = 3; + int64_t M = m1 * m2; + int64_t N = n1 * n2; + int64_t M_aug = M + N; + int64_t k = 5; + T lambda = 0.4; + + std::vector A1(m1 * n1), A2(m2 * n2); + fill_random(A1, 51); + fill_random(A2, 52); + std::vector A_full(M * N), A_aug_full(M_aug * N); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); + + KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); + RegularizedLinOp> J_aug(J, lambda); + DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); + + // Forward with non-trivial alpha/beta. + { + std::vector B(N * k), C_aug(M_aug * k), C_dense(M_aug * k); + fill_random(B, 61); + fill_random(C_aug, 71); + C_dense = C_aug; + T alpha = 0.7, beta = -0.3; + J_aug(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M_aug, k, N, alpha, B.data(), N, beta, C_aug.data(), M_aug); + A_aug_dense(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + M_aug, k, N, alpha, B.data(), N, beta, C_dense.data(), M_aug); + EXPECT_LT(frobenius_diff(C_aug, C_dense), 1e-11); + } + // Adjoint with non-trivial alpha/beta. + { + std::vector B(M_aug * k), C_aug(N * k), C_dense(N * k); + fill_random(B, 81); + fill_random(C_aug, 91); + C_dense = C_aug; + T alpha = 1.3, beta = 0.2; + J_aug(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, k, M_aug, alpha, B.data(), M_aug, beta, C_aug.data(), N); + A_aug_dense(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + N, k, M_aug, alpha, B.data(), M_aug, beta, C_dense.data(), N); + EXPECT_LT(frobenius_diff(C_aug, C_dense), 1e-11); + } +} + + +// Sparse SASO sketch via the COO-filter dispatch path: +// J_aug(Side::Right, NoTrans, NoTrans, S, ...) must match DenseLinOp on the +// materialized A_aug. +TEST_F(TestRegularizedLinOp, sparse_sketch_apply) { + using T = double; + using RNG = r123::Philox4x32; + int64_t m1 = 6, n1 = 4, m2 = 5, n2 = 3; + int64_t M = m1 * m2; + int64_t N = n1 * n2; + int64_t M_aug = M + N; + int64_t d = 8; + int64_t saso_nnz = 3; + T lambda = 0.6; + + std::vector A1(m1 * n1), A2(m2 * n2); + fill_random(A1, 101); + fill_random(A2, 102); + std::vector A_full(M * N), A_aug_full(M_aug * N); + materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); + materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); + + KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); + RegularizedLinOp> J_aug(J, lambda); + DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); + + RandBLAS::RNGState state(42); + RandBLAS::SparseDist DS(d, M_aug, saso_nnz); + RandBLAS::SparseSkOp S(DS, state); + RandBLAS::fill_sparse(S); + auto state2 = state; + RandBLAS::SparseSkOp S2(DS, state2); + RandBLAS::fill_sparse(S2); + + std::vector A_hat_aug(d * N, 0); + std::vector A_hat_dense(d * N, 0); + + J_aug(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, N, M_aug, (T)1.0, S, (T)0.0, A_hat_aug.data(), d); + A_aug_dense(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, N, M_aug, (T)1.0, S2, (T)0.0, A_hat_dense.data(), d); + + EXPECT_LT(frobenius_diff(A_hat_aug, A_hat_dense), 1e-12); +} From d92e012d501bef056040cf0627133c40fcf95454 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 7 May 2026 16:52:16 -0400 Subject: [PATCH 15/47] Comments update --- RandLAPACK/drivers/rl_iter_refine_lsq.hh | 19 +++++----- .../bench_CQRRT_linops/CQRRT_linop_nmr.cc | 35 +++++++++++++------ test/drivers/test_iter_refine_lsq.cc | 6 ++-- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/RandLAPACK/drivers/rl_iter_refine_lsq.hh b/RandLAPACK/drivers/rl_iter_refine_lsq.hh index a1c3b67c9..18d50d6fd 100644 --- a/RandLAPACK/drivers/rl_iter_refine_lsq.hh +++ b/RandLAPACK/drivers/rl_iter_refine_lsq.hh @@ -1,16 +1,19 @@ #pragma once // Public API: IterRefineLSQ — Q-less, sketch-and-precondition iterative-refinement -// least-squares solver. +// least-squares solver, with optional Tikhonov. // -// Solves min_x ||b - J x||_2 for a tall LinearOperator J using a precomputed -// triangular preconditioner R (e.g., the R-factor from CQRRT_linops on J or -// on a sketch SJ). R is treated as a right preconditioner on the normal -// equations, and two iterative-refinement steps are performed; under standard -// hypotheses two steps suffice for backward stability. The inner solver is -// CG on the symmetric-positive-definite preconditioned normal-equation matrix +// Solves min_x ||b - J x||_2 (or, with `lambda > 0`, +// min_x ||b - J x||_2^2 + lambda^2 ||x||_2^2) +// for a tall LinearOperator J using a precomputed triangular preconditioner R +// (e.g., the R-factor from CQRRT_linops on J or on a sketch SJ). R is treated +// as a right preconditioner on the normal equations, and two iterative- +// refinement steps are performed; under standard hypotheses two steps suffice +// for backward stability. The inner solver is CG on the symmetric-positive- +// definite preconditioned normal-equation matrix // -// M = R^{-T} J^T J R^{-1}. +// M = R^{-T} J^T J R^{-1} (lambda = 0) +// M_reg = R^{-T} (J^T J + lambda^2 I) R^{-1} (lambda > 0) // // Reference: E. N. Epperly, M. Meier, and Y. Nakatsukasa, // "Fast randomized least-squares solvers can be just as accurate and stable diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc index 85964b52e..66be91a42 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc @@ -6,17 +6,21 @@ // (A1)_{l,k} = 1 - 2 * exp(-tau1[l] / T1[k]) (m1 × n1) // (A2)_{l,k} = exp(-tau2[l] / T2[k]) (m2 × n2) // -// Time grids are logspace(taulogleft, taulogright). Default ranges follow -// the paper (T, tau ∈ [10^{-4}, 10^1]). Operator dimensions: +// Time grids are logspace(log_lo, log_hi). Default range here is (-2, 1) = +// 3 decades; the IR Tools paper uses (-4, 1) = 5 decades, but unregularized +// Q-less QR cannot handle that conditioning even with stabilized +// preconditioners. Operator dimensions: // M = m1*m2 (rows), N = n1*n2 (cols), default m_i = 2*n_i. // // Pipeline: // 1. Generate A1, A2, phantom x_true ∈ ℝ^{n1*n2}, b = A x_true + noise. -// 2. Wrap A as KroneckerOperator (no materialization). -// 3. Run CQRRT_linops on A → R (n × n upper triangular). -// 4. Run IterRefineLSQ(A, R, b) → x. -// 5. Report ||x - x_true||/||x_true||, ||b - Ax||/||b||, IR iter counts, -// timing, and analytical-memory predictions. +// 2. Wrap A as KroneckerOperator (no materialization). When λ > 0, also +// wrap that in RegularizedLinOp so the QR step runs on A_aug = [J; λI]. +// 3. For each Q-less QR variant selected by `method_mask`, run it on the +// operator → R (n × n upper triangular). +// 4. For each (algorithm, run): run IterRefineLSQ(A_eff, R, b_eff) → x. +// 5. Record per-(algorithm, run) ||x - x_true||/||x_true||, ||b - Ax||/||b||, +// IR iter counts, timing, and analytical-memory predictions. // // Usage: // ./CQRRT_linop_nmr @@ -376,18 +380,29 @@ int run_benchmark(int argc, char* argv[]) auto state = run_states[r]; RandLAPACK::PeakRSSTracker mem; mem.start(); - if (alg_name == "sCholQR3" || alg_name == "sCholQR3_basic") { - // Both sCholQR3 variants share the same call signature; we use the - // fully-blocked one. (sCholQR3_basic has identical numerics here.) + if (alg_name == "sCholQR3") { + // Fully-blocked shifted Cholesky-QR-3. Times has 18 entries; total at [17]. RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); qr_algo.block_size = block_size; res.qr_status = qr_algo.call(J_eff, R.data(), N); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[17]; + // First 11 of 18 entries are kept; full breakdown table is elsewhere. res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m_eff, N, block_size); } + } else if (alg_name == "sCholQR3_basic") { + // Non-blocked variant (matches the textbook sCholQR3 pseudocode). + // Times has 15 entries; total at [14]. + RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); + res.qr_status = qr_algo.call(J_eff, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[14]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m_eff, N); + } } else if (alg_name == "CholQR") { RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); qr_algo.block_size = block_size; diff --git a/test/drivers/test_iter_refine_lsq.cc b/test/drivers/test_iter_refine_lsq.cc index 575854adb..06090f573 100644 --- a/test/drivers/test_iter_refine_lsq.cc +++ b/test/drivers/test_iter_refine_lsq.cc @@ -3,8 +3,10 @@ // // Strategy: wrap a small dense tall A as a DenseLinOp, build R from // lapack::geqrf on a copy of A, then verify the IR-LSQ solution matches -// the gels reference within O(eps) on a synthetic well-conditioned LS -// problem. +// a closed-form reference: +// * unregularized cases → lapack::gels on (A, b) +// * Tikhonov case → lapack::posv on (A^T A + lambda^2 I, A^T b) +// across well-conditioned, residualful, regularized, and imperfect-R cases. #include #include From ad004195251c4423fa79ba2907ae435b9fc16a9c Mon Sep 17 00:00:00 2001 From: mmelnich Date: Fri, 15 May 2026 15:33:27 -0400 Subject: [PATCH 16/47] Fix TRSM direction when forming R_sk^{-1} from identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When solving for R_sk^{-1} explicitly to precondition a linop, solve R_sk * X = I (Side::Left) rather than X * R_sk = I (Side::Right). The two are mathematically equivalent but the latter exhibits poor backward error in the subsequent product A * R_sk^{-1} when R_sk is ill-conditioned. On photogrammetry2 (κ(R_sk) ≈ 2.2e8) this changes orth_err in CQRRT_linop from 1.6e-4 to 1.5e-9 -- matching the GEQP3/BQRRP stabilized variants and obviating their need as the default path. Same fix applied to the initial M = R_1^{-1} formation in sCholQR3_linops. Slim CQRRT_linop_applications to run only the patched CQRRT_linop; swap GETRI for BQRRP in CQRRT_diagnostic to show the stabilized counterpart alongside trsm/trtri. --- RandLAPACK/drivers/rl_cqrrt_linops.hh | 8 +- RandLAPACK/drivers/rl_scholqr3_linops.hh | 8 +- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 53 ++++- .../CQRRT_linop_applications.cc | 197 +++--------------- 4 files changed, 82 insertions(+), 184 deletions(-) diff --git a/RandLAPACK/drivers/rl_cqrrt_linops.hh b/RandLAPACK/drivers/rl_cqrrt_linops.hh index 69402ccce..d84a63b8c 100644 --- a/RandLAPACK/drivers/rl_cqrrt_linops.hh +++ b/RandLAPACK/drivers/rl_cqrrt_linops.hh @@ -261,7 +261,11 @@ class CQRRT_linops { T* R_sk_inv = nullptr; if (this->precond_method == CQRRTLinopPrecond::TRSM_IDENTITY) { - // Original: TRSM(I, R_sk) → R_sk^{-1} (upper triangular result) + // Solve R_sk * R_inv = I via Side::Left TRSM (column-by-column + // backward stable). Forming R_sk^{-1} by Side::Right TRSM + // (X * R_sk = I) is mathematically equivalent but exhibits + // significantly worse backward error in the subsequent + // product A * R_sk^{-1} when R_sk is ill-conditioned. T* Eye = new T[n * n](); RandLAPACK::util::eye(n, n, Eye); if (!RandLAPACK::util::diag_is_nonzero(n, A_hat, d)) { @@ -270,7 +274,7 @@ class CQRRT_linops { delete[] Eye; return 1; } - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, A_hat, d, Eye, n); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, A_hat, d, Eye, n); if (n > 1) { lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &Eye[1], n); } diff --git a/RandLAPACK/drivers/rl_scholqr3_linops.hh b/RandLAPACK/drivers/rl_scholqr3_linops.hh index abbef9ad5..131187bcd 100644 --- a/RandLAPACK/drivers/rl_scholqr3_linops.hh +++ b/RandLAPACK/drivers/rl_scholqr3_linops.hh @@ -237,8 +237,12 @@ class sCholQR3_linops { t_start = steady_clock::now(); } - // Update M: M = I * R1^{-1} = R1^{-1} - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + // Initialize M = R1^{-1} by solving R1 * M = I via Side::Left TRSM + // (column-by-column backward stable). Using Side::Right (X * R1 = I) + // is mathematically equivalent but produces an M whose subsequent + // product A * M has significantly worse backward error when R1 is + // ill-conditioned -- see rl_cqrrt_linops.hh for the same pattern. + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, G, n, M, n); if(this->timing) { diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index ea43db81b..f911da60b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -10,7 +10,7 @@ // R_inv = P * TRSM(R_buf, Q^T); DGEMM(A, R_inv) // [5] expl_inv_svd: GESDD(R_sk) = U*S*Vt; // R_inv = V * diag(1/S) * U^T; DGEMM(A, R_inv) -// [6] expl_inv_getri: GETRF(R_sk) then GETRI; DGEMM(A, R_inv) +// [6] expl_inv_bqrrp: BQRRP(R_sk)=Q*R_buf*P^T; R_inv=P*TRSM(R_buf,Q^T); DGEMM(A, R_inv) // // Path [1] never forms R_sk^{-1} explicitly (backward stable). // Paths [2]-[6] all form R_sk^{-1} explicitly via different methods. @@ -97,7 +97,7 @@ static constexpr const char* PATH_NAMES[N_PATHS] = { "expl_inv_trtri", "expl_inv_geqp3", "expl_inv_svd", - "expl_inv_getri", + "expl_inv_bqrrp", }; static constexpr const char* PATH_DESCS[N_PATHS] = { @@ -106,7 +106,7 @@ static constexpr const char* PATH_DESCS[N_PATHS] = { "TRTRI(R_sk)->R_inv; DGEMM(A, R_inv)", "GEQP3(R_sk)=Q*R_buf*P^T; R_inv=P*TRSM(R_buf,Q^T); DGEMM(A, R_inv)", "GESDD(R_sk)=U*S*Vt; R_inv=V*diag(1/S)*U^T; DGEMM(A, R_inv)", - "GETRF+GETRI(R_sk)->R_inv; DGEMM(A, R_inv)", + "BQRRP(R_sk)=Q*R_buf*P^T; R_inv=P*TRSM(R_buf,Q^T); DGEMM(A, R_inv)", }; // ============================================================================ @@ -194,7 +194,7 @@ struct TrialResult { double rd_Apre_13; // TRSM in-place vs trtri double rd_Apre_14; // TRSM in-place vs geqp3 double rd_Apre_15; // TRSM in-place vs svd - double rd_Apre_16; // TRSM in-place vs getri + double rd_Apre_16; // TRSM in-place vs bqrrp // Step-by-step pipeline divergence: paths [1] vs [2], faithful to actual implementations // Path [1] (CQRRT_expl): sketch via sketch_general(S, A_dense); TRSM in-place; SYRK // Path [2] (CQRRT_linop): sketch via A_linop(Side::Right, S) [SpGEMM]; @@ -317,12 +317,43 @@ static TrialResult run_trial( (T)0.0, R_inv_svd.data(), n); } - // Method E: GETRF + GETRI (general LU with partial pivoting) (path [6]) - std::vector R_inv_getri(R_sk.begin(), R_sk.end()); + // Method E: BQRRP factorization of R_sk (path [6]) + // Same output format as GEQP3: R_sk * P = Q_buf * R_buf, then + // R_sk^{-1} = P * R_buf^{-1} * Q_buf^T. + // BQRRP uses blocked randomized QRCP; block size matches CQRRT_linops + // adaptive heuristic (1.0 for n <= 2000, 0.5 for n <= 8000, 1/32 else). + std::vector R_inv_bqrrp(n * n, 0.0); { - std::vector ipiv(n); - lapack::getrf(n, n, R_inv_getri.data(), n, ipiv.data()); - lapack::getri(n, R_inv_getri.data(), n, ipiv.data()); + std::vector R_copy(R_sk.begin(), R_sk.end()); + std::vector jpiv(n, 0); + std::vector tau_qr(n); + + T block_ratio; + if (n <= 2000) block_ratio = (T)1.0; + else if (n <= 8000) block_ratio = (T)0.5; + else block_ratio = (T)1.0 / (T)32; + int64_t bqrrp_block = std::max(1, (int64_t)(n * block_ratio)); + RandLAPACK::BQRRP bqrrp(false, bqrrp_block); + bqrrp.call(n, n, R_copy.data(), n, (T)1.0, tau_qr.data(), jpiv.data(), state); + + std::vector R_buf(n * n, 0.0); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_buf[i + j*n] = R_copy[i + j*n]; + + lapack::ungqr(n, n, n, R_copy.data(), n, tau_qr.data()); + + std::vector W(n * n, 0.0); + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + W[i + j*n] = R_copy[j + i*n]; // W := Q_buf^T (col-major) + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, (T)1.0, + R_buf.data(), n, W.data(), n); + + for (int64_t k = 0; k < n; ++k) + for (int64_t j = 0; j < n; ++j) + R_inv_bqrrp[(jpiv[k]-1) + j*n] = W[k + j*n]; } // ---------------------------------------------------------------- @@ -341,7 +372,7 @@ static TrialResult run_trial( const T* R_invs[N_PATHS - 1] = { R_inv_trsm.data(), R_inv_trtri.data(), R_inv_geqp3.data(), - R_inv_svd.data(), R_inv_getri.data() + R_inv_svd.data(), R_inv_bqrrp.data() }; for (int p = 1; p < N_PATHS; ++p) blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, @@ -542,7 +573,7 @@ static void write_csv_and_run( printf(" rd_13 (trtri): %12.3e\n", res.rd_Apre_13); printf(" rd_14 (geqp3): %12.3e\n", res.rd_Apre_14); printf(" rd_15 (svd): %12.3e\n", res.rd_Apre_15); - printf(" rd_16 (getri): %12.3e\n", res.rd_Apre_16); + printf(" rd_16 (bqrrp): %12.3e\n", res.rd_Apre_16); printf(" run %ld step-by-step divergence [1] vs [2] (expl: sketch_general; linop: SpGEMM):\n", r); printf(" M^sk: %12.3e\n", res.rd_Msk_12); diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 91f7d6560..582935ad4 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -5,24 +5,14 @@ // OR a single A.mtx as a SparseLinOp (sparse mode) // 2. (Composite only) Cholesky-factorize K = LL^T, create L^{-1} operator // 3. (Composite only) Form CompositeOperator(L_inv_op, V_op) = L^{-1}V -// 4. Run Q-less QR on the operator via CQRRT_linops, CholQR_linops, sCholQR3_linops +// 4. Run Q-less QR on the operator via CQRRT_linops (TRSM_IDENTITY preconditioner) // 5. SVD post-processing: gesdd of R for generalized singular values + vectors // // (For LS post-processing on Kronecker-structured operators see CQRRT_linop_nmr.) // // Usage: // ./CQRRT_linop_applications -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl] -// [upcast_orth] [method_mask] -// -// method_mask (integer bitmask, optional, default = 0b001111 or 0b011111 if run_expl=1): -// bit 0 ( 1): CQRRT_linop -// bit 1 ( 2): CholQR -// bit 2 ( 4): sCholQR3 -// bit 3 ( 8): sCholQR3_basic -// bit 4 ( 16): CQRRT_expl (requires A materialization) -// bit 5 ( 32): CQRRT_linop_stb (GEQP3-stabilized preconditioner) -// bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner) +// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] #include "RandLAPACK.hh" #include "rl_blaspp.hh" @@ -370,14 +360,10 @@ static void write_breakdown_csv( write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); out << "# Times are in microseconds\n"; out << "# CQRRT_linop breakdown (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n"; - out << "# CholQR breakdown (6): alloc, fwd, adj, chol, rest, total\n"; - out << "# sCholQR3 breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total\n"; - out << "# sCholQR3_basic breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total\n"; // Column header: m, n, run, algorithm, then all breakdown times - // Max breakdown length is 18 (sCholQR3) out << "m,n,run,algorithm"; - for (int i = 0; i < 18; ++i) out << ",t" << i; + for (int i = 0; i < 11; ++i) out << ",t" << i; out << "\n"; for (const auto& r : results) { @@ -385,8 +371,8 @@ static void write_breakdown_csv( for (size_t i = 0; i < r.qr_breakdown.size(); ++i) { out << "," << r.qr_breakdown[i]; } - // Pad with zeros if fewer than 18 columns - for (size_t i = r.qr_breakdown.size(); i < 18; ++i) { + // Pad with zeros if fewer than 11 columns + for (size_t i = r.qr_breakdown.size(); i < 11; ++i) { out << ",0"; } out << "\n"; @@ -425,7 +411,7 @@ static int run_benchmark_inner( const std::string& output_dir, int64_t num_runs, T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond, bool upcast_orth, int64_t method_mask, + bool skip_apps, bool compute_cond, bool upcast_orth, long chol_time_us, const std::string& K_file, const std::string& V_file, const std::string& op_label) @@ -470,8 +456,8 @@ static int run_benchmark_inner( // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) // ================================================================ T* A_materialized = nullptr; - if (upcast_orth || (method_mask & 16)) { - std::cout << "Materializing " << op_label << " for upcast/expl (" << m << " x " << n << ", " + if (upcast_orth) { + std::cout << "Materializing " << op_label << " for upcast (" << m << " x " << n << ", " << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; A_materialized = new T[m * n]; auto Eye = make_eye(n); @@ -570,128 +556,21 @@ static int run_benchmark_inner( }; // ================================================================ - // CQRRT_linop variants (standard TRSM_IDENTITY and GEQP3-stabilized) - // ================================================================ - auto run_cqrrt_linop = [&](const std::string& name, RandLAPACK::CQRRTLinopPrecond precond, long analytical_kb_override = -1L) { - run_algo(name, [&, precond, analytical_kb_override](gsvd_result& res, std::vector& R, int64_t r) { - auto state = run_states[r]; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; algo.block_size = block_size; - algo.precond_method = precond; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[10]; - // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = (analytical_kb_override >= 0) - ? analytical_kb_override - : RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); - }); - }; - if (method_mask & 1) - run_cqrrt_linop("CQRRT_linop", RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY); - - // ================================================================ - // CholQR - // ================================================================ - if (method_mask & 2) { - run_algo("CholQR", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::CholQR_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[5]; - // breakdown: alloc, fwd, adj, chol, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 6); - res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); - }); - } - - // ================================================================ - // sCholQR3 - // ================================================================ - if (method_mask & 4) { - run_algo("sCholQR3", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops algo(true, tol, false); - algo.block_size = block_size; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[17]; - // breakdown (18): alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3, adj3, gemm3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 18); - res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); - }); - } - - // ================================================================ - // sCholQR3_basic (non-blocked, matches standard pseudocode) - // ================================================================ - if (method_mask & 8) { - run_algo("sCholQR3_basic", [&](gsvd_result& res, std::vector& R, int64_t) { - RandLAPACK::sCholQR3_linops_basic algo(true, tol, false); - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[14]; - // breakdown (15): alloc, fwd1, adj1, chol1, trsm1, fwd_q, syrk2, chol2, upd2, syrk3, chol3, upd3, q_mat, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 15); - res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); - }); - } - - // ================================================================ - // CQRRT_expl (uses pre-materialized A, run dense CQRRT) - // ================================================================ - if (method_mask & 16) { - run_algo("CQRRT_expl", [&](gsvd_result& res, std::vector& R, int64_t r) { - // Time the copy: CQRRT modifies A in-place, so a fresh copy is needed each run. - // This per-run materialization cost is not captured by algo.times — store in slot [2]. - T* A_copy = new T[m * n]; - auto copy_start = std::chrono::steady_clock::now(); - lapack::lacpy(lapack::MatrixType::General, m, n, A_materialized, m, A_copy, m); - auto copy_end = std::chrono::steady_clock::now(); - long copy_dur = std::chrono::duration_cast(copy_end - copy_start).count(); - - auto state = run_states[r]; - RandLAPACK::CQRRT algo(true, tol); - algo.compute_Q = false; - algo.orthogonalization = false; - algo.nnz = sketch_nnz; - - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(m, n, A_copy, m, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - - // CQRRT dense times layout (10 entries, 0-indexed): - // [0]=saso, [1]=qr, [2]=trtri(0), [3]=precond, [4]=gram, - // [5]=trmm_gram(0), [6]=potrf, [7]=finalize, [8]=rest, [9]=total - // Store copy_dur in slot [2] (repurposed trtri placeholder) and include in total. - res.qr_breakdown.assign(algo.times.begin(), algo.times.end()); - while (res.qr_breakdown.size() < 11) res.qr_breakdown.push_back(0); - res.qr_breakdown[2] = copy_dur; - res.qr_breakdown[9] += copy_dur; // update total to include copy - res.qr_time_us = res.qr_breakdown[9]; - res.analytical_kb = (m * n * sizeof(T)) / 1024; // just the dense matrix - - delete[] A_copy; - }); - } - - // ================================================================ - // CQRRT_linop_stb (GEQP3-stabilized preconditioner, uses pre-materialized A for orth check) - // ================================================================ - if (method_mask & 32) - run_cqrrt_linop("CQRRT_linop_stb", RandLAPACK::CQRRTLinopPrecond::GEQP3); - - // ================================================================ - // CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner) + // CQRRT_linop (TRSM_IDENTITY preconditioner) // ================================================================ - if (method_mask & 64) - run_cqrrt_linop("CQRRT_linop_stb_bqrrp", RandLAPACK::CQRRTLinopPrecond::BQRRP, - RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(n, d_factor)); + run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { + auto state = run_states[r]; + RandLAPACK::CQRRT_linops algo(true, tol, false); + algo.nnz = sketch_nnz; algo.block_size = block_size; + algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; + RandLAPACK::PeakRSSTracker mem; mem.start(); + algo.call(A_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + res.qr_time_us = algo.times[10]; + // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total + res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + }); // Free pre-materialized A delete[] A_materialized; @@ -730,17 +609,8 @@ int run_benchmark(int argc, char* argv[]) { if (argc < 7) { std::cerr << "Usage: " << argv[0] << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl]" - << " [upcast_orth] [method_mask]\n" - << " sparse mode: pass 'sparse' as K_file and a single A.mtx as V_file\n" - << " method_mask: bitmask selecting algorithms (default = 0b001111 or 0b011111 if run_expl=1)\n" - << " bit 0 ( 1): CQRRT_linop\n" - << " bit 1 ( 2): CholQR\n" - << " bit 2 ( 4): sCholQR3\n" - << " bit 3 ( 8): sCholQR3_basic\n" - << " bit 4 ( 16): CQRRT_expl (requires A materialization)\n" - << " bit 5 ( 32): CQRRT_linop_stb (GEQP3-stabilized preconditioner)\n" - << " bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP-stabilized preconditioner)\n"; + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" + << " sparse mode: pass 'sparse' as K_file and a single A.mtx as V_file\n"; return 1; } @@ -753,12 +623,7 @@ int run_benchmark(int argc, char* argv[]) { int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; - bool run_expl = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; - bool upcast_orth = (argc >= 13) ? (std::stol(argv[12]) != 0) : false; - // method_mask: bitmask selecting which algorithms to run. - // Default: bits 0-3 (all linop methods), plus bit 4 if run_expl=1 (backward compat). - // Explicit method_mask overrides run_expl for algorithm selection. - int64_t method_mask = (argc >= 14) ? std::stol(argv[13]) : (0b001111 | (run_expl ? 0b010000 : 0)); + bool upcast_orth = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; bool sparse_mode = (K_file == "sparse"); @@ -776,12 +641,7 @@ int run_benchmark(int argc, char* argv[]) { std::cout << " block_size: " << block_size << "\n"; std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; - std::cout << " run_expl: " << (run_expl ? "yes" : "no") << " (backward compat; overridden by method_mask)\n"; std::cout << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n"; - std::cout << " method_mask: " << method_mask << " (bits: linop=" << (method_mask&1) - << " CholQR=" << ((method_mask>>1)&1) << " sCholQR3=" << ((method_mask>>2)&1) - << " sCholQR3_basic=" << ((method_mask>>3)&1) << " expl=" << ((method_mask>>4)&1) - << " linop_stb=" << ((method_mask>>5)&1) << " linop_stb_bqrrp=" << ((method_mask>>6)&1) << ")\n"; std::cout << " num_runs: " << num_runs << "\n"; #ifdef _OPENMP std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; @@ -810,7 +670,7 @@ int run_benchmark(int argc, char* argv[]) { return run_benchmark_inner( V_linop, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, - skip_apps, compute_cond, upcast_orth, method_mask, + skip_apps, compute_cond, upcast_orth, 0L /*chol_time_us*/, "sparse", V_file, "A (" + V_file + ")"); } @@ -834,7 +694,7 @@ int run_benchmark(int argc, char* argv[]) { return run_benchmark_inner( LiV_op, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, - skip_apps, compute_cond, upcast_orth, method_mask, + skip_apps, compute_cond, upcast_orth, chol_time_us, K_file, V_file, "L^{-1}V"); } @@ -843,8 +703,7 @@ int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [run_expl]" - << " [upcast_orth] [method_mask]\n"; + << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; return 1; } From 8acd6b3009ab91b466cfd8fd096016194c469d30 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Fri, 22 May 2026 11:16:43 -0400 Subject: [PATCH 17/47] IR-LSQ refactor: sparse-input benchmark, fix breakdown + analytical_kb bugs - Replace NMR/Kronecker benchmark with CQRRT_linop_irlsq.cc. Loads a tall sparse .mtx, wraps as SparseLinOp, generates synthetic x_true + b = J*x_true + noise. For each Q-less QR variant: draws a fresh sparse sketch S2 (independent of CQRRT's S1), forms x_0 = R^{-1} R^{-T} (S2 A)^T (S2 b) (paper Algorithm 1, line 3, Q-less form), then runs 2-step IR with inner CG. - Strip Tikhonov from IterRefineLSQ. Drop KroneckerOperator, RegularizedLinOp, and their tests/includes/CMake entries. Drop the GEQP3-stabilized variant from the benchmark and rename CQRRT_linop_stb_bqrrp -> CQRRT_linop_bqrrp. - Fix IR-LSQ populate_times double-counting bug. Inner CG's TRSM/fwd/adj contributions were tracked into both t_inner_total (wallclock of the inner_cg call) and the shared t_{trsm,fwd,adj}_total counters, so 'other = outer_total - inner - trsm - fwd - adj' went negative. Split into outer-only and inner-only counters and report inner_ex = t_inner_total - t_inner_{trsm,fwd,adj}, total_trsm = t_outer_trsm + t_inner_trsm (similarly fwd, adj). 'other' is now a non-negative residue (axpy/copy/nrm2 bookkeeping). - Fix three analytical_kb formulas in rl_memory_tracker.hh. cqrrt_linops_bqrrp_analytical_kb captured only the BQRRP-precond moment (d*n + 4n^2) and missed the later Gram-loop moment (d*n + n + n^2 + m*b_eff). For tall inputs the latter dominates. Now returns the max of the two; signature extended to (m, n, d_factor, block_size). scholqr3_linops_analytical_kb forgot G3_factor at the iter-3 peak (5n^2 -> 6n^2). scholqr3_linops_basic_analytical_kb forgot all three G_i_factor members (3n^2 -> 6n^2). --- RandLAPACK/drivers/rl_iter_refine_lsq.hh | 111 ++-- RandLAPACK/linops/rl_kronecker_linop.hh | 334 ---------- RandLAPACK/linops/rl_linops.hh | 2 - RandLAPACK/linops/rl_regularized_linop.hh | 269 -------- RandLAPACK/testing/rl_memory_tracker.hh | 34 +- benchmark/CMakeLists.txt | 4 +- .../bench_CQRRT_linops/CQRRT_linop_irlsq.cc | 467 ++++++++++++++ .../bench_CQRRT_linops/CQRRT_linop_nmr.cc | 599 ------------------ test/CMakeLists.txt | 2 - test/drivers/test_iter_refine_lsq.cc | 51 +- test/linops/test_kronecker_linop.cc | 185 ------ test/linops/test_regularized_linop.cc | 221 ------- 12 files changed, 547 insertions(+), 1732 deletions(-) delete mode 100644 RandLAPACK/linops/rl_kronecker_linop.hh delete mode 100644 RandLAPACK/linops/rl_regularized_linop.hh create mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc delete mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc delete mode 100644 test/linops/test_kronecker_linop.cc delete mode 100644 test/linops/test_regularized_linop.cc diff --git a/RandLAPACK/drivers/rl_iter_refine_lsq.hh b/RandLAPACK/drivers/rl_iter_refine_lsq.hh index 18d50d6fd..c09b8ec01 100644 --- a/RandLAPACK/drivers/rl_iter_refine_lsq.hh +++ b/RandLAPACK/drivers/rl_iter_refine_lsq.hh @@ -1,19 +1,16 @@ #pragma once // Public API: IterRefineLSQ — Q-less, sketch-and-precondition iterative-refinement -// least-squares solver, with optional Tikhonov. +// least-squares solver. // -// Solves min_x ||b - J x||_2 (or, with `lambda > 0`, -// min_x ||b - J x||_2^2 + lambda^2 ||x||_2^2) -// for a tall LinearOperator J using a precomputed triangular preconditioner R -// (e.g., the R-factor from CQRRT_linops on J or on a sketch SJ). R is treated -// as a right preconditioner on the normal equations, and two iterative- -// refinement steps are performed; under standard hypotheses two steps suffice -// for backward stability. The inner solver is CG on the symmetric-positive- -// definite preconditioned normal-equation matrix +// Solves min_x ||b - J x||_2 for a tall LinearOperator J using a precomputed +// triangular preconditioner R (e.g., the R-factor from CQRRT_linops on J or on +// a sketch SJ). R is treated as a right preconditioner on the normal equations, +// and two iterative-refinement steps are performed; under standard hypotheses +// two steps suffice for backward stability. The inner solver is CG on the +// symmetric-positive-definite preconditioned normal-equation matrix // -// M = R^{-T} J^T J R^{-1} (lambda = 0) -// M_reg = R^{-T} (J^T J + lambda^2 I) R^{-1} (lambda > 0) +// M = R^{-T} J^T J R^{-1}. // // Reference: E. N. Epperly, M. Meier, and Y. Nakatsukasa, // "Fast randomized least-squares solvers can be just as accurate and stable @@ -42,27 +39,20 @@ namespace RandLAPACK { /// @brief Iterative-refinement least-squares solver with right preconditioner R. /// -/// Solves min_x ||b - J x||_2 (or, with `lambda > 0`, the Tikhonov-regularized -/// problem min_x ||b - J x||_2^2 + lambda^2 ||x||_2^2) by performing -/// `n_refine_steps` outer steps of +/// Solves min_x ||b - J x||_2 by performing `n_refine_steps` outer steps of /// /// r_i ← b - J x_i -/// g_i ← J^T r_i − lambda^2 x_i (regularized gradient; lambda=0 → unregularized) +/// g_i ← J^T r_i /// c_i ← R^{-T} g_i -/// z_i ← inner_solve(M_reg, c_i) with M_reg = R^{-T} (J^T J + lambda^2 I) R^{-1} +/// z_i ← inner_solve(M, c_i) with M = R^{-T} J^T J R^{-1} /// x_{i+1} ← x_i + R^{-1} z_i /// /// where R is upper triangular (n × n, ColMajor) and is held constant. The /// inner solver is preconditioner-free conjugate gradients on the SPD matrix -/// M_reg; each inner matvec costs two TRSMs, one J apply (forward), one J^T apply -/// (adjoint), plus a single axpy when `lambda > 0`. The default -/// `n_refine_steps = 2` and inner CG stopping rule (relative residual -/// `< inner_tol`) suffice for backward stability under the conditions of -/// Epperly et al. (2025) Theorem 6.1. -/// -/// Tikhonov is useful for ill-posed inverse problems (e.g., NMR relaxometry) -/// where J has tiny singular values and the unregularized normal-equation -/// matrix loses positive definiteness in finite precision. +/// M; each inner matvec costs two TRSMs, one J apply (forward), and one J^T +/// apply (adjoint). The default `n_refine_steps = 2` and inner CG stopping +/// rule (relative residual `< inner_tol`) suffice for backward stability +/// under the conditions of Epperly et al. (2025) Theorem 6.1. template struct IterRefineLSQ { // ------------- Configuration ------------- @@ -72,9 +62,6 @@ struct IterRefineLSQ { int max_inner_iters; /// Outer refinement steps (Algorithm 1 of Epperly et al. uses 2). int n_refine_steps; - /// Tikhonov regularization parameter. Solves min ||Jx-b||² + lambda²||x||². - /// Set to 0 (default) for unregularized LS. - T lambda; /// Enable per-step / per-substep timing breakdown. bool timing; /// Print convergence info to stdout. @@ -96,12 +83,10 @@ struct IterRefineLSQ { int max_inner = 200, int n_steps = 2, bool timing_on = false, - bool verbose_on = false, - T lambda_in = (T)0) + bool verbose_on = false) : inner_tol(tol), max_inner_iters(max_inner), n_refine_steps(n_steps), - lambda(lambda_in), timing(timing_on), verbose(verbose_on), outer_iters_done(0), @@ -129,7 +114,15 @@ struct IterRefineLSQ { using clock = std::chrono::steady_clock; auto outer_start = clock::now(); - long t_inner_total = 0, t_trsm_total = 0, t_fwd_total = 0, t_adj_total = 0; + // Separate outer-only (excluding inner CG) and inner-CG-only counters so + // we can report a non-overlapping breakdown. The total wallclock spent + // inside inner_cg() is tracked via t_inner_total; the per-op time + // inside inner_cg is captured by the t_inner_* counters and would + // otherwise be triple-counted (once in t_inner_total, once in the + // outer totals) if we shared counters. + long t_inner_total = 0; + long t_outer_trsm = 0, t_outer_fwd = 0, t_outer_adj = 0; + long t_inner_trsm = 0, t_inner_fwd = 0, t_inner_adj = 0; inner_iters_per_step.clear(); outer_iters_done = 0; @@ -154,7 +147,7 @@ struct IterRefineLSQ { auto t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); - t_fwd_total += std::chrono::duration_cast(clock::now() - t0).count(); + t_outer_fwd += std::chrono::duration_cast(clock::now() - t0).count(); // r = b - tmp_m for (int64_t i = 0; i < m; ++i) r[i] = b[i] - tmp_m[i]; @@ -164,15 +157,11 @@ struct IterRefineLSQ { std::printf("[IR-LSQ] step %d: ||r||/||b|| = %.4e\n", step, (double)(r_norm / b_norm)); } - // g = J^T r (regularized gradient: g = J^T r - lambda^2 x) + // g = J^T r t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, n, 1, m, (T)1.0, r.data(), m, (T)0.0, g.data(), n); - t_adj_total += std::chrono::duration_cast(clock::now() - t0).count(); - if (lambda != (T)0) { - T lambda_sq = lambda * lambda; - blas::axpy(n, -lambda_sq, x, 1, g.data(), 1); - } + t_outer_adj += std::chrono::duration_cast(clock::now() - t0).count(); // c = R^{-T} g (in-place TRSM on a copy of g) std::copy(g.begin(), g.end(), c.begin()); @@ -180,7 +169,7 @@ struct IterRefineLSQ { blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, c.data(), n); - t_trsm_total += std::chrono::duration_cast(clock::now() - t0).count(); + t_outer_trsm += std::chrono::duration_cast(clock::now() - t0).count(); // Inner CG on M*z = c int inner_iters = 0; @@ -189,14 +178,15 @@ struct IterRefineLSQ { z.data(), cg_r.data(), cg_p.data(), cg_Mp.data(), tmp_n.data(), tmp_m.data(), - inner_iters, t_trsm_total, t_fwd_total, t_adj_total); + inner_iters, t_inner_trsm, t_inner_fwd, t_inner_adj); t_inner_total += std::chrono::duration_cast(clock::now() - t_in0).count(); inner_iters_per_step.push_back(inner_iters); if (cg_status != 0) { outer_iters_done = step; final_residual_norm = r_norm / b_norm; - if (timing) populate_times(outer_start, t_inner_total, t_trsm_total, - t_fwd_total, t_adj_total); + if (timing) populate_times(outer_start, t_inner_total, + t_outer_trsm, t_outer_fwd, t_outer_adj, + t_inner_trsm, t_inner_fwd, t_inner_adj); return cg_status; } @@ -206,7 +196,7 @@ struct IterRefineLSQ { blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, n, 1, (T)1.0, R, ldr, dx.data(), n); - t_trsm_total += std::chrono::duration_cast(clock::now() - t0).count(); + t_outer_trsm += std::chrono::duration_cast(clock::now() - t0).count(); // x ← x + dx blas::axpy(n, (T)1.0, dx.data(), 1, x, 1); @@ -218,13 +208,14 @@ struct IterRefineLSQ { auto t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); - t_fwd_total += std::chrono::duration_cast(clock::now() - t0).count(); + t_outer_fwd += std::chrono::duration_cast(clock::now() - t0).count(); for (int64_t i = 0; i < m; ++i) tmp_m[i] = b[i] - tmp_m[i]; final_residual_norm = blas::nrm2(m, tmp_m.data(), 1) / b_norm; } - if (timing) populate_times(outer_start, t_inner_total, t_trsm_total, - t_fwd_total, t_adj_total); + if (timing) populate_times(outer_start, t_inner_total, + t_outer_trsm, t_outer_fwd, t_outer_adj, + t_inner_trsm, t_inner_fwd, t_inner_adj); return 0; } @@ -280,14 +271,6 @@ private: n, 1, m, (T)1.0, tmp_m, m, (T)0.0, cg_Mp, n); t_adj += std::chrono::duration_cast(clock::now() - t0).count(); - // Add Tikhonov term: M_reg v = R^{-T} (J^T J + lambda^2 I) R^{-1} v. - // We have JtJv ≡ cg_Mp = J^T J R^{-1} v, and tmp_n ≡ R^{-1} v. - // So (J^T J + lambda^2 I) R^{-1} v = JtJv + lambda^2 * tmp_n. - if (lambda != (T)0) { - T lambda_sq = lambda * lambda; - blas::axpy(n, lambda_sq, tmp_n, 1, cg_Mp, 1); - } - // cg_Mp ← R^{-T} cg_Mp (in-place TRSM) t0 = clock::now(); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, @@ -329,13 +312,25 @@ private: } void populate_times(std::chrono::steady_clock::time_point outer_start, - long inner, long trsm, long fwd, long adj) + long t_inner_total, + long t_outer_trsm, long t_outer_fwd, long t_outer_adj, + long t_inner_trsm, long t_inner_fwd, long t_inner_adj) { using clock = std::chrono::steady_clock; long outer_total = std::chrono::duration_cast( clock::now() - outer_start).count(); - long other = outer_total - inner - trsm - fwd - adj; - times = { outer_total, inner, trsm, fwd, adj, other }; + // Non-overlapping breakdown of outer_total: + // inner_ex = inner CG wallclock minus its TRSM/fwd/adj portions + // total_trsm = outer-loop TRSMs + inner-CG TRSMs + // total_fwd = outer-loop J·v + inner-CG J·v + // total_adj = outer-loop J^T·v + inner-CG J^T·v + // other = residual setup, axpy/copy/nrm2 bookkeeping, clock overhead + long inner_ex = t_inner_total - t_inner_trsm - t_inner_fwd - t_inner_adj; + long total_trsm = t_outer_trsm + t_inner_trsm; + long total_fwd = t_outer_fwd + t_inner_fwd; + long total_adj = t_outer_adj + t_inner_adj; + long other = outer_total - inner_ex - total_trsm - total_fwd - total_adj; + times = { outer_total, inner_ex, total_trsm, total_fwd, total_adj, other }; } }; diff --git a/RandLAPACK/linops/rl_kronecker_linop.hh b/RandLAPACK/linops/rl_kronecker_linop.hh deleted file mode 100644 index a337c1f96..000000000 --- a/RandLAPACK/linops/rl_kronecker_linop.hh +++ /dev/null @@ -1,334 +0,0 @@ -#pragma once - -// Public API: KroneckerOperator — implicit Kronecker-product linear operator -// A = A2 ⊗ A1 with two small dense factors. - -#include "rl_concepts.hh" -#include "rl_blaspp.hh" - -#include -#include -#include - - -namespace RandLAPACK::linops { - -/*********************************************************/ -/* */ -/* KroneckerOperator */ -/* */ -/*********************************************************/ - -/// @brief Linear operator representing A = A2 ⊗ A1, with small dense factors. -/// -/// @details Applies the Kronecker product implicitly via the identity -/// (A2 ⊗ A1) vec(X) = vec(A1 X A2^T), with X = reshape(x, n1, n2). -/// Each matvec is two GEMM calls (one with A1, one with A2). The full -/// operator A is m1*m2 × n1*n2 and is never materialized. Both factors are -/// stored in column-major layout with leading dimensions m1 (for A1) and -/// m2 (for A2). -/// -/// Use case: tall, structured least-squares problems whose forward operator -/// has Kronecker structure — most notably 2D NMR relaxometry (PRnmr in the -/// IR Tools toolbox) and any separable-kernel Fredholm integral equation. -/// -/// Storage: this operator owns its A1 and A2 buffers (allocated in the -/// constructor, freed by the destructor). The factors are typically tiny -/// (e.g., 256×128 each at default NMR scale) compared to the implicit -/// m1*m2 × n1*n2 operator. -/// -/// Supported call patterns: -/// - Side::Left, NoTrans/NoTrans, dense B — forward apply (per-col loop) -/// - Side::Left, Trans/NoTrans, dense B — adjoint apply (per-col loop) -/// - Side::Right, NoTrans/NoTrans, dense B — used by Side::Right SkOp path -/// - Side::Right, NoTrans/NoTrans, sparse SkOp — sketch step (nnz-loop) -/// -/// Limitations: Side::Right with general dense B uses a row-loop dispatch; -/// other configurations (trans_B != NoTrans, sketch with dense SkOp, etc.) -/// will throw via randblas_require. -template -struct KroneckerOperator { - using scalar_t = T; - const int64_t n_rows; ///< m1 * m2 - const int64_t n_cols; ///< n1 * n2 - const int64_t m1, n1, m2, n2; - T* A1; ///< m1 × n1, ColMajor, lda = m1, owned - T* A2; ///< m2 × n2, ColMajor, lda = m2, owned - - /// Construct a Kronecker operator. The constructor copies A1 and A2 into - /// internally-owned buffers (the source pointers may be freed afterwards). - KroneckerOperator(int64_t m1_, int64_t n1_, int64_t m2_, int64_t n2_, - const T* A1_buff, const T* A2_buff) - : n_rows(m1_ * m2_), n_cols(n1_ * n2_), - m1(m1_), n1(n1_), m2(m2_), n2(n2_) - { - A1 = new T[m1 * n1]; - A2 = new T[m2 * n2]; - std::copy(A1_buff, A1_buff + m1 * n1, A1); - std::copy(A2_buff, A2_buff + m2 * n2, A2); - } - - ~KroneckerOperator() { - delete[] A1; - delete[] A2; - } - - KroneckerOperator(const KroneckerOperator&) = delete; - KroneckerOperator& operator=(const KroneckerOperator&) = delete; - - /// @brief Frobenius norm: ||A2 ⊗ A1||_F = ||A1||_F * ||A2||_F. - T fro_nrm() { - T n1_fro = blas::nrm2(m1 * n1, A1, 1); - T n2_fro = blas::nrm2(m2 * n2, A2, 1); - return n1_fro * n2_fro; - } - - // ----------------------------------------------------------------- - // Concept-required overload (no Side; defaults to Side::Left) - // ----------------------------------------------------------------- - void operator()( - Layout layout, Op trans_A, Op trans_B, - int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) - { - (*this)(Side::Left, layout, trans_A, trans_B, m, n, k, alpha, B, ldb, beta, C, ldc); - } - - // ----------------------------------------------------------------- - // Dense matmul with explicit Side - // Side::Left: C = alpha * op(A) * B + beta * C - // Side::Right: C = alpha * B * op(A) + beta * C - // ----------------------------------------------------------------- - void operator()( - Side side, Layout layout, Op trans_A, Op trans_B, - int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) - { - randblas_require(layout == Layout::ColMajor); - randblas_require(trans_B == Op::NoTrans); - - if (side == Side::Left) { - apply_left_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); - } else { - apply_right_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); - } - } - - // ----------------------------------------------------------------- - // Sketching-operator overloads - // ----------------------------------------------------------------- - template - void operator()( - Layout layout, Op trans_A, Op trans_S, - int64_t m, int64_t n, int64_t k, - T alpha, SkOp& S, T beta, T* C, int64_t ldc) - { - (*this)(Side::Right, layout, trans_A, trans_S, m, n, k, alpha, S, beta, C, ldc); - } - - template - void operator()( - Side side, Layout layout, Op trans_A, Op trans_S, - int64_t m, int64_t n, int64_t k, - T alpha, SkOp& S, T beta, T* C, int64_t ldc) - { - randblas_require(layout == Layout::ColMajor); - randblas_require(side == Side::Right); - randblas_require(trans_A == Op::NoTrans); - randblas_require(trans_S == Op::NoTrans); - // Side::Right, NoTrans/NoTrans: C = alpha * S * A + beta * C - // m = rows of C = rows of S = d - // n = cols of C = cols of A = n1*n2 - // k = inner = cols of S = rows of A = m1*m2 - randblas_require(k == m1 * m2); - randblas_require(n == n1 * n2); - randblas_require(ldc >= m); - - if constexpr (requires { S.buff; S.layout; S.dist; }) { - // Dense SkOp: forward to the dense Side::Right path using S.buff. - if (S.buff == nullptr) RandBLAS::fill_dense(S); - int64_t lds = S.dist.dim_major; - // RandBLAS DenseSkOp may be RowMajor — densely-stored SkOps store data - // with a single layout. If S.layout != ColMajor, treat the buffer as a - // ColMajor view of S^T (which is m × d), and apply with trans_S = Trans. - // Currently we only need NoTrans so require matching layout. - randblas_require(S.layout == Layout::ColMajor); - apply_right_dense(Op::NoTrans, m, n, k, alpha, S.buff, lds, beta, C, ldc); - } else { - // Sparse SkOp (SASO): iterate nnz directly. No materialization. - if (S.nnz < 0) RandBLAS::fill_sparse(S); - auto S_coo = RandBLAS::coo_view_of_skop(S); - apply_right_sparse_coo(m, n, alpha, beta, - S_coo.nnz, S_coo.rows, S_coo.cols, S_coo.vals, - C, ldc); - } - } - -private: - // ----------------------------------------------------------------- - // Side::Left dense apply: C = alpha * op(A) * B + beta * C - // - // Per-column loop. For each column j of B (length n1*n2 in NoTrans case - // or m1*m2 in Trans case), reshape into a small matrix, apply two GEMMs. - // Math: - // NoTrans: A x = vec(A1 X A2^T), X = reshape(x, n1, n2), output m1×m2 - // Trans: A^T y = vec(A1^T Y A2), Y = reshape(y, m1, m2), output n1×n2 - // ----------------------------------------------------------------- - void apply_left_dense(Op trans_A, int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, - T beta, T* C, int64_t ldc) - { - int64_t rows_in, cols_in, rows_out, cols_out; - Op trans_first, trans_second; - const T* fac1; int64_t lda_fac1; - const T* fac2; int64_t lda_fac2; - - if (trans_A == Op::NoTrans) { - // dims-before-op for op(A): (m, k) = (n_rows, n_cols) - randblas_require(m == n_rows); - randblas_require(k == n_cols); - rows_in = n1; cols_in = n2; - rows_out = m1; cols_out = m2; - // GEMM 1: Tmp = A1 * X (m1 × n2) <- (m1 × n1) * (n1 × n2) - // GEMM 2: Y = Tmp * A2^T (m1 × m2) <- (m1 × n2) * (n2 × m2) - trans_first = Op::NoTrans; fac1 = A1; lda_fac1 = m1; - trans_second = Op::Trans; fac2 = A2; lda_fac2 = m2; - } else { - // dims-before-op for op(A) = A^T: (m, k) = (n_cols, n_rows) - randblas_require(m == n_cols); - randblas_require(k == n_rows); - rows_in = m1; cols_in = m2; - rows_out = n1; cols_out = n2; - // GEMM 1: Tmp = A1^T * Y (n1 × m2) <- (n1 × m1) * (m1 × m2) - // GEMM 2: Z = Tmp * A2 (n1 × n2) <- (n1 × m2) * (m2 × n2) - trans_first = Op::Trans; fac1 = A1; lda_fac1 = m1; - trans_second = Op::NoTrans; fac2 = A2; lda_fac2 = m2; - } - - randblas_require(ldb >= rows_in * cols_in); // B columns are length k = rows_in*cols_in - randblas_require(ldc >= rows_out * cols_out); - - std::vector Tmp(static_cast(rows_out) * cols_in); - - for (int64_t j = 0; j < n; ++j) { - const T* Bj = B + j * ldb; // n1*n2 (or m1*m2) vector - T* Cj = C + j * ldc; // m1*m2 (or n1*n2) vector - - // X = reshape(Bj, rows_in, cols_in) ← view, no copy (lda = rows_in) - // Tmp = op(fac1) * X (rows_out × cols_in) - blas::gemm( - Layout::ColMajor, - trans_first, Op::NoTrans, - rows_out, cols_in, rows_in, - (T)1.0, - fac1, lda_fac1, - Bj, rows_in, - (T)0.0, - Tmp.data(), rows_out - ); - // Y = alpha * Tmp * op(fac2)^T (rows_out × cols_out) - // (where the "T" depends on direction: Trans for forward, NoTrans for adjoint) - blas::gemm( - Layout::ColMajor, - Op::NoTrans, trans_second, - rows_out, cols_out, cols_in, - alpha, - Tmp.data(), rows_out, - fac2, lda_fac2, - beta, - Cj, rows_out - ); - } - } - - // ----------------------------------------------------------------- - // Side::Right dense apply: C = alpha * B * op(A) + beta * C - // Implementation: row-by-row of B, delegate to Side::Left with flipped trans_A. - // C[i, :] = (B[i, :]) * op(A) = (op(A)^T * B[i, :]^T)^T - // ----------------------------------------------------------------- - void apply_right_dense(Op trans_A, int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, - T beta, T* C, int64_t ldc) - { - int64_t rows_op_A, cols_op_A; - if (trans_A == Op::NoTrans) { - rows_op_A = n_rows; cols_op_A = n_cols; - } else { - rows_op_A = n_cols; cols_op_A = n_rows; - } - randblas_require(k == rows_op_A); - randblas_require(n == cols_op_A); - - Op flipped = (trans_A == Op::NoTrans) ? Op::Trans : Op::NoTrans; - - std::vector b_row(k); - std::vector c_row(n); - - for (int64_t i = 0; i < m; ++i) { - // b_row = B[i, :] (gather across the i-th row in ColMajor B) - for (int64_t j = 0; j < k; ++j) b_row[j] = B[i + j * ldb]; - - // c_row = op(A)^T * b_row (Side::Left with flipped trans_A, single column) - apply_left_dense(flipped, n, /*ncols*/1, k, - (T)1.0, b_row.data(), k, - (T)0.0, c_row.data(), n); - - // C[i, :] = alpha * c_row + beta * C[i, :] - for (int64_t j = 0; j < n; ++j) { - T& ce = C[i + j * ldc]; - ce = alpha * c_row[j] + beta * ce; - } - } - } - -public: - // ----------------------------------------------------------------- - // Sparse-COO sketch apply: C = alpha * S * A + beta * C - // S is given as a COO view (rows[l], cols[l], vals[l]) with `nnz` entries. - // For each nonzero (i, p, val): C[i, :] += alpha * val * A[p, :], where - // A[p, j] = A1[p_inner, j_inner] * A2[p_outer, j_outer], with the standard - // Kronecker convention p = p_outer*m1 + p_inner, j = j_outer*n1 + j_inner. - // - // Public so wrapper operators (e.g., RegularizedLinOp) can dispatch a - // pre-filtered COO directly without re-materializing through SparseSkOp. - // ----------------------------------------------------------------- - template - void apply_right_sparse_coo(int64_t m, int64_t n, - T alpha, T beta, - int64_t nnz, const SInt* rows, const SInt* cols, const T* vals, - T* C, int64_t ldc) - { - // Scale C by beta first. - if (beta == (T)0) { - for (int64_t j = 0; j < n; ++j) - std::fill(C + j * ldc, C + j * ldc + m, (T)0); - } else if (beta != (T)1) { - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i < m; ++i) - C[i + j * ldc] *= beta; - } - - // For each nonzero, accumulate alpha * val * outer_prod(A1[p_inner,:], A2[p_outer,:]) - // into C[i, :] viewed as n1 × n2. - for (int64_t l = 0; l < nnz; ++l) { - int64_t i = rows[l]; - int64_t p = cols[l]; - T av = alpha * vals[l]; - - int64_t p_outer = p / m1; - int64_t p_inner = p % m1; - - // C[i, j_outer*n1 + j_inner] += av * A1[p_inner, j_inner] * A2[p_outer, j_outer] - for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { - T scaled = av * A2[p_outer + j_outer * m2]; - if (scaled == (T)0) continue; - T* C_col_block = C + (j_outer * n1) * ldc + i; // start of i-th row, j_outer*n1-th col block - for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { - C_col_block[j_inner * ldc] += scaled * A1[p_inner + j_inner * m1]; - } - } - } - } -}; - -} // end namespace RandLAPACK::linops diff --git a/RandLAPACK/linops/rl_linops.hh b/RandLAPACK/linops/rl_linops.hh index a76d426e5..b981f2c35 100644 --- a/RandLAPACK/linops/rl_linops.hh +++ b/RandLAPACK/linops/rl_linops.hh @@ -16,5 +16,3 @@ #include "rl_composite_linop.hh" #include "rl_sym_linops.hh" #include "rl_materialize.hh" -#include "rl_kronecker_linop.hh" -#include "rl_regularized_linop.hh" diff --git a/RandLAPACK/linops/rl_regularized_linop.hh b/RandLAPACK/linops/rl_regularized_linop.hh deleted file mode 100644 index c3c6b6cc6..000000000 --- a/RandLAPACK/linops/rl_regularized_linop.hh +++ /dev/null @@ -1,269 +0,0 @@ -#pragma once - -// Public API: RegularizedLinOp — wraps a tall LinearOperator J as the -// augmented (m+n) × n operator -// -// ⎡ J ⎤ -// A_aug = ⎣ λI ⎦ -// -// so that QR(A_aug) succeeds even when J is highly ill-conditioned (σ_min(J) -// can be anything, but σ_min(A_aug) ≥ λ by construction). Solving -// min ||A_aug x − [b; 0]||₂² is mathematically equivalent to the Tikhonov- -// regularized LS problem min ||J x − b||₂² + λ²||x||₂². This is the -// standard augmentation trick for handling ill-posed inverse problems. - -#include "rl_concepts.hh" -#include "rl_blaspp.hh" - -#include -#include -#include - - -namespace RandLAPACK::linops { - -/*********************************************************/ -/* */ -/* RegularizedLinOp */ -/* */ -/*********************************************************/ - -/// @brief Tikhonov-augmented LinearOperator wrapper: A_aug = [J; λI]. -/// -/// @tparam JLO Underlying tall LinearOperator type. Must satisfy -/// `LinearOperator` and provide both forward + adjoint apply -/// and a Side::Right + SkOp overload (used by the sketch step -/// of CQRRT_linops). -template -struct RegularizedLinOp { - using scalar_t = typename JLO::scalar_t; - using T = scalar_t; - - JLO& J; - const T lambda; - const int64_t m_J; ///< J.n_rows - const int64_t n_J; ///< J.n_cols - const int64_t n_rows; ///< m_J + n_J - const int64_t n_cols; ///< n_J - - RegularizedLinOp(JLO& J_in, T lambda_in) - : J(J_in), lambda(lambda_in), - m_J(J_in.n_rows), n_J(J_in.n_cols), - n_rows(J_in.n_rows + J_in.n_cols), - n_cols(J_in.n_cols) {} - - // ----------------------------------------------------------------- - // Concept-required overload (no Side; defaults to Side::Left) - // ----------------------------------------------------------------- - void operator()( - Layout layout, Op trans_A, Op trans_B, - int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) - { - (*this)(Side::Left, layout, trans_A, trans_B, m, n, k, alpha, B, ldb, beta, C, ldc); - } - - // ----------------------------------------------------------------- - // Dense matmul with Side - // Side::Left: C = alpha * op(A_aug) * B + beta * C - // Side::Right: C = alpha * B * op(A_aug) + beta * C - // ----------------------------------------------------------------- - void operator()( - Side side, Layout layout, Op trans_A, Op trans_B, - int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) - { - randblas_require(layout == Layout::ColMajor); - randblas_require(trans_B == Op::NoTrans); - - if (side == Side::Left) { - apply_left_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); - } else { - apply_right_dense(trans_A, m, n, k, alpha, B, ldb, beta, C, ldc); - } - } - - // ----------------------------------------------------------------- - // Sketching-operator overloads - // - // Side::Right, NoTrans/NoTrans corresponds to the CQRRT_linops sketch - // step `A_hat = S * A_aug` where S is d × (m_J+n_J). - // ----------------------------------------------------------------- - template - void operator()( - Layout layout, Op trans_A, Op trans_S, - int64_t m, int64_t n, int64_t k, - T alpha, SkOp& S, T beta, T* C, int64_t ldc) - { - (*this)(Side::Right, layout, trans_A, trans_S, m, n, k, alpha, S, beta, C, ldc); - } - - template - void operator()( - Side side, Layout layout, Op trans_A, Op trans_S, - int64_t m, int64_t n, int64_t k, - T alpha, SkOp& S, T beta, T* C, int64_t ldc) - { - randblas_require(layout == Layout::ColMajor); - randblas_require(side == Side::Right); - randblas_require(trans_A == Op::NoTrans); - randblas_require(trans_S == Op::NoTrans); - randblas_require(k == m_J + n_J); - randblas_require(n == n_J); - randblas_require(ldc >= m); - - if constexpr (requires { S.buff; S.layout; S.dist; }) { - // Dense SkOp: split S.buff column-wise. Forwards to dense Side::Right. - if (S.buff == nullptr) RandBLAS::fill_dense(S); - int64_t lds = S.dist.dim_major; - randblas_require(S.layout == Layout::ColMajor); - apply_right_dense(Op::NoTrans, m, n, k, alpha, S.buff, lds, beta, C, ldc); - } else { - // Sparse SASO path. Partition S's COO into J-touching nonzeros - // (col < m_J) and I-touching nonzeros (col ≥ m_J), then dispatch - // each subset in bulk: - // J-touching → one call to J.apply_right_sparse_coo (assumes - // JLO exposes it; KroneckerOperator does) - // I-touching → scalar updates C[i, p − m_J] += α·val·λ - // This avoids the per-nonzero J-adjoint apply that scales as - // O(nnz_J × cost(J adj)) and dominated runtime at moderate n. - if (S.nnz < 0) RandBLAS::fill_sparse(S); - auto S_coo = RandBLAS::coo_view_of_skop(S); - using sint_t = std::remove_pointer_t; - - // First pass: count J-touching nonzeros. - int64_t nnz_J = 0; - for (int64_t l = 0; l < S_coo.nnz; ++l) { - if (static_cast(S_coo.cols[l]) < m_J) ++nnz_J; - } - int64_t nnz_I = S_coo.nnz - nnz_J; - - // Second pass: copy J-touching into local arrays. - std::vector J_rows; J_rows.reserve(nnz_J); - std::vector J_cols; J_cols.reserve(nnz_J); - std::vector J_vals; J_vals.reserve(nnz_J); - // I-touching: collected into parallel arrays for the scalar pass. - std::vector I_rows; I_rows.reserve(nnz_I); - std::vector I_cols; I_cols.reserve(nnz_I); - std::vector I_vals; I_vals.reserve(nnz_I); - for (int64_t l = 0; l < S_coo.nnz; ++l) { - int64_t p = static_cast(S_coo.cols[l]); - if (p < m_J) { - J_rows.push_back(S_coo.rows[l]); - J_cols.push_back(S_coo.cols[l]); - J_vals.push_back(S_coo.vals[l]); - } else { - I_rows.push_back(S_coo.rows[l]); - I_cols.push_back(p - m_J); - I_vals.push_back(S_coo.vals[l]); - } - } - - // J-touching: bulk dispatch via J's COO entry point. - // The call writes C ← α·(S_J · J) + β·C in one pass. - J.apply_right_sparse_coo(m, n_J, alpha, beta, - nnz_J, J_rows.data(), J_cols.data(), J_vals.data(), - C, ldc); - - // I-touching: scalar updates C[i, col] += α·val·λ. - // Note: the β scaling above already applied to the entire C, so - // we just accumulate (no need to re-apply β). - T scale = alpha * lambda; - for (int64_t l = 0; l < nnz_I; ++l) { - C[I_rows[l] + I_cols[l] * ldc] += scale * I_vals[l]; - } - } - } - -private: - // ----------------------------------------------------------------- - // Side::Left dense apply: C = alpha * op(A_aug) * B + beta * C - // - // NoTrans: C is (m_J + n_J) × n_blas. - // C_top (first m_J rows) ← alpha * J * B + beta * C_top - // C_bot (last n_J rows) ← alpha * λ * B + beta * C_bot - // - // Trans: C is n_J × n_blas, B is (m_J + n_J) × n_blas. - // C ← alpha * (J^T * B_top + λ * B_bot) + beta * C - // ----------------------------------------------------------------- - void apply_left_dense(Op trans_A, int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, - T beta, T* C, int64_t ldc) - { - if (trans_A == Op::NoTrans) { - randblas_require(m == n_rows); - randblas_require(k == n_cols); - randblas_require(ldc >= n_rows); - - // Top: C[0:m_J, :] = alpha * J * B + beta * C[0:m_J, :] - J(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m_J, n, n_J, alpha, B, ldb, beta, C, ldc); - - // Bottom: C[m_J:m_J+n_J, :] = alpha * λ * B + beta * C[m_J:..., :] - for (int64_t j = 0; j < n; ++j) { - T* Cj_bot = C + j * ldc + m_J; - const T* Bj = B + j * ldb; - T scale = alpha * lambda; - if (beta == (T)0) { - for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] = scale * Bj[i]; - } else if (beta == (T)1) { - for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] += scale * Bj[i]; - } else { - for (int64_t i = 0; i < n_J; ++i) Cj_bot[i] = scale * Bj[i] + beta * Cj_bot[i]; - } - } - } else { - // Trans: m == n_cols == n_J, k == n_rows == m_J + n_J - randblas_require(m == n_cols); - randblas_require(k == n_rows); - randblas_require(ldb >= n_rows); - - // First: C = alpha * J^T * B_top + beta * C - // B_top is the top m_J rows of B (length-m_J vectors), stride ldb. - J(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n_J, n, m_J, alpha, B, ldb, beta, C, ldc); - - // Then: C += alpha * λ * B_bot, where B_bot starts at row m_J of B. - for (int64_t j = 0; j < n; ++j) { - T* Cj = C + j * ldc; - const T* Bj_bot = B + j * ldb + m_J; - T scale = alpha * lambda; - for (int64_t i = 0; i < n_J; ++i) Cj[i] += scale * Bj_bot[i]; - } - } - } - - // ----------------------------------------------------------------- - // Side::Right dense apply: C = alpha * B * op(A_aug) + beta * C - // - // NoTrans on A_aug: B is p × (m_J + n_J), output C is p × n_J. - // B = [B1 | B2], B1 = p × m_J, B2 = p × n_J. - // C = alpha * (B1 * J + λ * B2) + beta * C. - // ----------------------------------------------------------------- - void apply_right_dense(Op trans_A, int64_t m, int64_t n, int64_t k, - T alpha, const T* B, int64_t ldb, - T beta, T* C, int64_t ldc) - { - randblas_require(trans_A == Op::NoTrans); // Trans on Side::Right is uncommon - randblas_require(k == n_rows); - randblas_require(n == n_cols); - randblas_require(ldb >= n_rows); - randblas_require(ldc >= m); - - // First: C = alpha * B1 * J + beta * C (B1 is the leftmost m_J cols of B) - // J's Side::Right takes a (p × m_J) dense block and returns p × n_J. - J(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, m_J, alpha, B, ldb, beta, C, ldc); - - // Then: C += alpha * λ * B2 where B2 starts at column m_J of B. - T scale = alpha * lambda; - for (int64_t j = 0; j < n; ++j) { - T* Cj = C + j * ldc; - const T* Bj_2 = B + (m_J + j) * ldb; - // B2 is (m × n_J), col-major. B2[:, j] starts at B + (m_J + j)*ldb. - for (int64_t i = 0; i < m; ++i) Cj[i] += scale * Bj_2[i]; - } - } -}; - -} // end namespace RandLAPACK::linops diff --git a/RandLAPACK/testing/rl_memory_tracker.hh b/RandLAPACK/testing/rl_memory_tracker.hh index 7ddfd3c69..9040c2be8 100644 --- a/RandLAPACK/testing/rl_memory_tracker.hh +++ b/RandLAPACK/testing/rl_memory_tracker.hh @@ -12,6 +12,7 @@ #include #include #include +#include namespace RandLAPACK { @@ -93,14 +94,22 @@ static inline long cqrrt_linops_analytical_kb(int64_t m, int64_t n, double d_fac return bytes / 1024; } -// CQRRT_linops (BQRRP): A_hat(d*n) + R_sk_copy(n*n) + R_buf(n*n) + W(n*n) + R_sk_inv(n*n) -// Peak is in the BQRRP preconditioner block (lines 288-342 of rl_cqrrt_linops.hh) where all -// five buffers are simultaneously live. A_pre (m*b) is not yet allocated at this point. +// CQRRT_linops (BQRRP): the execution has two distinct peak-memory moments: +// (1) BQRRP-preconditioner moment (lines 288-342 of rl_cqrrt_linops.hh): +// A_hat(d*n) + R_sk_copy(n*n) + R_buf(n*n) + W(n*n) + R_sk_inv(n*n) +// = d*n + 4*n*n. A_pre is NOT allocated yet here. +// (2) Gram-loop moment (same as the non-BQRRP path): +// A_hat(d*n) + R_sk_inv(n*n) + tau(n) + A_pre(m*b_eff) +// = d*n + n + n*n + m*b_eff. R_sk_copy/R_buf/W have been freed by now. +// The true analytical peak is the max of (1) and (2). Roughly: moment (1) wins for +// short-and-wide matrices (m <~ 3n); moment (2) wins for tall matrices. template -static inline long cqrrt_linops_bqrrp_analytical_kb(int64_t n, double d_factor) { +static inline long cqrrt_linops_bqrrp_analytical_kb(int64_t m, int64_t n, double d_factor, int64_t block_size) { int64_t d = static_cast(std::ceil(d_factor * n)); - long bytes = static_cast(sizeof(T)) * ((long)d * n + 4L * n * n); - return bytes / 1024; + int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; + long bqrrp_peak = static_cast(sizeof(T)) * ((long)d * n + 4L * n * n); + long gram_peak = static_cast(sizeof(T)) * ((long)d * n + n + (long)n * n + (long)m * b_eff); + return std::max(bqrrp_peak, gram_peak) / 1024; } // CholQR_linops: I_mat(n*n) + A_temp(m*b_eff) @@ -113,21 +122,24 @@ static inline long cholqr_linops_analytical_kb(int64_t m, int64_t n, int64_t blo // sCholQR3_linops (fully-blocked): // local: G(n*n) + R_temp(n*n) + M(n*n) + A_temp(m*b) + Z_buf(n*b) -// member: G1_factor(n*n) + G2_factor(n*n) (both live during iteration 3) -// Peak is during the iteration-3 Gram loop where all 5 n*n buffers and the block buffers are live. +// member: G1_factor(n*n) + G2_factor(n*n) + G3_factor(n*n) +// Peak is right after G3_factor.resize() at the end of iteration 3, where all 6 n*n +// buffers (G + R_temp + M + G1 + G2 + G3) and the block buffers (A_temp + Z_buf) are live. template static inline long scholqr3_linops_analytical_kb(int64_t m, int64_t n, int64_t block_size = 0) { int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; - long bytes = static_cast(sizeof(T)) * (5L * n * n + (long)(m + n) * b_eff); + long bytes = static_cast(sizeof(T)) * (6L * n * n + (long)(m + n) * b_eff); return bytes / 1024; } // sCholQR3_linops_basic: Q_buf(m*n) + G(n*n) + R_temp(n*n) + M(n*n) +// + G1_factor(n*n) + G2_factor(n*n) + G3_factor(n*n) // Materializes Q = A * R1^{-1} after iteration 1, then uses dense syrk for iterations 2-3. -// No blocking — always O(m*n + n^2) peak. +// Peak is right after G3_factor.resize() in iteration 3, where Q_buf, the three local +// n*n workspaces, and all three persisted Cholesky factors are simultaneously live. template static inline long scholqr3_linops_basic_analytical_kb(int64_t m, int64_t n) { - long bytes = static_cast(sizeof(T)) * ((long)m * n + 3L * n * n); + long bytes = static_cast(sizeof(T)) * ((long)m * n + 6L * n * n); return bytes / 1024; } diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index fa6da7377..b2c0decbf 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -159,8 +159,8 @@ set( add_benchmark(NAME CQRRT_linop_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_diagnostic CXX_SOURCES bench_CQRRT_linops/CQRRT_diagnostic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -# NMR / Kronecker-operator benchmark — no Eigen / fast_matrix_market dependency -add_benchmark(NAME CQRRT_linop_nmr CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_nmr.cc LINK_LIBS ${Benchmark_libs}) +# Sparse-input IR-LSQ benchmark: Q-less QR + sketch-and-solve x0 + 2-step IR. +add_benchmark(NAME CQRRT_linop_irlsq CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_irlsq.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc new file mode 100644 index 000000000..d2bab2e4d --- /dev/null +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc @@ -0,0 +1,467 @@ +// Sparse-input IR-LSQ Benchmark — Q-less QR + sketch-and-solve x₀ + 2-step IR. +// +// Loads a tall sparse matrix A from a Matrix Market (.mtx) file (M ≥ N), +// wraps it as a SparseLinOp, generates a synthetic LS problem +// x_true ~ U(-1, 1)^N, b = A x_true + Gaussian noise (||noise||/||A x_true|| = noise_level), +// and for each Q-less QR variant selected by `method_mask`: +// 1. Run the QR variant on J → R (n × n upper triangular). +// 2. Draw a fresh sparse sketch S₂ (independent of CQRRT's S₁), form +// SA = S₂·J and Sb = S₂·b, then x₀ = R⁻¹ R⁻ᵀ (SA)ᵀ Sb (paper Alg. 1, line 3). +// 3. Run IterRefineLSQ(J, R, b) starting from x₀. +// 4. Record per-(algorithm, run) ||x − x_true||/||x_true||, ||b − Ax||/||b||, +// IR iter counts, timing, peak vs predicted memory. +// +// Usage: +// ./CQRRT_linop_irlsq +// [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask] +// where: +// prec = "double" | "float" +// mtx_path = path to a tall (M ≥ N) sparse matrix in Matrix Market format +// noise_level = ||noise|| / ||A x_true|| (default 0.05) +// d_factor = sketch oversampling for both CQRRT and x₀ (default 2.0) +// sketch_nnz = nonzeros per column of the SASO sketch (default 4) +// block_size = blocking parameter for CQRRT / sCholQR3 (default 0 = library default) +// method_mask = bitmask of Q-less QR variants (default 0b1001111 = 79) +// bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) +// bit 1 ( 2): CholQR +// bit 2 ( 4): sCholQR3 +// bit 3 ( 8): sCholQR3_basic +// bit 6 ( 64): CQRRT_linop_bqrrp (BQRRP) + +#include "RandLAPACK.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "RandLAPACK/testing/rl_memory_tracker.hh" +#include "cqrrt_bench_common.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP +#include +#endif + + +using std::chrono::steady_clock; +using std::chrono::duration_cast; +using std::chrono::microseconds; + + +// ============================================================================= +// Result struct +// ============================================================================= +template +struct irlsq_result { + int64_t m, n; + int64_t run_idx; + std::string alg_name; // "CQRRT_linop", "CholQR", "sCholQR3", "sCholQR3_basic", "CQRRT_linop_bqrrp" + T noise_level; + int qr_status; // 0 on success; nonzero indicates QR breakdown (no IR-LSQ run) + + // Q-less QR + long qr_time_us; + T orth_error; + long peak_rss_kb; + long analytical_kb; + + // IR LSQ + long ir_total_us; // includes the sketch-and-solve x₀ computation + int ir_outer_iters; + int ir_inner_iters_total; + T ls_residual_norm; + T ls_solution_error; + + // Breakdowns + std::vector qr_breakdown; + std::vector ir_breakdown; +}; + + +template +static void print_summary(const irlsq_result& r) { + std::printf("\n [%s] Run %ld (noise=%.3f):\n", + r.alg_name.c_str(), (long)r.run_idx, (double)r.noise_level); + if (r.qr_status != 0) { + std::printf(" QR returned status %d — IR-LSQ skipped.\n", r.qr_status); + return; + } + std::printf(" QR: %ld us, peak_RSS=%ld KB, predicted=%ld KB\n", + r.qr_time_us, r.peak_rss_kb, r.analytical_kb); + if (r.orth_error >= 0) std::printf(" orth_err = %.3e\n", (double)r.orth_error); + std::printf(" IR-LSQ (with x_0): total=%ld us, outer=%d, inner_total=%d\n", + r.ir_total_us, r.ir_outer_iters, r.ir_inner_iters_total); + std::printf(" ||r||/||b|| = %.3e\n", (double)r.ls_residual_norm); + std::printf(" ||x-x_true||/||x_true|| = %.3e\n", (double)r.ls_solution_error); +} + + +// ============================================================================= +// Core templated runner +// ============================================================================= +template +int run_benchmark(int argc, char* argv[]) +{ + if (argc < 5) { + std::cerr << "Usage: " << argv[0] + << " " + << " [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask]\n" + << " mtx_path: tall (M >= N) sparse matrix in Matrix Market format\n" + << " noise_level: ||noise||/||A x_true|| (default 0.05)\n" + << " method_mask: bitmask of Q-less QR variants (default = 0b1001111 = 79)\n" + << " bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY)\n" + << " bit 1 ( 2): CholQR\n" + << " bit 2 ( 4): sCholQR3\n" + << " bit 3 ( 8): sCholQR3_basic\n" + << " bit 6 ( 64): CQRRT_linop_bqrrp (BQRRP)\n"; + return 1; + } + + std::string output_dir = argv[2]; + int64_t num_runs = std::stol(argv[3]); + std::string mtx_path = argv[4]; + T noise_level = (argc >= 6) ? (T)std::stod(argv[5]) : (T)0.05; + T d_factor = (argc >= 7) ? (T)std::stod(argv[6]) : (T)2.0; + int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; + int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; + int64_t method_mask = (argc >= 10) ? std::stol(argv[9]) : 0b1001111; + + std::cout << "=== Sparse IR-LSQ Benchmark (SparseLinOp + Q-less QR + IR-LSQ) ===\n" + << " mtx_path = " << mtx_path << "\n" + << " noise_lvl = " << noise_level << "\n" + << " method_mask= " << method_mask + << " (linop=" << (method_mask&1) + << " CholQR=" << ((method_mask>>1)&1) + << " sCholQR3=" << ((method_mask>>2)&1) + << " sCholQR3_basic=" << ((method_mask>>3)&1) + << " linop_bqrrp=" << ((method_mask>>6)&1) << ")\n" + << " d_factor = " << d_factor << "\n" + << " sketch_nnz = " << sketch_nnz << "\n" + << " block_size = " << block_size << "\n" + << " num_runs = " << num_runs << "\n" +#ifdef _OPENMP + << " OpenMP threads: " << omp_get_max_threads() << "\n"; +#else + << " OpenMP threads: 1\n"; +#endif + + // -------- Load matrix -------- + int64_t M = 0, N = 0, nnz = 0; + std::cout << "Loading " << mtx_path << " ... " << std::flush; + auto load_t0 = steady_clock::now(); + auto A_csr = load_csr(mtx_path, M, N, nnz); + auto load_t1 = steady_clock::now(); + std::cout << "done (" << duration_cast(load_t1 - load_t0).count() << " us)\n" + << " M=" << M << " N=" << N << " nnz=" << nnz + << " density=" << ((double)nnz / ((double)M * (double)N)) << "\n"; + + if (M < N) { + std::cerr << "Error: need tall matrix (M >= N), got M=" << M << " N=" << N << "\n"; + return 1; + } + + RandLAPACK::linops::SparseLinOp> J(M, N, A_csr); + + // -------- Synthetic LS problem: x_true ~ U(-1,1), b = J x_true + noise -------- + std::vector x_true(N), b_clean(M, (T)0), b(M, (T)0), noise_vec(M, (T)0); + { + std::mt19937 rng(42); + std::uniform_real_distribution dist((T)-1.0, (T)1.0); + for (auto& v : x_true) v = dist(rng); + } + T x_true_norm = blas::nrm2(N, x_true.data(), 1); + if (x_true_norm == 0) { std::cerr << "x_true is zero — aborting\n"; return 1; } + + J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + M, 1, N, (T)1.0, x_true.data(), N, (T)0.0, b_clean.data(), M); + T b_clean_norm = blas::nrm2(M, b_clean.data(), 1); + + { + std::mt19937 noise_rng(13); + std::normal_distribution N01(0, 1); + for (auto& v : noise_vec) v = N01(noise_rng); + T raw_noise_norm = blas::nrm2(M, noise_vec.data(), 1); + T scale = noise_level * b_clean_norm / raw_noise_norm; + for (int64_t i = 0; i < M; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; + } + T b_norm = blas::nrm2(M, b.data(), 1); + std::cout << "Synthetic LS problem: ||x_true|| = " << x_true_norm + << ", ||b|| = " << b_norm << "\n\n"; + + // -------- RNG states for runs -------- + RandBLAS::RNGState main_state(123); + std::vector> run_states(num_runs); + for (int64_t r = 0; r < num_runs; ++r) { + run_states[r] = main_state; + if (r > 0) run_states[r].key.incr(r); + } + T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); + + // -------- Warmup -------- + std::cout << "Running warmup... " << std::flush; + { + auto warm_state = run_states[0]; + std::vector R_warm(N * N, (T)0); + RandLAPACK::CQRRT_linops warm_algo(false, tol, false); + warm_algo.nnz = sketch_nnz; + warm_algo.block_size = block_size; + warm_algo.call(J, R_warm.data(), N, d_factor, warm_state); + } + std::cout << "done\n\n"; + + // -------- Per-(algorithm, run) lambda -------- + auto run_one = [&](const std::string& alg_name, int64_t r) -> irlsq_result + { + irlsq_result res{}; + res.m = M; res.n = N; + res.run_idx = r; + res.alg_name = alg_name; + res.noise_level = noise_level; + res.qr_status = 0; + + // ---- Q-less QR on J: dispatch on alg_name ---- + std::cout << "[Run " << r << ", " << alg_name << "] QR ... " << std::flush; + std::vector R(N * N, (T)0); + auto state = run_states[r]; + + RandLAPACK::PeakRSSTracker mem; mem.start(); + if (alg_name == "sCholQR3") { + RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(J, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[17]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(M, N, block_size); + } + } else if (alg_name == "sCholQR3_basic") { + RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); + res.qr_status = qr_algo.call(J, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[14]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(M, N); + } + } else if (alg_name == "CholQR") { + RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(J, R.data(), N); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[5]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 6); + res.qr_breakdown.resize(11, 0); + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(M, N, block_size); + } + } else { + RandLAPACK::CQRRT_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.nnz = sketch_nnz; + qr_algo.block_size = block_size; + if (alg_name == "CQRRT_linop") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; + else /* CQRRT_linop_bqrrp */ qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; + res.qr_status = qr_algo.call(J, R.data(), N, d_factor, state); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[10]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = (alg_name == "CQRRT_linop_bqrrp") + ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(M, N, d_factor, block_size) + : RandLAPACK::cqrrt_linops_analytical_kb(M, N, d_factor, block_size); + } + } + + if (res.qr_status != 0) { + std::cerr << "\n [" << alg_name << "] Run " << r + << ": QR returned status " << res.qr_status + << " (likely Cholesky breakdown).\n" + << " The input may be too ill-conditioned for the chosen variant;\n" + << " try a more stabilized variant (CQRRT_linop_bqrrp / sCholQR3) or a larger d_factor.\n"; + res.qr_time_us = -1; + res.qr_breakdown.assign(11, 0); + res.analytical_kb = 0; + res.orth_error = (T)-1.0; + res.ir_total_us = 0; + res.ir_outer_iters = 0; + res.ir_inner_iters_total = 0; + res.ls_residual_norm = (T)-1.0; + res.ls_solution_error = (T)-1.0; + print_summary(res); + return res; + } + res.orth_error = (T)-1.0; + std::cout << "done (" << res.qr_time_us << " us). IR-LSQ ... " << std::flush; + + // ---- Sketch-and-solve initial guess (Algorithm 1, line 3 of + // Epperly–Meier–Nakatsukasa 2025), with a fresh sparse sketch + // S₂ independent of CQRRT's S₁: + // SA = S₂·J (d_init × N) + // Sb = S₂·b (d_init × 1) + // x₀ = R⁻¹ R⁻ᵀ (SA)ᵀ Sb (Q-less form; R reused as preconditioner) + // Timing folded into ir_total_us alongside the IR steps. + auto ls_t0 = steady_clock::now(); + const int64_t d_init = (int64_t)(d_factor * (T)N); + RandBLAS::SparseDist DS_init(d_init, M, sketch_nnz); + auto x0_state = state; + x0_state.key.incr(0xA1B2C3D4u); + RandBLAS::SparseSkOp S2(DS_init, x0_state); + RandBLAS::fill_sparse(S2); + + std::vector SA(d_init * N, (T)0); + J(blas::Side::Right, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + d_init, N, M, (T)1.0, S2, (T)0.0, SA.data(), d_init); + + std::vector Sb(d_init, (T)0); + RandBLAS::sketch_general(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + d_init, (int64_t)1, M, (T)1.0, + S2, b.data(), M, (T)0.0, Sb.data(), d_init); + + std::vector x_ls(N, (T)0); + blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, d_init, N, + (T)1.0, SA.data(), d_init, Sb.data(), 1, + (T)0.0, x_ls.data(), 1); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, N, 1, + (T)1.0, R.data(), N, x_ls.data(), N); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, N, 1, + (T)1.0, R.data(), N, x_ls.data(), N); + + // ---- IterRefineLSQ ---- + RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, + /*max_inner=*/200, + /*n_steps=*/2, + /*timing=*/true, + /*verbose=*/false); + int ir_status = ir.call(J, R.data(), N, b.data(), M, x_ls.data(), N); + auto ls_t1 = steady_clock::now(); + if (ir_status != 0) { + std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; + } + + res.ir_total_us = duration_cast(ls_t1 - ls_t0).count(); + res.ir_outer_iters = ir.outer_iters_done; + res.ir_inner_iters_total = 0; + for (int v : ir.inner_iters_per_step) res.ir_inner_iters_total += v; + res.ls_residual_norm = ir.final_residual_norm; + if (!ir.times.empty()) res.ir_breakdown = ir.times; + + T err_sq = 0; + for (int64_t i = 0; i < N; ++i) { + T d = x_ls[i] - x_true[i]; + err_sq += d * d; + } + res.ls_solution_error = std::sqrt(err_sq) / x_true_norm; + std::cout << "done (" << res.ir_total_us << " us)\n"; + + print_summary(res); + return res; + }; + + // Build the ordered list of selected algorithm names from the bitmask. + std::vector selected_algs; + if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); + if (method_mask & 2) selected_algs.push_back("CholQR"); + if (method_mask & 4) selected_algs.push_back("sCholQR3"); + if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); + if (method_mask & 64) selected_algs.push_back("CQRRT_linop_bqrrp"); + + if (selected_algs.empty()) { + std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; + return 1; + } + + std::vector> all_results; + for (const auto& alg_name : selected_algs) { + std::cout << "\n=== Algorithm: " << alg_name << " ===\n"; + for (int64_t r = 0; r < num_runs; ++r) { + all_results.push_back(run_one(alg_name, r)); + } + } + + // -------- CSV output -------- + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + + std::string results_file = output_dir + "/" + time_buf + "_irlsq_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_irlsq_breakdown.csv"; + + { + std::ofstream out(results_file); + out << "# Sparse IR-LSQ Benchmark results\n" + << "# Date: " << ctime(&now) + << "# mtx_path=" << mtx_path << "\n" + << "# M=" << M << " N=" << N << " nnz=" << nnz << "\n" + << "# noise_level=" << noise_level << "\n" + << "# d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz + << " block_size=" << block_size << "\n" + << "# method_mask=" << method_mask << "\n" +#ifdef _OPENMP + << "# OpenMP threads: " << omp_get_max_threads() << "\n" +#else + << "# OpenMP threads: 1\n" +#endif + ; + out << "algorithm,run,m,n,qr_status,qr_time_us,peak_rss_kb,analytical_kb," + "ir_total_us,ir_outer_iters,ir_inner_iters_total," + "ls_residual_norm,ls_solution_error\n"; + for (const auto& r : all_results) { + out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," + << r.qr_status << "," << r.qr_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "," + << r.ir_total_us << "," << r.ir_outer_iters << "," << r.ir_inner_iters_total << "," + << std::scientific << std::setprecision(6) << r.ls_residual_norm << "," + << std::scientific << std::setprecision(6) << r.ls_solution_error + << "\n"; + } + std::cout << "\nResults written to " << results_file << "\n"; + } + + { + std::ofstream out(breakdown_file); + out << "# Sparse IR-LSQ Benchmark runtime breakdown (microseconds)\n" + << "# QR breakdown layout depends on algorithm (see CQRRT_linop_applications.cc).\n" + << "# IR-LSQ breakdown (6): outer_total, inner_cg_total, trsm_total, fwd_total, adj_total, other\n" + << "# (sketch-and-solve x_0 time is folded into the difference between ir_total_us\n" + << "# in the results CSV and outer_total here)\n" + << "algorithm,run,phase,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10\n"; + for (const auto& r : all_results) { + out << r.alg_name << "," << r.run_idx << ",QR"; + for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; + for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + out << r.alg_name << "," << r.run_idx << ",IR"; + for (size_t i = 0; i < r.ir_breakdown.size(); ++i) out << "," << r.ir_breakdown[i]; + for (size_t i = r.ir_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + } + std::cout << "Breakdown written to " << breakdown_file << "\n"; + } + + return 0; +} + + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] + << " " + << " [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask]\n"; + return 1; + } + std::string prec = argv[1]; + if (prec == "double") return run_benchmark(argc, argv); + if (prec == "float") return run_benchmark(argc, argv); + std::cerr << "Unknown precision: " << prec << " (use 'double' or 'float')\n"; + return 1; +} diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc deleted file mode 100644 index 66be91a42..000000000 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_nmr.cc +++ /dev/null @@ -1,599 +0,0 @@ -// 2D NMR Relaxometry Benchmark — Q-less QR + iterative-refinement LSQ. -// -// Test problem: PRnmr from the IR Tools toolbox (Gazzola, Hansen, Nagy 2018). -// Forward operator A = A2 ⊗ A1 with separable Laplace-type kernel: -// -// (A1)_{l,k} = 1 - 2 * exp(-tau1[l] / T1[k]) (m1 × n1) -// (A2)_{l,k} = exp(-tau2[l] / T2[k]) (m2 × n2) -// -// Time grids are logspace(log_lo, log_hi). Default range here is (-2, 1) = -// 3 decades; the IR Tools paper uses (-4, 1) = 5 decades, but unregularized -// Q-less QR cannot handle that conditioning even with stabilized -// preconditioners. Operator dimensions: -// M = m1*m2 (rows), N = n1*n2 (cols), default m_i = 2*n_i. -// -// Pipeline: -// 1. Generate A1, A2, phantom x_true ∈ ℝ^{n1*n2}, b = A x_true + noise. -// 2. Wrap A as KroneckerOperator (no materialization). When λ > 0, also -// wrap that in RegularizedLinOp so the QR step runs on A_aug = [J; λI]. -// 3. For each Q-less QR variant selected by `method_mask`, run it on the -// operator → R (n × n upper triangular). -// 4. For each (algorithm, run): run IterRefineLSQ(A_eff, R, b_eff) → x. -// 5. Record per-(algorithm, run) ||x - x_true||/||x_true||, ||b - Ax||/||b||, -// IR iter counts, timing, and analytical-memory predictions. -// -// Usage: -// ./CQRRT_linop_nmr -// [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size] -// [log_lo] [log_hi] [method_mask] [lambda] -// where: -// prec = "double" | "float" -// n = scalar; sets n1 = n2 = n, m1 = m2 = 2*n -// phantom = "two_blob" (default) | "one_blob" -// noise_level = ||noise||/||b|| (default 0.05) -// method_mask = bitmask selecting Q-less QR variants to run, ALL piped -// through the IR-LSQ pipeline. Default 0b1101111 (all -// linop variants; CQRRT_expl excluded since materializing -// the Kronecker would defeat the purpose). -// bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) -// bit 1 ( 2): CholQR -// bit 2 ( 4): sCholQR3 -// bit 3 ( 8): sCholQR3_basic -// bit 5 ( 32): CQRRT_linop_stb (GEQP3) -// bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP) -// lambda = Tikhonov regularization. min ||Jx-b||² + λ²||x||². -// When λ>0 the QR is run on the augmented operator -// A_aug = [J; λI] so it succeeds even when J alone is -// too ill-conditioned for Q-less QR. - -#include "RandLAPACK.hh" -#include "rl_blaspp.hh" -#include "rl_lapackpp.hh" -#include "RandLAPACK/testing/rl_memory_tracker.hh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _OPENMP -#include -#endif - - -using std::chrono::steady_clock; -using std::chrono::duration_cast; -using std::chrono::microseconds; - - -// ============================================================================= -// Grid generation -// ============================================================================= -// logspace(a, b, n) -> n points evenly spaced in log10 from 10^a to 10^b. -template -static std::vector logspace(T a, T b, int64_t n) { - std::vector v(n); - if (n == 1) { v[0] = std::pow((T)10, a); return v; } - T step = (b - a) / (T)(n - 1); - for (int64_t i = 0; i < n; ++i) v[i] = std::pow((T)10, a + step * (T)i); - return v; -} - - -// ============================================================================= -// Build A1 and A2 per the PRnmr formulas. -// ============================================================================= -template -static void build_A_factors(int64_t m1, int64_t n1, int64_t m2, int64_t n2, - T log_lo, T log_hi, - std::vector& A1, std::vector& A2) -{ - auto T1 = logspace(log_lo, log_hi, n1); - auto T2 = logspace(log_lo, log_hi, n2); - auto tau1 = logspace(log_lo, log_hi, m1); - auto tau2 = logspace(log_lo, log_hi, m2); - - A1.assign(m1 * n1, (T)0); - A2.assign(m2 * n2, (T)0); - // ColMajor: A1[i + j*m1] = (A1)_{i,j} = 1 - 2*exp(-tau1[i] / T1[j]) - for (int64_t j = 0; j < n1; ++j) - for (int64_t i = 0; i < m1; ++i) - A1[i + j * m1] = (T)1.0 - (T)2.0 * std::exp(-tau1[i] / T1[j]); - for (int64_t j = 0; j < n2; ++j) - for (int64_t i = 0; i < m2; ++i) - A2[i + j * m2] = std::exp(-tau2[i] / T2[j]); -} - - -// ============================================================================= -// Phantom generation: synthetic 2D T1-T2 distribution. -// ============================================================================= -// "two_blob": sum of two 2D Gaussians in (log T1, log T2) space — coarse -// approximation of multi-population NMR phantoms (carbonate / methane). -// "one_blob": single Gaussian (simpler reference). -template -static std::vector make_phantom(int64_t n1, int64_t n2, - T log_lo, T log_hi, - const std::string& kind) -{ - std::vector x(n1 * n2, (T)0); - auto T1 = logspace(log_lo, log_hi, n1); - auto T2 = logspace(log_lo, log_hi, n2); - - auto add_gaussian = [&](T mu1, T mu2, T sigma1, T sigma2, T amp) { - for (int64_t j2 = 0; j2 < n2; ++j2) { - T lt2 = std::log10(T2[j2]); - for (int64_t j1 = 0; j1 < n1; ++j1) { - T lt1 = std::log10(T1[j1]); - T d1 = (lt1 - mu1) / sigma1; - T d2 = (lt2 - mu2) / sigma2; - x[j1 + j2 * n1] += amp * std::exp(-(T)0.5 * (d1 * d1 + d2 * d2)); - } - } - }; - - if (kind == "one_blob") { - add_gaussian((T)-1.0, (T)-1.0, (T)0.5, (T)0.5, (T)1.0); - } else { - // "two_blob" (default) - add_gaussian((T)-2.5, (T)-2.5, (T)0.4, (T)0.4, (T)1.0); // short relaxation - add_gaussian((T) 0.0, (T)-0.5, (T)0.5, (T)0.5, (T)0.7); // long relaxation - } - return x; -} - - -// ============================================================================= -// Result struct -// ============================================================================= -template -struct nmr_result { - int64_t m, n, m1, n1, m2, n2; - int64_t run_idx; - std::string alg_name; // "CQRRT_linop", "CholQR", "sCholQR3", "sCholQR3_basic", "CQRRT_linop_stb", "CQRRT_linop_stb_bqrrp" - std::string phantom; - T noise_level; - int qr_status; // 0 on success; nonzero indicates QR breakdown (no IR-LSQ run) - - // Q-less QR (CQRRT_linop) - long qr_time_us; - T orth_error; // ||Q^T Q - I||_F / sqrt(n) (only if cheap to check) - long peak_rss_kb; - long analytical_kb; - - // IR LSQ - long ir_total_us; - int ir_outer_iters; - int ir_inner_iters_total; - T ls_residual_norm; // ||b - A x|| / ||b|| - T ls_solution_error; // ||x - x_true|| / ||x_true|| - - // QR breakdown (CQRRT_linop times: 11 entries) - std::vector qr_breakdown; - // IR breakdown (6 entries: outer, inner, trsm, fwd, adj, other) - std::vector ir_breakdown; -}; - - -template -static void print_summary(const nmr_result& r) { - std::printf("\n [%s] Run %ld (phantom=%s, noise=%.3f):\n", - r.alg_name.c_str(), (long)r.run_idx, r.phantom.c_str(), (double)r.noise_level); - if (r.qr_status != 0) { - std::printf(" QR returned status %d — IR-LSQ skipped.\n", r.qr_status); - return; - } - std::printf(" QR: %ld us, peak_RSS=%ld KB, predicted=%ld KB\n", - r.qr_time_us, r.peak_rss_kb, r.analytical_kb); - if (r.orth_error >= 0) std::printf(" orth_err = %.3e\n", (double)r.orth_error); - std::printf(" IR-LSQ: total=%ld us, outer=%d, inner_total=%d\n", - r.ir_total_us, r.ir_outer_iters, r.ir_inner_iters_total); - std::printf(" ||r||/||b|| = %.3e\n", (double)r.ls_residual_norm); - std::printf(" ||x-x_true||/||x_true|| = %.3e\n", (double)r.ls_solution_error); -} - - -// ============================================================================= -// Core templated runner -// ============================================================================= -template -int run_benchmark(int argc, char* argv[]) -{ - if (argc < 5) { - std::cerr << "Usage: " << argv[0] - << " " - << " [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size]" - << " [log_lo] [log_hi] [method_mask] [lambda]\n" - << " method_mask: bitmask of Q-less QR variants (default = 0b1101111)\n" - << " bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY)\n" - << " bit 1 ( 2): CholQR\n" - << " bit 2 ( 4): sCholQR3\n" - << " bit 3 ( 8): sCholQR3_basic\n" - << " bit 5 ( 32): CQRRT_linop_stb (GEQP3)\n" - << " bit 6 ( 64): CQRRT_linop_stb_bqrrp (BQRRP)\n" - << " lambda: Tikhonov regularization. min ||Jx-b||² + λ²||x||²; default 0.0\n" - << " log_lo, log_hi: τ and T grids on logspace(log_lo, log_hi).\n" - << " Default (-2, 1) = 3 decades. The IR Tools paper uses (-4, 1) = 5 decades,\n" - << " but unregularized Q-less QR (any preconditioner) cannot handle that conditioning.\n"; - return 1; - } - - std::string output_dir = argv[2]; - int64_t num_runs = std::stol(argv[3]); - int64_t n = std::stol(argv[4]); - std::string phantom = (argc >= 6) ? std::string(argv[5]) : std::string("two_blob"); - T noise_level = (argc >= 7) ? (T)std::stod(argv[6]) : (T)0.05; - T d_factor = (argc >= 8) ? (T)std::stod(argv[7]) : (T)2.0; - int64_t sketch_nnz = (argc >= 9) ? std::stol(argv[8]) : 4; - int64_t block_size = (argc >= 10) ? std::stol(argv[9]) : 0; - T log_lo = (argc >= 11) ? (T)std::stod(argv[10]) : (T)-2.0; - T log_hi = (argc >= 12) ? (T)std::stod(argv[11]) : (T)1.0; - int64_t method_mask = (argc >= 13) ? std::stol(argv[12]) : 0b1101111; - T lambda_reg = (argc >= 14) ? (T)std::stod(argv[13]) : (T)0.0; - - int64_t n1 = n, n2 = n; - int64_t m1 = 2 * n1, m2 = 2 * n2; - int64_t M = m1 * m2; - int64_t N = n1 * n2; - - std::cout << "=== NMR Relaxometry Benchmark (Kronecker + CQRRT + IR-LSQ) ===\n" - << " m1, n1, m2, n2 = " << m1 << ", " << n1 << ", " << m2 << ", " << n2 << "\n" - << " M (=m1*m2) = " << M << ", N (=n1*n2) = " << N - << " (aspect M:N = " << (double)M / (double)N << ":1)\n" - << " phantom = " << phantom << "\n" - << " noise_lvl = " << noise_level << "\n" - << " method_mask= " << method_mask - << " (linop=" << (method_mask&1) - << " CholQR=" << ((method_mask>>1)&1) - << " sCholQR3=" << ((method_mask>>2)&1) - << " sCholQR3_basic=" << ((method_mask>>3)&1) - << " linop_stb=" << ((method_mask>>5)&1) - << " linop_stb_bqrrp=" << ((method_mask>>6)&1) << ")\n" - << " lambda = " << lambda_reg << "\n" - << " d_factor = " << d_factor << "\n" - << " sketch_nnz = " << sketch_nnz << "\n" - << " block_size = " << block_size << "\n" - << " num_runs = " << num_runs << "\n" -#ifdef _OPENMP - << " OpenMP threads: " << omp_get_max_threads() << "\n"; -#else - << " OpenMP threads: 1\n"; -#endif - - if (M < N) { - std::cerr << "Error: NMR setup requires M >= N (got M=" << M << ", N=" << N << ")\n"; - return 1; - } - - std::cout << " log range = [10^" << log_lo << ", 10^" << log_hi << "] (" - << (log_hi - log_lo) << " decades)\n"; - - // -------- Build A1, A2, KroneckerOperator -------- - std::vector A1, A2; - std::cout << "Building A1, A2 (logspace formulas)... " << std::flush; - auto build_t0 = steady_clock::now(); - build_A_factors(m1, n1, m2, n2, log_lo, log_hi, A1, A2); - auto build_t1 = steady_clock::now(); - std::cout << "done (" << duration_cast(build_t1 - build_t0).count() << " us)\n"; - - RandLAPACK::linops::KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); - std::cout << "Kronecker operator J = A2 ⊗ A1: " << M << " × " << N << "\n"; - - // When lambda > 0, the QR step runs on the augmented operator - // A_aug = [J; λI] (M+N rows × N cols), - // which is positive-definite-Gram by construction (σ_min ≥ λ). This - // bypasses the Cholesky-breakdown problem that hits unregularized Q-less - // QR on ill-conditioned operators (e.g., NMR Fredholm kernels). - // Solving min ||A_aug x − [b; 0]||² is mathematically the Tikhonov - // regularized problem min ||Jx − b||² + λ²||x||². - const bool use_augmented = (lambda_reg > (T)0); - RandLAPACK::linops::RegularizedLinOp> - J_aug(J, lambda_reg); - if (use_augmented) { - std::cout << "Augmented operator A_aug = [J; λI]: " << (M + N) << " × " << N - << " (λ = " << lambda_reg << ")\n"; - } - - // -------- Phantom + RHS -------- - std::vector x_true = make_phantom(n1, n2, log_lo, log_hi, phantom); - T x_true_norm = blas::nrm2(N, x_true.data(), 1); - if (x_true_norm == 0) { std::cerr << "Phantom is zero — aborting\n"; return 1; } - - // b = A * x_true (clean) - std::vector b_clean(M, (T)0), b(M, (T)0), noise_vec(M, (T)0); - J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - M, 1, N, (T)1.0, x_true.data(), N, (T)0.0, b_clean.data(), M); - T b_clean_norm = blas::nrm2(M, b_clean.data(), 1); - - // Add Gaussian noise scaled to give ||noise||/||b_clean|| = noise_level. - { - std::mt19937 noise_rng(13); - std::normal_distribution N01(0, 1); - for (auto& v : noise_vec) v = N01(noise_rng); - T raw_noise_norm = blas::nrm2(M, noise_vec.data(), 1); - T scale = noise_level * b_clean_norm / raw_noise_norm; - for (int64_t i = 0; i < M; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; - } - T b_norm = blas::nrm2(M, b.data(), 1); - std::cout << "Synthetic LS problem: ||x_true|| = " << x_true_norm - << ", ||b|| = " << b_norm << "\n\n"; - - // Augmented RHS b_aug = [b; 0] (length M+N), used when lambda > 0. - std::vector b_aug; - if (use_augmented) { - b_aug.assign(M + N, (T)0); - std::copy(b.begin(), b.end(), b_aug.begin()); - } - - // -------- RNG states for runs -------- - RandBLAS::RNGState main_state(123); - std::vector> run_states(num_runs); - for (int64_t r = 0; r < num_runs; ++r) { - run_states[r] = main_state; - if (r > 0) run_states[r].key.incr(r); - } - T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); - - // -------- Warmup -------- - std::cout << "Running warmup... " << std::flush; - { - auto warm_state = run_states[0]; - std::vector R_warm(N * N, (T)0); - RandLAPACK::CQRRT_linops warm_algo(false, tol, false); - warm_algo.nnz = sketch_nnz; - warm_algo.block_size = block_size; - warm_algo.call(J, R_warm.data(), N, d_factor, warm_state); - } - std::cout << "done\n\n"; - - // -------- Per-(algorithm, run) lambda -------- - // Same body for J (unregularized) or J_aug (regularized) via the JLO template. - // m_eff = number of rows of the operator (M for J, M+N for J_aug). - // b_eff = synthetic RHS of length m_eff. - // ir_lambda = regularization strength to pass to IterRefineLSQ. When the QR - // ran on J_aug we set this to 0 (the regularization is built into A_aug); - // when the QR ran on J alone we pass lambda_reg through. - auto run_one = [&](const std::string& alg_name, - JLO& J_eff, const T* b_eff, int64_t m_eff, - T ir_lambda, int64_t r) -> nmr_result - { - nmr_result res{}; - res.m = M; res.n = N; - res.m1 = m1; res.n1 = n1; res.m2 = m2; res.n2 = n2; - res.run_idx = r; - res.alg_name = alg_name; - res.phantom = phantom; - res.noise_level = noise_level; - res.qr_status = 0; - - // ---- Q-less QR on J_eff: dispatch on alg_name ---- - std::cout << "[Run " << r << ", " << alg_name << "] QR ... " << std::flush; - std::vector R(N * N, (T)0); - auto state = run_states[r]; - - RandLAPACK::PeakRSSTracker mem; mem.start(); - if (alg_name == "sCholQR3") { - // Fully-blocked shifted Cholesky-QR-3. Times has 18 entries; total at [17]. - RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(J_eff, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[17]; - // First 11 of 18 entries are kept; full breakdown table is elsewhere. - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m_eff, N, block_size); - } - } else if (alg_name == "sCholQR3_basic") { - // Non-blocked variant (matches the textbook sCholQR3 pseudocode). - // Times has 15 entries; total at [14]. - RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); - res.qr_status = qr_algo.call(J_eff, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[14]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m_eff, N); - } - } else if (alg_name == "CholQR") { - RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(J_eff, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[5]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 6); - res.qr_breakdown.resize(11, 0); // pad to 11 for breakdown CSV uniformity - res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m_eff, N, block_size); - } - } else { - // CQRRT_linops with selectable preconditioner. - RandLAPACK::CQRRT_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.nnz = sketch_nnz; - qr_algo.block_size = block_size; - if (alg_name == "CQRRT_linop") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; - else if (alg_name == "CQRRT_linop_stb") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::GEQP3; - else /* CQRRT_linop_stb_bqrrp */ qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; - res.qr_status = qr_algo.call(J_eff, R.data(), N, d_factor, state); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[10]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = (alg_name == "CQRRT_linop_stb_bqrrp") - ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(N, d_factor) - : RandLAPACK::cqrrt_linops_analytical_kb(m_eff, N, d_factor, block_size); - } - } - - if (res.qr_status != 0) { - std::cerr << "\n [" << alg_name << "] Run " << r - << ": QR returned status " << res.qr_status - << " (likely Cholesky breakdown).\n" - << " Try a different algorithm, larger d_factor, or increase lambda.\n"; - res.qr_time_us = -1; - res.qr_breakdown.assign(11, 0); - res.analytical_kb = 0; - res.orth_error = (T)-1.0; - res.ir_total_us = 0; - res.ir_outer_iters = 0; - res.ir_inner_iters_total = 0; - res.ls_residual_norm = (T)-1.0; - res.ls_solution_error = (T)-1.0; - print_summary(res); - return res; - } - res.orth_error = (T)-1.0; - std::cout << "done (" << res.qr_time_us << " us). IR-LSQ ... " << std::flush; - - // ---- IterRefineLSQ ---- - std::vector x_ls(N, (T)0); - RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, - /*max_inner=*/200, - /*n_steps=*/2, - /*timing=*/true, - /*verbose=*/false, - /*lambda=*/ir_lambda); - auto ls_t0 = steady_clock::now(); - int ir_status = ir.call(J_eff, R.data(), N, b_eff, m_eff, x_ls.data(), N); - auto ls_t1 = steady_clock::now(); - if (ir_status != 0) { - std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; - } - - res.ir_total_us = duration_cast(ls_t1 - ls_t0).count(); - res.ir_outer_iters = ir.outer_iters_done; - res.ir_inner_iters_total = 0; - for (int v : ir.inner_iters_per_step) res.ir_inner_iters_total += v; - res.ls_residual_norm = ir.final_residual_norm; - if (!ir.times.empty()) res.ir_breakdown = ir.times; - - // Solution error: ||x_ls - x_true|| / ||x_true|| - T err_sq = 0; - for (int64_t i = 0; i < N; ++i) { - T d = x_ls[i] - x_true[i]; - err_sq += d * d; - } - res.ls_solution_error = std::sqrt(err_sq) / x_true_norm; - std::cout << "done (" << res.ir_total_us << " us)\n"; - - print_summary(res); - return res; - }; - - // Build the ordered list of selected algorithm names from the bitmask. - // Bit assignments match CQRRT_linop_applications.cc (CQRRT_expl, bit 4, is - // intentionally excluded — materializing the Kronecker would defeat the - // purpose). sCholQR3_basic shares the implementation with sCholQR3 here. - std::vector selected_algs; - if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); - if (method_mask & 2) selected_algs.push_back("CholQR"); - if (method_mask & 4) selected_algs.push_back("sCholQR3"); - if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); - if (method_mask & 32) selected_algs.push_back("CQRRT_linop_stb"); - if (method_mask & 64) selected_algs.push_back("CQRRT_linop_stb_bqrrp"); - - if (selected_algs.empty()) { - std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; - return 1; - } - - std::vector> all_results; - for (const auto& alg_name : selected_algs) { - std::cout << "\n=== Algorithm: " << alg_name << " ===\n"; - for (int64_t r = 0; r < num_runs; ++r) { - nmr_result res; - if (use_augmented) { - res = run_one(alg_name, J_aug, b_aug.data(), M + N, (T)0, r); - } else { - res = run_one(alg_name, J, b.data(), M, lambda_reg, r); - } - all_results.push_back(std::move(res)); - } - } - - // -------- CSV output -------- - char time_buf[64]; - time_t now = time(nullptr); - strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); - - std::string results_file = output_dir + "/" + time_buf + "_nmr_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_nmr_breakdown.csv"; - - { - std::ofstream out(results_file); - out << "# NMR Relaxometry Benchmark results\n" - << "# Date: " << ctime(&now) - << "# m1=" << m1 << " n1=" << n1 << " m2=" << m2 << " n2=" << n2 << "\n" - << "# M=" << M << " N=" << N << "\n" - << "# phantom=" << phantom << " noise_level=" << noise_level << "\n" - << "# d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz - << " block_size=" << block_size << "\n" - << "# method_mask=" << method_mask << " lambda=" << lambda_reg << "\n" - << "# log_lo=" << log_lo << " log_hi=" << log_hi << "\n" -#ifdef _OPENMP - << "# OpenMP threads: " << omp_get_max_threads() << "\n" -#else - << "# OpenMP threads: 1\n" -#endif - ; - out << "algorithm,run,m,n,qr_status,qr_time_us,peak_rss_kb,analytical_kb," - "ir_total_us,ir_outer_iters,ir_inner_iters_total," - "ls_residual_norm,ls_solution_error\n"; - for (const auto& r : all_results) { - out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," - << r.qr_status << "," << r.qr_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "," - << r.ir_total_us << "," << r.ir_outer_iters << "," << r.ir_inner_iters_total << "," - << std::scientific << std::setprecision(6) << r.ls_residual_norm << "," - << std::scientific << std::setprecision(6) << r.ls_solution_error - << "\n"; - } - std::cout << "\nResults written to " << results_file << "\n"; - } - - { - std::ofstream out(breakdown_file); - out << "# NMR Benchmark runtime breakdown (microseconds)\n" - << "# QR breakdown layout depends on algorithm:\n" - << "# CQRRT_linop[_stb*] (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n" - << "# sCholQR3 / sCholQR3_basic — full 18 entries truncated to first 11 (alloc, fwd1, adj1, chol1, upd1, fwd2, adj2, gemm2, chol2, upd2, fwd3)\n" - << "# CholQR (6, padded to 11): alloc, fwd, adj, chol, rest, total\n" - << "# IR-LSQ breakdown (6): outer_total, inner_cg_total, trsm_total, fwd_total, adj_total, other\n" - << "algorithm,run,phase,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10\n"; - for (const auto& r : all_results) { - out << r.alg_name << "," << r.run_idx << ",QR"; - for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; - for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; - out << "\n"; - out << r.alg_name << "," << r.run_idx << ",IR"; - for (size_t i = 0; i < r.ir_breakdown.size(); ++i) out << "," << r.ir_breakdown[i]; - for (size_t i = r.ir_breakdown.size(); i < 11; ++i) out << ",0"; - out << "\n"; - } - std::cout << "Breakdown written to " << breakdown_file << "\n"; - } - - return 0; -} - - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] - << " " - << " [phantom] [noise_level] [d_factor] [sketch_nnz] [block_size]\n"; - return 1; - } - std::string prec = argv[1]; - if (prec == "double") return run_benchmark(argc, argv); - if (prec == "float") return run_benchmark(argc, argv); - std::cerr << "Unknown precision: " << prec << " (use 'double' or 'float')\n"; - return 1; -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7a1e7155f..2bbd5dba0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,8 +27,6 @@ if (GTest_FOUND) misc/test_pdkernels.cc linops/test_linops.cc linops/test_linop_unified.cc - linops/test_kronecker_linop.cc - linops/test_regularized_linop.cc misc/test_gen.cc misc/test_memory_tracker.cc linops/test_linop_block_views.cc diff --git a/test/drivers/test_iter_refine_lsq.cc b/test/drivers/test_iter_refine_lsq.cc index 06090f573..19fa29c7b 100644 --- a/test/drivers/test_iter_refine_lsq.cc +++ b/test/drivers/test_iter_refine_lsq.cc @@ -3,10 +3,8 @@ // // Strategy: wrap a small dense tall A as a DenseLinOp, build R from // lapack::geqrf on a copy of A, then verify the IR-LSQ solution matches -// a closed-form reference: -// * unregularized cases → lapack::gels on (A, b) -// * Tikhonov case → lapack::posv on (A^T A + lambda^2 I, A^T b) -// across well-conditioned, residualful, regularized, and imperfect-R cases. +// the closed-form lapack::gels reference across well-conditioned, +// residualful, and imperfect-R cases. #include #include @@ -136,51 +134,6 @@ TEST_F(TestIterRefineLSQ, dense_with_residual) { } -// Tikhonov regularization: solving min ||Jx-b||² + λ²||x||² should match -// the closed-form solution x = (J^T J + λ² I)^{-1} J^T b. -TEST_F(TestIterRefineLSQ, tikhonov_regularization) { - using T = double; - int64_t m = 80, n = 12; - T lambda = 0.3; - - std::vector A(m * n), b(m); - fill_random(A, 101); - fill_random(b, 102); - - // Reference: solve (A^T A + λ² I) x = A^T b directly via lapack::posv. - std::vector Gram(n * n, 0.0), AtB(n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, (T)1.0, A.data(), m, (T)0.0, Gram.data(), n); - for (int64_t i = 0; i < n; ++i) Gram[i + i * n] += lambda * lambda; - blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, m, n, (T)1.0, A.data(), m, - b.data(), 1, (T)0.0, AtB.data(), 1); - std::vector x_ref(AtB); - lapack::posv(blas::Uplo::Upper, n, 1, Gram.data(), n, x_ref.data(), n); - - // Build R from QR(A) — perfect preconditioner for J^T J alone, plus the - // Tikhonov shift gets added inside the inner CG. - std::vector R(n * n, 0); - build_R_from_A(A.data(), m, n, R.data(), n); - - DenseLinOp J(m, n, A.data(), m, Layout::ColMajor); - IterRefineLSQ ir(/*tol=*/1e-12, /*max_inner=*/200, /*n_steps=*/2, - /*timing=*/false, /*verbose=*/false, /*lambda=*/lambda); - std::vector x_ir(n, 0); - int status = ir.call(J, R.data(), n, b.data(), m, x_ir.data(), n); - EXPECT_EQ(status, 0); - - // Compare: x_ir vs x_ref, both solving the same regularized normal equations. - T diff_norm = 0; - for (int64_t i = 0; i < n; ++i) { - T d = x_ir[i] - x_ref[i]; - diff_norm += d * d; - } - diff_norm = std::sqrt(diff_norm); - T xref_norm = blas::nrm2(n, x_ref.data(), 1); - EXPECT_LT(diff_norm / xref_norm, 1e-10); -} - - // Imperfect R: from QR of a slightly perturbed A. CG should take more iters // but the final solution must still match gels. TEST_F(TestIterRefineLSQ, imperfect_preconditioner) { diff --git a/test/linops/test_kronecker_linop.cc b/test/linops/test_kronecker_linop.cc deleted file mode 100644 index 46753e0c6..000000000 --- a/test/linops/test_kronecker_linop.cc +++ /dev/null @@ -1,185 +0,0 @@ -// Unit tests for KroneckerOperator: -// forward / adjoint dense apply (single column, multi-column), -// sparse SASO sketching (S * A) via the SkOp overload. -// All tests verify against an explicitly-materialized A = kron(A2, A1). - -#include -#include -#include - -#include -#include - - -using RandLAPACK::linops::KroneckerOperator; -using RandLAPACK::linops::DenseLinOp; -using blas::Layout; -using blas::Op; -using blas::Side; - - -// Build A = kron(A2, A1) explicitly in ColMajor as an (m1*m2) × (n1*n2) buffer. -template -static void materialize_kron(int64_t m1, int64_t n1, int64_t m2, int64_t n2, - const T* A1, const T* A2, T* A_out) -{ - int64_t M = m1 * m2; - int64_t N = n1 * n2; - for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { - for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { - int64_t j = j_outer * n1 + j_inner; - for (int64_t i_outer = 0; i_outer < m2; ++i_outer) { - T a2 = A2[i_outer + j_outer * m2]; - for (int64_t i_inner = 0; i_inner < m1; ++i_inner) { - int64_t i = i_outer * m1 + i_inner; - A_out[i + j * M] = a2 * A1[i_inner + j_inner * m1]; - } - } - } - } - (void)M; (void)N; -} - - -template -static void fill_random(std::vector& v, uint32_t seed) { - std::mt19937 rng(seed); - std::uniform_real_distribution dist(-1.0, 1.0); - for (auto& x : v) x = dist(rng); -} - - -template -static T frobenius_diff(const std::vector& a, const std::vector& b) { - T sum = 0; - for (size_t i = 0; i < a.size(); ++i) { - T d = a[i] - b[i]; - sum += d * d; - } - return std::sqrt(sum); -} - - -class TestKroneckerOperator : public ::testing::Test {}; - - -// Test forward apply (NoTrans) and adjoint (Trans) for single-column input. -TEST_F(TestKroneckerOperator, forward_and_adjoint_single_column) { - using T = double; - int64_t m1 = 4, n1 = 3, m2 = 5, n2 = 2; - int64_t M = m1 * m2; // 20 - int64_t N = n1 * n2; // 6 - - std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); - fill_random(A1, 11); - fill_random(A2, 22); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - - KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); - DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); - - // forward: y = A x - { - std::vector x(N), y_kop(M, 0), y_dense(M, 0); - fill_random(x, 33); - kop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M, 1, N, (T)1.0, x.data(), N, (T)0.0, y_kop.data(), M); - dop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M, 1, N, (T)1.0, x.data(), N, (T)0.0, y_dense.data(), M); - EXPECT_LT(frobenius_diff(y_kop, y_dense), 1e-12); - } - // adjoint: z = A^T y - { - std::vector y(M), z_kop(N, 0), z_dense(N, 0); - fill_random(y, 44); - kop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, 1, M, (T)1.0, y.data(), M, (T)0.0, z_kop.data(), N); - dop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, 1, M, (T)1.0, y.data(), M, (T)0.0, z_dense.data(), N); - EXPECT_LT(frobenius_diff(z_kop, z_dense), 1e-12); - } -} - - -// Test alpha and beta correctness on a multi-column input. -TEST_F(TestKroneckerOperator, alpha_beta_multi_column) { - using T = double; - int64_t m1 = 3, n1 = 2, m2 = 4, n2 = 3; - int64_t M = m1 * m2, N = n1 * n2; - int64_t k = 5; - - std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); - fill_random(A1, 51); - fill_random(A2, 52); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - - KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); - DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); - - // forward: B is N × k, C accumulator nonzero - { - std::vector B(N * k), C_kop(M * k), C_dense(M * k); - fill_random(B, 61); - fill_random(C_kop, 71); - C_dense = C_kop; - T alpha = 0.7, beta = -0.3; - kop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M, k, N, alpha, B.data(), N, beta, C_kop.data(), M); - dop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M, k, N, alpha, B.data(), N, beta, C_dense.data(), M); - EXPECT_LT(frobenius_diff(C_kop, C_dense), 1e-12); - } - // adjoint: B is M × k, C accumulator nonzero - { - std::vector B(M * k), C_kop(N * k), C_dense(N * k); - fill_random(B, 81); - fill_random(C_kop, 91); - C_dense = C_kop; - T alpha = 1.3, beta = 0.2; - kop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, k, M, alpha, B.data(), M, beta, C_kop.data(), N); - dop(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, k, M, alpha, B.data(), M, beta, C_dense.data(), N); - EXPECT_LT(frobenius_diff(C_kop, C_dense), 1e-12); - } -} - - -// Test sparse-SASO sketch: A_hat = S * A. -// The SkOp overload should match what DenseLinOp produces on the materialized A. -TEST_F(TestKroneckerOperator, sparse_sketch_apply) { - using T = double; - using RNG = r123::Philox4x32; - int64_t m1 = 6, n1 = 4, m2 = 5, n2 = 3; - int64_t M = m1 * m2; // 30 - int64_t N = n1 * n2; // 12 - int64_t d = 8; - int64_t saso_nnz = 3; - - std::vector A1(m1 * n1), A2(m2 * n2), A_full(M * N); - fill_random(A1, 101); - fill_random(A2, 102); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - - KroneckerOperator kop(m1, n1, m2, n2, A1.data(), A2.data()); - DenseLinOp dop(M, N, A_full.data(), M, Layout::ColMajor); - - // Build a sparse SASO sketch and apply via both linops. - RandBLAS::RNGState state(42); - RandBLAS::SparseDist DS(d, M, saso_nnz); - RandBLAS::SparseSkOp S(DS, state); - RandBLAS::fill_sparse(S); - auto state2 = state; - RandBLAS::SparseSkOp S2(DS, state2); - RandBLAS::fill_sparse(S2); - - std::vector A_hat_kop(d * N, 0); - std::vector A_hat_dense(d * N, 0); - - kop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, N, M, (T)1.0, S, (T)0.0, A_hat_kop.data(), d); - dop(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, N, M, (T)1.0, S2, (T)0.0, A_hat_dense.data(), d); - - EXPECT_LT(frobenius_diff(A_hat_kop, A_hat_dense), 1e-12); -} diff --git a/test/linops/test_regularized_linop.cc b/test/linops/test_regularized_linop.cc deleted file mode 100644 index fe89c2323..000000000 --- a/test/linops/test_regularized_linop.cc +++ /dev/null @@ -1,221 +0,0 @@ -// Unit tests for RegularizedLinOp: -// verifies that A_aug = [J; λI] applies correctly for forward, adjoint, -// and sparse-SASO sketch paths, by comparing against an explicitly-built -// (m+n) × n dense matrix. - -#include -#include -#include - -#include -#include - - -using RandLAPACK::linops::KroneckerOperator; -using RandLAPACK::linops::RegularizedLinOp; -using RandLAPACK::linops::DenseLinOp; -using blas::Layout; -using blas::Op; -using blas::Side; - - -// Build A = kron(A2, A1) explicitly in ColMajor. -template -static void materialize_kron(int64_t m1, int64_t n1, int64_t m2, int64_t n2, - const T* A1, const T* A2, T* A_out) -{ - int64_t M = m1 * m2; - for (int64_t j_outer = 0; j_outer < n2; ++j_outer) { - for (int64_t j_inner = 0; j_inner < n1; ++j_inner) { - int64_t j = j_outer * n1 + j_inner; - for (int64_t i_outer = 0; i_outer < m2; ++i_outer) { - T a2 = A2[i_outer + j_outer * m2]; - for (int64_t i_inner = 0; i_inner < m1; ++i_inner) { - int64_t i = i_outer * m1 + i_inner; - A_out[i + j * M] = a2 * A1[i_inner + j_inner * m1]; - } - } - } - } -} - - -// Build A_aug = [A; λI] explicitly in ColMajor. -template -static void materialize_augmented(int64_t M, int64_t N, T lambda, - const T* A, T* A_aug) -{ - int64_t M_aug = M + N; - // Top: copy A into rows [0..M). - for (int64_t j = 0; j < N; ++j) { - for (int64_t i = 0; i < M; ++i) { - A_aug[i + j * M_aug] = A[i + j * M]; - } - // Bottom: λI in rows [M..M+N). - for (int64_t i = 0; i < N; ++i) { - A_aug[(M + i) + j * M_aug] = (i == j) ? lambda : (T)0; - } - } -} - - -template -static void fill_random(std::vector& v, uint32_t seed) { - std::mt19937 rng(seed); - std::uniform_real_distribution dist(-1.0, 1.0); - for (auto& x : v) x = dist(rng); -} - - -template -static T frobenius_diff(const std::vector& a, const std::vector& b) { - T sum = 0; - for (size_t i = 0; i < a.size(); ++i) { - T d = a[i] - b[i]; - sum += d * d; - } - return std::sqrt(sum); -} - - -class TestRegularizedLinOp : public ::testing::Test {}; - - -// Forward + adjoint single column on a Kronecker-backed RegularizedLinOp -// must match the explicitly-materialized A_aug = [kron(A2,A1); λI]. -TEST_F(TestRegularizedLinOp, kron_forward_adjoint_single_column) { - using T = double; - int64_t m1 = 4, n1 = 3, m2 = 5, n2 = 2; - int64_t M = m1 * m2; - int64_t N = n1 * n2; - int64_t M_aug = M + N; - T lambda = 0.7; - - std::vector A1(m1 * n1), A2(m2 * n2); - fill_random(A1, 11); - fill_random(A2, 22); - - std::vector A_full(M * N), A_aug_full(M_aug * N); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); - - KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); - RegularizedLinOp> J_aug(J, lambda); - DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); - - // Forward: y = A_aug x - { - std::vector x(N), y_aug(M_aug, 0), y_dense(M_aug, 0); - fill_random(x, 33); - J_aug(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M_aug, 1, N, (T)1.0, x.data(), N, (T)0.0, y_aug.data(), M_aug); - A_aug_dense(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M_aug, 1, N, (T)1.0, x.data(), N, (T)0.0, y_dense.data(), M_aug); - EXPECT_LT(frobenius_diff(y_aug, y_dense), 1e-12); - } - // Adjoint: z = A_aug^T y - { - std::vector y(M_aug), z_aug(N, 0), z_dense(N, 0); - fill_random(y, 44); - J_aug(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, 1, M_aug, (T)1.0, y.data(), M_aug, (T)0.0, z_aug.data(), N); - A_aug_dense(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, 1, M_aug, (T)1.0, y.data(), M_aug, (T)0.0, z_dense.data(), N); - EXPECT_LT(frobenius_diff(z_aug, z_dense), 1e-12); - } -} - - -// alpha/beta + multi-column on forward and adjoint. -TEST_F(TestRegularizedLinOp, kron_alpha_beta_multi_column) { - using T = double; - int64_t m1 = 3, n1 = 2, m2 = 4, n2 = 3; - int64_t M = m1 * m2; - int64_t N = n1 * n2; - int64_t M_aug = M + N; - int64_t k = 5; - T lambda = 0.4; - - std::vector A1(m1 * n1), A2(m2 * n2); - fill_random(A1, 51); - fill_random(A2, 52); - std::vector A_full(M * N), A_aug_full(M_aug * N); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); - - KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); - RegularizedLinOp> J_aug(J, lambda); - DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); - - // Forward with non-trivial alpha/beta. - { - std::vector B(N * k), C_aug(M_aug * k), C_dense(M_aug * k); - fill_random(B, 61); - fill_random(C_aug, 71); - C_dense = C_aug; - T alpha = 0.7, beta = -0.3; - J_aug(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M_aug, k, N, alpha, B.data(), N, beta, C_aug.data(), M_aug); - A_aug_dense(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - M_aug, k, N, alpha, B.data(), N, beta, C_dense.data(), M_aug); - EXPECT_LT(frobenius_diff(C_aug, C_dense), 1e-11); - } - // Adjoint with non-trivial alpha/beta. - { - std::vector B(M_aug * k), C_aug(N * k), C_dense(N * k); - fill_random(B, 81); - fill_random(C_aug, 91); - C_dense = C_aug; - T alpha = 1.3, beta = 0.2; - J_aug(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, k, M_aug, alpha, B.data(), M_aug, beta, C_aug.data(), N); - A_aug_dense(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - N, k, M_aug, alpha, B.data(), M_aug, beta, C_dense.data(), N); - EXPECT_LT(frobenius_diff(C_aug, C_dense), 1e-11); - } -} - - -// Sparse SASO sketch via the COO-filter dispatch path: -// J_aug(Side::Right, NoTrans, NoTrans, S, ...) must match DenseLinOp on the -// materialized A_aug. -TEST_F(TestRegularizedLinOp, sparse_sketch_apply) { - using T = double; - using RNG = r123::Philox4x32; - int64_t m1 = 6, n1 = 4, m2 = 5, n2 = 3; - int64_t M = m1 * m2; - int64_t N = n1 * n2; - int64_t M_aug = M + N; - int64_t d = 8; - int64_t saso_nnz = 3; - T lambda = 0.6; - - std::vector A1(m1 * n1), A2(m2 * n2); - fill_random(A1, 101); - fill_random(A2, 102); - std::vector A_full(M * N), A_aug_full(M_aug * N); - materialize_kron(m1, n1, m2, n2, A1.data(), A2.data(), A_full.data()); - materialize_augmented(M, N, lambda, A_full.data(), A_aug_full.data()); - - KroneckerOperator J(m1, n1, m2, n2, A1.data(), A2.data()); - RegularizedLinOp> J_aug(J, lambda); - DenseLinOp A_aug_dense(M_aug, N, A_aug_full.data(), M_aug, Layout::ColMajor); - - RandBLAS::RNGState state(42); - RandBLAS::SparseDist DS(d, M_aug, saso_nnz); - RandBLAS::SparseSkOp S(DS, state); - RandBLAS::fill_sparse(S); - auto state2 = state; - RandBLAS::SparseSkOp S2(DS, state2); - RandBLAS::fill_sparse(S2); - - std::vector A_hat_aug(d * N, 0); - std::vector A_hat_dense(d * N, 0); - - J_aug(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, N, M_aug, (T)1.0, S, (T)0.0, A_hat_aug.data(), d); - A_aug_dense(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, N, M_aug, (T)1.0, S2, (T)0.0, A_hat_dense.data(), d); - - EXPECT_LT(frobenius_diff(A_hat_aug, A_hat_dense), 1e-12); -} From e9e9ee162a471a0d6bb2d326a9ac7701318592ed Mon Sep 17 00:00:00 2001 From: mmelnich Date: Thu, 28 May 2026 09:00:22 -0700 Subject: [PATCH 18/47] GSVD benchmark: rewrite FEM mode for J = L^{-1} * K * V composite Per collaborator's correction (Oleg, 2026-05-25), the FEM2 operator is the Petrov-Galerkin form J = B^{-1} * A * U_h with B = chol(M), A = K, U_h = V (mass-matrix Cholesky factor, stiffness, prolongation respectively). CQRRT_linop_applications now takes three .mtx files in FEM mode and builds a doubly-nested CompositeOperator: J = CompositeOperator(L_inv_op, CompositeOperator(K_op, V_op)) with L_inv_op = CholSolverLinOp(M_file, half_solve=true). Because L factors M (not K), the composite does NOT algebraically collapse; the LS normal equations land on J' J = V' K M^{-1} K V (Petrov-Galerkin coarse- grid mass-weighted stiffness-squared). CLI changes: FEM mode (NEW): [...] (3 files) Sparse mode: sparse [...] (unchanged) The previous 2-stage composite (L^{-1} * V with L = chol(K)) is gone -- that formulation was based on a misread of the generator's outputs. The sparse mode is unchanged. No driver, linop, test, or CMake changes. --- .../CQRRT_linop_applications.cc | 194 ++++++++++++------ 1 file changed, 131 insertions(+), 63 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 582935ad4..54b463f0b 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -1,18 +1,22 @@ // Generalized SVD benchmark // // Pipeline: -// 1. Load K.mtx (m x m SPD) and V.mtx (m x n sparse) (composite mode) -// OR a single A.mtx as a SparseLinOp (sparse mode) -// 2. (Composite only) Cholesky-factorize K = LL^T, create L^{-1} operator -// 3. (Composite only) Form CompositeOperator(L_inv_op, V_op) = L^{-1}V -// 4. Run Q-less QR on the operator via CQRRT_linops (TRSM_IDENTITY preconditioner) -// 5. SVD post-processing: gesdd of R for generalized singular values + vectors -// -// (For LS post-processing on Kronecker-structured operators see CQRRT_linop_nmr.) +// 1. Load matrices (FEM mode: K, M, V .mtx files; sparse mode: a single A.mtx). +// 2. (FEM only) Cholesky-factorize M = L L^T via CholSolverLinOp(half_solve=true). +// 3. (FEM only) Build J = L^{-1} K V as a doubly-nested CompositeOperator +// J = CompositeOperator(L_inv_op, CompositeOperator(K_op, V_op)). +// This is the Petrov-Galerkin form B^{-1} A U_h with B = chol(M), A = K, U_h = V. +// Because L factors M (not K), the composite does NOT algebraically collapse; +// the LS normal equations land on J^T J = V^T K M^{-1} K V (Galerkin coarse-grid +// mass-weighted stiffness-squared). +// 4. Run Q-less QR on the operator via CQRRT_linops (TRSM_IDENTITY preconditioner). +// 5. SVD post-processing: gesdd of R for generalized singular values + vectors. // // Usage: -// ./CQRRT_linop_applications -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] +// ./CQRRT_linop_applications +// sparse [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] +// ./CQRRT_linop_applications +// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] #include "RandLAPACK.hh" #include "rl_blaspp.hh" @@ -606,104 +610,168 @@ static int run_benchmark_inner( template int run_benchmark(int argc, char* argv[]) { + // Sparse mode: sparse [...] (argc >= 7) + // FEM mode: [...] (argc >= 8) + // + // FEM operator is the Petrov-Galerkin form J = L^{-1} * K * V, where L = chol(M) + // (mass-matrix factor applied via half-solve). Built as a doubly-nested + // CompositeOperator: + // J = CompositeOperator(L_inv_op, CompositeOperator(K_op, V_op)) if (argc < 7) { std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" - << " sparse mode: pass 'sparse' as K_file and a single A.mtx as V_file\n"; + << " \n" + << " sparse mode: 'sparse' [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" + << " FEM mode: [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; return 1; } std::string output_dir = argv[2]; int64_t num_runs = std::stol(argv[3]); - std::string K_file = argv[4]; - std::string V_file = argv[5]; - T d_factor = std::stod(argv[6]); - int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; - int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; - bool skip_apps = (argc >= 10) ? (std::stol(argv[9]) != 0) : false; - bool compute_cond = (argc >= 11) ? (std::stol(argv[10]) != 0) : false; - bool upcast_orth = (argc >= 12) ? (std::stol(argv[11]) != 0) : false; + std::string arg4 = argv[4]; + bool sparse_mode = (arg4 == "sparse"); + + std::string K_file, M_file, V_file, A_file; + T d_factor; + int dfactor_idx; // index of d_factor in argv; subsequent optional args follow + + if (sparse_mode) { + if (argc < 7) { + std::cerr << "Error: sparse mode needs \n"; + return 1; + } + A_file = argv[5]; + d_factor = std::stod(argv[6]); + dfactor_idx = 6; + } else { + if (argc < 8) { + std::cerr << "Error: FEM mode needs \n"; + return 1; + } + K_file = arg4; + M_file = argv[5]; + V_file = argv[6]; + d_factor = std::stod(argv[7]); + dfactor_idx = 7; + } - bool sparse_mode = (K_file == "sparse"); + auto opt_long = [&](int rel, int64_t def) { + int idx = dfactor_idx + rel; + return (argc > idx) ? std::stol(argv[idx]) : def; + }; + int64_t sketch_nnz = opt_long(1, 4); + int64_t block_size = opt_long(2, 0); + bool skip_apps = (opt_long(3, 0) != 0); + bool compute_cond = (opt_long(4, 0) != 0); + bool upcast_orth = (opt_long(5, 0) != 0); std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; if (sparse_mode) { - std::cout << " Mode: sparse (single-matrix SparseLinOp)\n"; - std::cout << " A file: " << V_file << "\n"; + std::cout << " Mode: sparse (single-matrix SparseLinOp)\n" + << " A file: " << A_file << "\n"; } else { - std::cout << " Mode: composite (L^{-1}V)\n"; - std::cout << " K file: " << K_file << "\n"; - std::cout << " V file: " << V_file << "\n"; + std::cout << " Mode: FEM composite (J = L^{-1} K V with L = chol(M))\n" + << " K file: " << K_file << "\n" + << " M file: " << M_file << "\n" + << " V file: " << V_file << "\n"; } - std::cout << " d_factor: " << d_factor << "\n"; - std::cout << " sketch_nnz: " << sketch_nnz << "\n"; - std::cout << " block_size: " << block_size << "\n"; - std::cout << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n"; - std::cout << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n"; - std::cout << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n"; - std::cout << " num_runs: " << num_runs << "\n"; + std::cout << " d_factor: " << d_factor << "\n" + << " sketch_nnz: " << sketch_nnz << "\n" + << " block_size: " << block_size << "\n" + << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n" + << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n" + << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n" + << " num_runs: " << num_runs << "\n" #ifdef _OPENMP - std::cout << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; + << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; #else - std::cout << " OpenMP threads: 1\n\n"; + << " OpenMP threads: 1\n\n"; #endif - // ================================================================ - // Step 1: Load A (sparse mode) or V (composite mode) from Matrix Market - // ================================================================ - std::cout << "Loading " << (sparse_mode ? "A" : "V") << " from " << V_file << "... " << std::flush; - int64_t m, n, nnz_V; - auto V_csr = load_csr(V_file, m, n, nnz_V); - RandLAPACK::linops::SparseLinOp> V_linop(m, n, V_csr); - std::cout << "done (" << m << " x " << n << ", nnz=" << nnz_V << ")\n"; - - if (m < n) { - std::cerr << "Error: matrix must be overdetermined (m >= n), got " << m << "x" << n << "\n"; - return 1; - } - // ================================================================ // Sparse mode: wrap A directly as SparseLinOp, skip Cholesky // ================================================================ if (sparse_mode) { + std::cout << "Loading A from " << A_file << "... " << std::flush; + int64_t m, n, nnz_A; + auto A_csr = load_csr(A_file, m, n, nnz_A); + RandLAPACK::linops::SparseLinOp> A_linop(m, n, A_csr); + std::cout << "done (" << m << " x " << n << ", nnz=" << nnz_A << ")\n"; + + if (m < n) { + std::cerr << "Error: matrix must be overdetermined (m >= n), got " << m << "x" << n << "\n"; + return 1; + } return run_benchmark_inner( - V_linop, m, n, output_dir, num_runs, + A_linop, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, skip_apps, compute_cond, upcast_orth, - 0L /*chol_time_us*/, "sparse", V_file, - "A (" + V_file + ")"); + 0L /*chol_time_us*/, "sparse", A_file, + "A (" + A_file + ")"); } // ================================================================ - // Composite mode: Cholesky-factorize K, build L^{-1}V composite operator + // FEM mode: load K, V; Cholesky-factorize M; build J = L^{-1} * K * V + // as a doubly-nested CompositeOperator. // ================================================================ - std::cout << "Factorizing K = LL^T from " << K_file << "... " << std::flush; + std::cout << "Loading K (stiffness) from " << K_file << "... " << std::flush; + int64_t m_K, n_K, nnz_K; + auto K_csr = load_csr(K_file, m_K, n_K, nnz_K); + std::cout << "done (" << m_K << " x " << n_K << ", nnz=" << nnz_K << ")\n"; + if (m_K != n_K) { + std::cerr << "Error: K must be square; got " << m_K << " x " << n_K << "\n"; + return 1; + } + RandLAPACK::linops::SparseLinOp> K_op(m_K, m_K, K_csr); + + std::cout << "Loading V (prolongation) from " << V_file << "... " << std::flush; + int64_t m_V, n_V, nnz_V; + auto V_csr = load_csr(V_file, m_V, n_V, nnz_V); + std::cout << "done (" << m_V << " x " << n_V << ", nnz=" << nnz_V << ")\n"; + if (m_V != m_K) { + std::cerr << "Error: V row count (" << m_V << ") must match K size (" << m_K << ")\n"; + return 1; + } + if (m_V < n_V) { + std::cerr << "Error: need tall V (m_fine >= n_coarse); got " << m_V << " x " << n_V << "\n"; + return 1; + } + RandLAPACK::linops::SparseLinOp> V_op(m_V, n_V, V_csr); - RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(K_file, /*half_solve=*/true); + std::cout << "Factorizing M = L L^T from " << M_file << "... " << std::flush; + RandLAPACK_extras::linops::CholSolverLinOp L_inv_op(M_file, /*half_solve=*/true); auto chol_start = steady_clock::now(); L_inv_op.factorize(); auto chol_stop = steady_clock::now(); long chol_time_us = duration_cast(chol_stop - chol_start).count(); std::cout << "done (" << chol_time_us << " us)\n"; + if (L_inv_op.n_rows != m_K) { + std::cerr << "Error: M size (" << L_inv_op.n_rows << ") must match K size (" + << m_K << ")\n"; + return 1; + } - RandLAPACK::linops::CompositeOperator LiV_op(m, n, L_inv_op, V_linop); - LiV_op.block_size = block_size; - std::cout << "Composite operator L^{-1}V: " << m << " x " << n << "\n\n"; + int64_t m = m_V; + int64_t n = n_V; + RandLAPACK::linops::CompositeOperator KV_op(m, n, K_op, V_op); + KV_op.block_size = block_size; + RandLAPACK::linops::CompositeOperator J_op(m, n, L_inv_op, KV_op); + J_op.block_size = block_size; + std::cout << "Composite operator J = L^{-1} K V : " << m << " x " << n << "\n\n"; return run_benchmark_inner( - LiV_op, m, n, output_dir, num_runs, + J_op, m, n, output_dir, num_runs, d_factor, sketch_nnz, block_size, skip_apps, compute_cond, upcast_orth, chol_time_us, K_file, V_file, - "L^{-1}V"); + "L^{-1} K V (M=" + M_file + ")"); } int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] - << " " - << " [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; + << " \n" + << " sparse mode: 'sparse' [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" + << " FEM mode: [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; return 1; } From 9c7b19f78539a4a96e5e3b3585173b19a6504461 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 1 Jun 2026 10:33:37 -0700 Subject: [PATCH 19/47] Merge IR-LSQ into CQRRT_linop_applications; add Higham residual metric - Single binary now selects between SVD post-processing, IR-LSQ refinement, or both via a positional arg. - 5-method dispatch (CQRRT_linop, CholQR, sCholQR3, sCholQR3_basic, CQRRT_linop_bqrrp) restored to the FEM/sparse composite paths. - Add power-iteration estimate of ||A||_2 and replace ls_residual_norm with the Higham normwise backward error ||Ax-b||/(||A||*||x||+||b||), drivable to machine epsilon for a backward-stable LS solver. - Add memlite blocked-compute orth_err (O(n^2 + m*b)), runs for every selected method in every mode; new orth_error column in irlsq CSV. - FEM + irlsq: b = L^{-1} * Gaussian random vector (no x_true). - CQRRT_linop_irlsq.cc removed (CMake target dropped). --- benchmark/CMakeLists.txt | 2 - .../CQRRT_linop_applications.cc | 913 ++++++++++++------ .../bench_CQRRT_linops/CQRRT_linop_irlsq.cc | 467 --------- 3 files changed, 614 insertions(+), 768 deletions(-) delete mode 100644 benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index b2c0decbf..2d5be5054 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -159,8 +159,6 @@ set( add_benchmark(NAME CQRRT_linop_applications CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_applications.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_linop_basic CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_basic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) add_benchmark(NAME CQRRT_diagnostic CXX_SOURCES bench_CQRRT_linops/CQRRT_diagnostic.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) -# Sparse-input IR-LSQ benchmark: Q-less QR + sketch-and-solve x0 + 2-step IR. -add_benchmark(NAME CQRRT_linop_irlsq CXX_SOURCES bench_CQRRT_linops/CQRRT_linop_irlsq.cc LINK_LIBS ${Benchmark_libs_cqrrt_linops}) # ABRIK benchmarks add_benchmark(NAME ABRIK_runtime_breakdown CXX_SOURCES bench_ABRIK/ABRIK_runtime_breakdown.cc LINK_LIBS ${Benchmark_libs}) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 54b463f0b..b421a2a8d 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -1,22 +1,30 @@ -// Generalized SVD benchmark +// Unified Q-less QR benchmark — GSVD post-processing and/or IR-LSQ. // // Pipeline: // 1. Load matrices (FEM mode: K, M, V .mtx files; sparse mode: a single A.mtx). // 2. (FEM only) Cholesky-factorize M = L L^T via CholSolverLinOp(half_solve=true). // 3. (FEM only) Build J = L^{-1} K V as a doubly-nested CompositeOperator // J = CompositeOperator(L_inv_op, CompositeOperator(K_op, V_op)). -// This is the Petrov-Galerkin form B^{-1} A U_h with B = chol(M), A = K, U_h = V. -// Because L factors M (not K), the composite does NOT algebraically collapse; -// the LS normal equations land on J^T J = V^T K M^{-1} K V (Galerkin coarse-grid -// mass-weighted stiffness-squared). -// 4. Run Q-less QR on the operator via CQRRT_linops (TRSM_IDENTITY preconditioner). -// 5. SVD post-processing: gesdd of R for generalized singular values + vectors. +// 4. Run Q-less QR via one of 5 variants (CQRRT_linop, CholQR, sCholQR3, +// sCholQR3_basic, CQRRT_linop_bqrrp), selected by method_mask. +// 5. Post-processing dictated by : +// svd — orth_err + r_backward_error + gesdd(R) for sing. values/vectors +// irlsq — sketch-and-solve x_0 (Epperly–Meier–Nakatsukasa 2025 Alg. 1 line 3) + IterRefineLSQ +// both — both, sharing the same QR step and timestamp // // Usage: -// ./CQRRT_linop_applications -// sparse [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] -// ./CQRRT_linop_applications -// [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth] +// ./CQRRT_linop_applications +// sparse [nnz] [b] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level] +// ./CQRRT_linop_applications +// [nnz] [b] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level] +// +// mode = "svd" | "irlsq" | "both" +// method_mask = bitmask of Q-less QR variants (default 0b1001111 = 79) +// bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) +// bit 1 ( 2): CholQR +// bit 2 ( 4): sCholQR3 +// bit 3 ( 8): sCholQR3_basic +// bit 6 (64): CQRRT_linop_bqrrp (BQRRP) #include "RandLAPACK.hh" #include "rl_blaspp.hh" @@ -38,6 +46,7 @@ #include #include #include +#include // Extras utilities (Eigen-dependent) #include "../../extras/misc/ext_util.hh" @@ -45,7 +54,7 @@ #include "RandLAPACK/testing/rl_test_utils.hh" #include "cqrrt_bench_common.hh" -// Linops algorithms (now in main RandLAPACK) +// Linops algorithms #include "rl_cqrrt_linops.hh" #include "rl_cholqr_linops.hh" #include "rl_scholqr3_linops.hh" @@ -56,48 +65,44 @@ using std::chrono::duration_cast; using std::chrono::microseconds; // ============================================================================ -// Result struct +// Result struct (unified — sentinel values for fields irrelevant to the mode) // ============================================================================ template -struct gsvd_result { +struct bench_result { int64_t m, n; int64_t run_idx; std::string alg_name; + T noise_level; - // Cholesky factorization time (shared, measured once) - long chol_time_us; - - // Q-less QR time - long qr_time_us; + long chol_time_us; // FEM: shared, measured once. Sparse: 0. + int qr_status; // 0 on success + long qr_time_us; // -1 if QR failed - // Orthogonality of Q = (L^{-1}V) R^{-1} + // SVD-mode fields (sentinel -1 / unset when not computed) T orth_error; bool is_orthonormal; - - // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 double r_backward_error; - - // Orthogonality computed with upcast reconstruction (float->double or double->long double) double orth_error_upcast; - - // SVD post-processing (b): generalized singular values - long app_b_time_us; // SVD of R time - - // SVD post-processing (c): generalized singular vectors - long app_c_time_us; // Full SVD of R + V_R orthogonality check - T right_svec_orth_error; // ||V_R^T V_R - I||_F / sqrt(n) - - // Totals (QR + application post-processing) + long app_b_time_us; + long app_c_time_us; + T right_svec_orth_error; long total_b_time_us; long total_c_time_us; + // IR-LSQ-mode fields + long ir_total_us; + int ir_outer_iters; + int ir_inner_iters_total; + T ls_residual_norm; + T ls_solution_error; // -1 sentinel when undefined (FEM irlsq) + // QR timing breakdown (from algo.times[]) std::vector qr_breakdown; + std::vector ir_breakdown; - // Memory tracking - long peak_rss_kb; // Peak RSS increase during QR call (KB) - long analytical_kb; // Analytical peak working memory (KB) + long peak_rss_kb; + long analytical_kb; }; // Compute Q = A * R^{-1} uniformly for all algorithms @@ -113,57 +118,46 @@ static void compute_Q_from_R( } // Compute A^T A via blocked linop calls. Peak memory: O(m*b + n^2). -// No m x n materialization needed. template static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { std::fill(AtA, AtA + n * n, (T)0.0); - std::vector E_block(n * b, 0.0); // n x b identity block - std::vector A_block(m * b, 0.0); // m x b = A * E_block - std::vector AtA_block(n * b, 0.0); // n x b = A^T * A_block + std::vector E_block(n * b, 0.0); + std::vector A_block(m * b, 0.0); + std::vector AtA_block(n * b, 0.0); for (int64_t j0 = 0; j0 < n; j0 += b) { int64_t bk = std::min(b, n - j0); - // E_block = I[:, j0:j0+bk] std::fill(E_block.begin(), E_block.end(), (T)0.0); for (int64_t j = 0; j < bk; ++j) E_block[(j0 + j) + j * n] = (T)1.0; - // A_block = A * E_block (m x bk) A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, m, bk, n, (T)1.0, E_block.data(), n, (T)0.0, A_block.data(), m); - // AtA[:, j0:j0+bk] = A^T * A_block (n x bk) A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); - // Copy into AtA columns j0..j0+bk lapack::lacpy(lapack::MatrixType::General, n, bk, AtA_block.data(), n, AtA + j0 * n, n); } } -// Compute R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 -// Uses blocked linop calls — peak memory O(m*b + n^2), no m x n materialization. template static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { int64_t b = (block_size > 0) ? block_size : 256; - // A^T A via blocked linop (n x n) std::vector AtA(n * n, 0.0); compute_AtA_blocked(A_op, m, n, AtA.data(), b); - // R^T R (n x n) std::vector RtR(n * n, 0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); fill_lower_from_upper(RtR.data(), n); - // ||A||_F^2 = trace(A^T A) T norm_A_sq = 0; for (int64_t i = 0; i < n; ++i) norm_A_sq += AtA[i + i * n]; - // ||A^T A - R^T R||_F T diff_norm_sq = 0; #pragma omp parallel for reduction(+:diff_norm_sq) schedule(static) for (int64_t i = 0; i < n * n; ++i) { @@ -174,11 +168,7 @@ static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t return (double)(std::sqrt(diff_norm_sq) / (norm_A_sq)); } -// Compute orthogonality with upcast reconstruction. -// Algorithm runs in precision T, Q = A R^{-1} computed in precision U (one level up). -// float -> double: uses BLAS++/LAPACK++ (MKL-backed DTRSM + DSYRK + DLANSY). -// double -> long double: uses Eigen (BLAS++ does not support long double). -// If A_dense is non-null, uses it directly; otherwise materializes via linop. +// Upcast orthogonality: float->double via BLAS++; double->long double via Eigen. template static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, const T* A_dense = nullptr) { @@ -194,7 +184,6 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, A_ptr = A_buf.data(); } - // Upcast A and R to precision U std::vector A_U(m * n), R_U(n * n); #pragma omp parallel for schedule(static) for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; @@ -202,27 +191,21 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; if constexpr (std::is_same_v) { - // float->double: BLAS++ path — MKL DTRSM + DSYRK + DLANSY. - // Solve in-place: A_U = A_U * R_U^{-1} (becomes Q in double). blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, m, n, 1.0, R_U.data(), n, A_U.data(), m); - // GmI = Q^T Q - I (upper triangle via SYRK, then lansy for Frobenius norm) std::vector GmI(n * n, 0.0); RandLAPACK::util::eye(n, n, GmI.data()); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, m, 1.0, A_U.data(), m, -1.0, GmI.data(), n); return lapack::lansy(lapack::Norm::Fro, blas::Uplo::Upper, n, GmI.data(), n) / std::sqrt((double)n); } else { - // double->long double: Eigen path (BLAS++ does not support long double). Eigen::Map> A_map(A_U.data(), m, n); Eigen::Map> R_map(R_U.data(), n, n); - // Solve R^T * Q^T = A^T for Q^T Eigen::Matrix Qt = R_map.transpose().template triangularView().solve(A_map.transpose()); - // ||Q^T Q - I||_F / sqrt(n) Eigen::Matrix QtQ = Qt * Qt.transpose(); for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; return (double)(QtQ.norm() / std::sqrt((U)n)); @@ -230,69 +213,88 @@ static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, } // ============================================================================ -// SVD post-processing (b): Generalized Singular Values -// SVD of R gives the generalized singular values of (V, K) +// SVD post-processing // ============================================================================ template static void app_generalized_svals( const T* R, int64_t ldr, int64_t n, - T* sigma, - long& app_time_us) + T* sigma, long& app_time_us) { auto start = steady_clock::now(); - - // Copy R to work buffer (gesdd destroys input) std::vector R_copy(n * n); lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - - // SVD of n x n R: only singular values (no vectors) std::vector dummy_U(1), dummy_Vt(1); lapack::gesdd(lapack::Job::NoVec, n, n, R_copy.data(), n, sigma, dummy_U.data(), 1, dummy_Vt.data(), 1); - auto stop = steady_clock::now(); app_time_us = duration_cast(stop - start).count(); } -// ============================================================================ -// SVD post-processing (c): Generalized Singular Vectors -// Full SVD of R: R = U_R * Sigma * V_R^T -// Right generalized singular vectors = columns of V_R -// ============================================================================ - template static void app_generalized_svecs( const T* R, int64_t ldr, int64_t n, - T* sigma, - T* V_R, // n x n right singular vectors - T* U_R, // n x n left singular vectors of R - T& right_svec_orth_error, - long& app_time_us) + T* sigma, T* V_R, T* U_R, + T& right_svec_orth_error, long& app_time_us) { auto start = steady_clock::now(); - - // Copy R to work buffer std::vector R_copy(n * n); lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - - // Full SVD of n x n R: R = U_R * Sigma * V_R^T lapack::gesdd(lapack::Job::AllVec, n, n, R_copy.data(), n, sigma, U_R, n, V_R, n); - auto stop = steady_clock::now(); app_time_us = duration_cast(stop - start).count(); - - // Verify right singular vector orthogonality: ||V_R^T V_R - I||_F / sqrt(n) right_svec_orth_error = RandLAPACK::testing::orthogonality_error(V_R, n, n); } +// Estimate ||A||_2 via power iteration on A^T A. O(iters * (m+n) memory) — no materialization. +template +static T estimate_op_2norm(GLO& A_op, int64_t m, int64_t n, int iters = 10) { + std::vector v(n), Av(m); + { + std::mt19937 rng(7); + std::normal_distribution N01(0, 1); + for (auto& x : v) x = N01(rng); + } + T sigma = (T)0; + for (int it = 0; it < iters; ++it) { + T nv = blas::nrm2(n, v.data(), 1); + if (nv == 0) return (T)0; + blas::scal(n, (T)1.0 / nv, v.data(), 1); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, v.data(), n, (T)0.0, Av.data(), m); + sigma = blas::nrm2(m, Av.data(), 1); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, + n, 1, m, (T)1.0, Av.data(), m, (T)0.0, v.data(), n); + } + return sigma; +} + +// Blocked orth_err: ||R^{-T} A^T A R^{-1} - I||_F / sqrt(n). +// O(n^2 + m*b) memory (no Q materialization). Mathematically equivalent to +// ||Q^T Q - I||_F / sqrt(n) with Q = A * R^{-1}. +template +static T compute_orth_error_memlite(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { + int64_t b = (block_size > 0) ? block_size : 256; + std::vector X(n * n, (T)0); + compute_AtA_blocked(A_op, m, n, X.data(), b); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X.data(), n); + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X.data(), n); + for (int64_t i = 0; i < n; ++i) X[i + i * n] -= (T)1.0; + T s = 0; + #pragma omp parallel for reduction(+:s) schedule(static) + for (int64_t i = 0; i < n * n; ++i) s += X[i] * X[i]; + return std::sqrt(s) / std::sqrt((T)n); +} + // ============================================================================ -// CSV output +// CSV writers — GSVD (preserves the column order of the original applications.cc) // ============================================================================ template -static void write_common_header_comments( +static void write_gsvd_header_comments( std::ofstream& out, int64_t m, int64_t n, int num_runs, const std::string& K_file, const std::string& V_file, T d_factor, int64_t sketch_nnz, int64_t block_size, @@ -317,43 +319,42 @@ static void write_common_header_comments( } template -static void write_csv_header(std::ofstream& out, int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) { +static void write_gsvd_results( + const std::string& filename, + const std::vector>& results, + int64_t m, int64_t n, int num_runs, + const std::string& K_file, const std::string& V_file, + T d_factor, int64_t sketch_nnz, int64_t block_size, + bool skip_apps, bool compute_cond) +{ + std::ofstream out(filename); out << "# GSVD Benchmark results\n"; - write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + write_gsvd_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," << "app_b_time_us," << "app_c_time_us,right_svec_orth_error," << "total_b_time_us,total_c_time_us," << "peak_rss_kb,analytical_kb\n"; + for (const auto& r : results) { + out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name << "," + << r.chol_time_us << "," + << r.qr_time_us << "," + << std::scientific << std::setprecision(6) << r.orth_error << "," + << std::scientific << std::setprecision(6) << r.r_backward_error << "," + << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," + << r.app_b_time_us << "," + << r.app_c_time_us << "," + << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," + << r.total_b_time_us << "," << r.total_c_time_us << "," + << r.peak_rss_kb << "," << r.analytical_kb + << "\n"; + } } template -static void write_csv_row(std::ofstream& out, const gsvd_result& r) { - out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name << "," - << r.chol_time_us << "," - << r.qr_time_us << "," - << std::scientific << std::setprecision(6) << r.orth_error << "," - << std::scientific << std::setprecision(6) << r.r_backward_error << "," - << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," - << r.app_b_time_us << "," - << r.app_c_time_us << "," - << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," - << r.total_b_time_us << "," << r.total_c_time_us << "," - << r.peak_rss_kb << "," << r.analytical_kb - << "\n"; -} - -// ============================================================================ -// Breakdown CSV output -// ============================================================================ - -template -static void write_breakdown_csv( +static void write_gsvd_breakdown( const std::string& filename, - const std::vector>& results, + const std::vector>& results, int64_t m, int64_t n, int num_runs, const std::string& K_file, const std::string& V_file, T d_factor, int64_t sketch_nnz, int64_t block_size, @@ -361,36 +362,101 @@ static void write_breakdown_csv( { std::ofstream out(filename); out << "# GSVD Benchmark runtime breakdown\n"; - write_common_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); + write_gsvd_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); out << "# Times are in microseconds\n"; out << "# CQRRT_linop breakdown (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n"; - - // Column header: m, n, run, algorithm, then all breakdown times out << "m,n,run,algorithm"; for (int i = 0; i < 11; ++i) out << ",t" << i; out << "\n"; - for (const auto& r : results) { out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name; - for (size_t i = 0; i < r.qr_breakdown.size(); ++i) { + for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; - } - // Pad with zeros if fewer than 11 columns - for (size_t i = r.qr_breakdown.size(); i < 11; ++i) { + for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; - } out << "\n"; } } // ============================================================================ -// Console summary +// CSV writers — IR-LSQ (preserves the column order plot_irlsq_results.m expects) +// ============================================================================ + +template +static void write_irlsq_results( + const std::string& filename, + const std::vector>& results, + int64_t m, int64_t n, int64_t nnz_or_zero, const std::string& input_label, + T noise_level, T d_factor, int64_t sketch_nnz, int64_t block_size, + int64_t method_mask) +{ + std::ofstream out(filename); + time_t now = time(nullptr); + out << "# Sparse IR-LSQ Benchmark results\n" + << "# Date: " << ctime(&now) + << "# input=" << input_label << "\n" + << "# M=" << m << " N=" << n << " nnz=" << nnz_or_zero << "\n" + << "# noise_level=" << noise_level << "\n" + << "# d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz + << " block_size=" << block_size << "\n" + << "# method_mask=" << method_mask << "\n" +#ifdef _OPENMP + << "# OpenMP threads: " << omp_get_max_threads() << "\n" +#else + << "# OpenMP threads: 1\n" +#endif + ; + out << "algorithm,run,m,n,qr_status,qr_time_us,peak_rss_kb,analytical_kb," + "orth_error," + "ir_total_us,ir_outer_iters,ir_inner_iters_total," + "ls_residual_norm,ls_solution_error\n"; + for (const auto& r : results) { + out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," + << r.qr_status << "," << r.qr_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "," + << std::scientific << std::setprecision(6) << r.orth_error << "," + << r.ir_total_us << "," << r.ir_outer_iters << "," << r.ir_inner_iters_total << "," + << std::scientific << std::setprecision(6) << r.ls_residual_norm << "," + << std::scientific << std::setprecision(6) << r.ls_solution_error + << "\n"; + } +} + +template +static void write_irlsq_breakdown( + const std::string& filename, + const std::vector>& results) +{ + std::ofstream out(filename); + out << "# Sparse IR-LSQ Benchmark runtime breakdown (microseconds)\n" + << "# QR breakdown layout depends on algorithm (see CQRRT_linop_applications.cc).\n" + << "# IR-LSQ breakdown (6): outer_total, inner_cg_total, trsm_total, fwd_total, adj_total, other\n" + << "# (sketch-and-solve x_0 time is folded into the difference between ir_total_us\n" + << "# in the results CSV and outer_total here)\n" + << "algorithm,run,phase,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10\n"; + for (const auto& r : results) { + out << r.alg_name << "," << r.run_idx << ",QR"; + for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; + for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + out << r.alg_name << "," << r.run_idx << ",IR"; + for (size_t i = 0; i < r.ir_breakdown.size(); ++i) out << "," << r.ir_breakdown[i]; + for (size_t i = r.ir_breakdown.size(); i < 11; ++i) out << ",0"; + out << "\n"; + } +} + +// ============================================================================ +// Console summaries // ============================================================================ template -static void print_summary(const std::string& alg_name, const std::vector>& results) { - printf("\n %s:\n", alg_name.c_str()); +static void print_svd_summary(const std::string& alg_name, const std::vector>& results) { + printf("\n %s (SVD):\n", alg_name.c_str()); for (const auto& r : results) { + if (r.qr_status != 0) { + printf(" Run %ld: QR breakdown (status=%d)\n", (long)r.run_idx, r.qr_status); + continue; + } printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, QR=%ld us\n", (long)r.run_idx, (double)r.orth_error, r.r_backward_error, r.qr_time_us); if (r.orth_error_upcast > 0) @@ -404,30 +470,67 @@ static void print_summary(const std::string& alg_name, const std::vector +static void print_irlsq_summary(const bench_result& r) { + std::printf("\n [%s] Run %ld (noise=%.3f):\n", + r.alg_name.c_str(), (long)r.run_idx, (double)r.noise_level); + if (r.qr_status != 0) { + std::printf(" QR returned status %d — IR-LSQ skipped.\n", r.qr_status); + return; + } + std::printf(" QR: %ld us, peak_RSS=%ld KB, predicted=%ld KB\n", + r.qr_time_us, r.peak_rss_kb, r.analytical_kb); + if (r.orth_error >= 0) std::printf(" orth_err = %.3e\n", (double)r.orth_error); + std::printf(" IR-LSQ (with x_0): total=%ld us, outer=%d, inner_total=%d\n", + r.ir_total_us, r.ir_outer_iters, r.ir_inner_iters_total); + std::printf(" ||Ax-b||/(||A||*||x||+||b||) = %.3e\n", (double)r.ls_residual_norm); + if (r.ls_solution_error >= 0) + std::printf(" ||x-x_true||/||x_true|| = %.3e\n", (double)r.ls_solution_error); + else + std::printf(" ||x-x_true||/||x_true|| = N/A (no ground-truth x_true)\n"); +} + // ============================================================================ -// Core benchmark logic (templated over operator type) +// Core templated runner // ============================================================================ template static int run_benchmark_inner( OpType& A_op, - int64_t m, int64_t n, - const std::string& output_dir, - int64_t num_runs, + int64_t m, int64_t n, int64_t input_nnz, + const std::string& output_dir, int64_t num_runs, + const std::string& mode, T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond, bool upcast_orth, + bool skip_svd, bool compute_cond, bool upcast_orth, + int64_t method_mask, T noise_level, long chol_time_us, const std::string& K_file, const std::string& V_file, - const std::string& op_label) + const std::string& op_label, + const std::string& input_label, + const std::vector* b_ptr, // M-vector RHS (irlsq/both); nullptr if svd-only + const std::vector* x_true_ptr) // N-vector ground truth (sparse irlsq); nullptr otherwise { - // Condition number diagnostic (materializes A_op, runs two SVDs) + bool do_svd = (mode == "svd" || mode == "both"); + bool do_irlsq = (mode == "irlsq" || mode == "both"); + + // Build the ordered list of selected algorithm names from the bitmask. + std::vector selected_algs; + if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); + if (method_mask & 2) selected_algs.push_back("CholQR"); + if (method_mask & 4) selected_algs.push_back("sCholQR3"); + if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); + if (method_mask & 64) selected_algs.push_back("CQRRT_linop_bqrrp"); + + if (selected_algs.empty()) { + std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; + return 1; + } + if (compute_cond) { RandLAPACK::testing::print_condition_diagnostics(A_op, op_label); } - // ================================================================ - // Prepare RNG states for each run - // ================================================================ + // Per-run RNG states RandBLAS::RNGState main_state(123); std::vector> run_states(num_runs); for (int64_t r = 0; r < num_runs; ++r) { @@ -435,32 +538,35 @@ static int run_benchmark_inner( if (r > 0) run_states[r].key.incr(r); } - // Shared buffers - std::vector Q_buf(m * n); - T tol = std::pow(std::numeric_limits::epsilon(), 0.85); - - // Storage for all results - std::vector> all_results; + T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); - // ================================================================ - // Run warmup (unreported) - // ================================================================ + // Warmup (CQRRT_linop) std::cout << "Running warmup... " << std::flush; { - auto warmup_state = run_states[0]; - std::vector R_warmup(n * n, 0.0); - RandLAPACK::CQRRT_linops warmup_algo(false, tol, false); - warmup_algo.nnz = sketch_nnz; - warmup_algo.block_size = block_size; - warmup_algo.call(A_op, R_warmup.data(), n, d_factor, warmup_state); + auto warm_state = run_states[0]; + std::vector R_warm(n * n, (T)0); + RandLAPACK::CQRRT_linops warm_algo(false, tol, false); + warm_algo.nnz = sketch_nnz; + warm_algo.block_size = block_size; + warm_algo.call(A_op, R_warm.data(), n, d_factor, warm_state); } std::cout << "done\n\n"; - // ================================================================ - // Pre-materialize A for upcast orthogonality (once, shared across all algorithms) - // ================================================================ + // Precompute ||A||_2 and ||b|| for the Higham backward-error metric used by IR-LSQ: + // ls_residual_norm = ||A x - b|| / (||A||_2 * ||x|| + ||b||) + T A_2norm = (T)0, b_norm = (T)0; + if (do_irlsq && b_ptr) { + std::cout << "Estimating ||A||_2 via power iteration (10 iters)... " << std::flush; + A_2norm = estimate_op_2norm(A_op, m, n, 10); + b_norm = blas::nrm2(m, b_ptr->data(), 1); + std::cout << "||A||_2 ~ " << A_2norm << ", ||b|| = " << b_norm << "\n\n"; + } + + // Pre-materialize A for upcast / r_backward_error (SVD path only) T* A_materialized = nullptr; - if (upcast_orth) { + std::vector AtA_precomputed; + T norm_A_sq_precomputed = 0; + if (do_svd && upcast_orth) { std::cout << "Materializing " << op_label << " for upcast (" << m << " x " << n << ", " << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; A_materialized = new T[m * n]; @@ -468,15 +574,8 @@ static int run_benchmark_inner( A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_materialized, m); std::cout << "done\n\n"; - } - // ================================================================ - // Pre-compute A^T A and ||A||_F^2 for r_backward_error (A is constant) - // ================================================================ - std::vector AtA_precomputed; - T norm_A_sq_precomputed = 0; - if (A_materialized) { - AtA_precomputed.resize(n * n, 0.0); + AtA_precomputed.assign(n * n, (T)0.0); blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); fill_lower_from_upper(AtA_precomputed.data(), n); @@ -484,192 +583,354 @@ static int run_benchmark_inner( norm_A_sq_precomputed += AtA_precomputed[i + i * n]; } + T x_true_norm = (T)0; + if (x_true_ptr) x_true_norm = blas::nrm2(n, x_true_ptr->data(), 1); + + std::vector> all_results; + // ================================================================ - // Common per-algorithm loop: run QR, check orthogonality, run apps + // Per-(method, run) loop // ================================================================ - // call_algo(res, R, run_idx) fills res.qr_time_us, res.qr_breakdown, - // res.peak_rss_kb, res.analytical_kb — everything else is shared. - auto run_algo = [&](const std::string& name, auto call_algo) { - std::cout << "\n=== " << name << " ===\n"; - std::vector> results(num_runs); - for (int64_t r = 0; r < num_runs; ++r) { - auto& res = results[r]; - res.m = m; res.n = n; res.run_idx = r; - res.alg_name = name; + for (const auto& alg_name : selected_algs) { + std::cout << "\n=== Algorithm: " << alg_name << " ===\n"; + std::vector> alg_results; + + for (int64_t run_idx = 0; run_idx < num_runs; ++run_idx) { + bench_result res{}; + res.m = m; res.n = n; + res.run_idx = run_idx; + res.alg_name = alg_name; + res.noise_level = noise_level; res.chol_time_us = chol_time_us; - - std::vector R(n * n, 0.0); - call_algo(res, R, r); - - // Compute Q = A * R^{-1}: use pre-materialized A if available, else via linop - if (A_materialized) { - lapack::lacpy(lapack::MatrixType::General, m, n, A_materialized, m, Q_buf.data(), m); - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, m, n, (T)1.0, R.data(), n, Q_buf.data(), m); + res.qr_status = 0; + res.qr_time_us = 0; + res.orth_error = (T)-1.0; + res.is_orthonormal = false; + res.r_backward_error = -1.0; + res.orth_error_upcast = 0.0; + res.app_b_time_us = 0; + res.app_c_time_us = 0; + res.right_svec_orth_error = (T)-1.0; + res.total_b_time_us = 0; + res.total_c_time_us = 0; + res.ir_total_us = 0; + res.ir_outer_iters = 0; + res.ir_inner_iters_total = 0; + res.ls_residual_norm = (T)-1.0; + res.ls_solution_error = (T)-1.0; + res.peak_rss_kb = 0; + res.analytical_kb = 0; + + std::vector R(n * n, (T)0); + auto state = run_states[run_idx]; + + // ---- QR dispatch (5-way, lifted verbatim from CQRRT_linop_irlsq.cc) ---- + std::cout << "[Run " << run_idx << ", " << alg_name << "] QR ... " << std::flush; + RandLAPACK::PeakRSSTracker mem; mem.start(); + if (alg_name == "sCholQR3") { + RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[17]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); + } + } else if (alg_name == "sCholQR3_basic") { + RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); + res.qr_status = qr_algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[14]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); + } + } else if (alg_name == "CholQR") { + RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(A_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[5]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 6); + res.qr_breakdown.resize(11, 0); + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); + } } else { - compute_Q_from_R(A_op, R.data(), n, Q_buf.data(), m, n); + RandLAPACK::CQRRT_linops qr_algo(/*time_subroutines=*/true, tol); + qr_algo.nnz = sketch_nnz; + qr_algo.block_size = block_size; + if (alg_name == "CQRRT_linop") + qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; + else /* CQRRT_linop_bqrrp */ + qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; + res.qr_status = qr_algo.call(A_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[10]; + res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); + res.analytical_kb = (alg_name == "CQRRT_linop_bqrrp") + ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(m, n, d_factor, block_size) + : RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + } } - res.orth_error = RandLAPACK::testing::orthogonality_error(Q_buf.data(), m, n); - res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - // R-factor backward error: ||A^T A - R^T R||_F / ||A||_F^2 - // Use precomputed A^T A (A is constant across algorithms), else blocked linop - if (A_materialized) { - std::vector RtR(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); - fill_lower_from_upper(RtR.data(), n); - - T diff_sq = 0; - #pragma omp parallel for reduction(+:diff_sq) schedule(static) - for (int64_t i = 0; i < n * n; ++i) { T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; } - res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); - } else { - res.r_backward_error = compute_r_backward_error(A_op, R.data(), m, n, block_size); + if (res.qr_status != 0) { + std::cerr << "\n [" << alg_name << "] Run " << run_idx + << ": QR returned status " << res.qr_status + << " (likely Cholesky breakdown). Skipping post-processing.\n"; + res.qr_time_us = -1; + res.qr_breakdown.assign(11, 0); + res.analytical_kb = 0; + alg_results.push_back(res); + all_results.push_back(res); + if (do_irlsq) print_irlsq_summary(res); + continue; } + std::cout << "done (" << res.qr_time_us << " us)"; - // Upcast orthogonality: reconstruct Q in higher precision (uses pre-materialized A) - if (upcast_orth) { - if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); - } else if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); + // ---- Orth_error: ||Q^T Q - I||_F / sqrt(n) via blocked compute (memlite). ---- + // Runs for both SVD and IR-LSQ modes — Max wants this for every method. + res.orth_error = compute_orth_error_memlite(A_op, R.data(), m, n, block_size); + res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); + + // ---- SVD post-processing ---- + if (do_svd) { + if (A_materialized) { + std::vector RtR(n * n, 0.0); + blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, + n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); + fill_lower_from_upper(RtR.data(), n); + T diff_sq = 0; + #pragma omp parallel for reduction(+:diff_sq) schedule(static) + for (int64_t i = 0; i < n * n; ++i) { + T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; + } + res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); + } else { + res.r_backward_error = compute_r_backward_error(A_op, R.data(), m, n, block_size); } - } else { - res.orth_error_upcast = 0.0; - } - res.app_b_time_us = 0; - res.app_c_time_us = 0; - res.right_svec_orth_error = (T)-1.0; + if (upcast_orth) { + if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); + } else if constexpr (std::is_same_v) { + res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); + } + } + + if (!skip_svd) { + std::vector sigma_b(n, 0.0); + app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); - if (!skip_apps) { - std::vector sigma_b(n, 0.0); - app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); + std::vector sigma_c(n, 0.0), V_R(n * n, 0.0), U_R(n * n, 0.0); + app_generalized_svecs(R.data(), n, n, sigma_c.data(), V_R.data(), U_R.data(), + res.right_svec_orth_error, res.app_c_time_us); + } - std::vector sigma_c(n, 0.0), V_R(n * n, 0.0), U_R(n * n, 0.0); - app_generalized_svecs(R.data(), n, n, sigma_c.data(), V_R.data(), U_R.data(), - res.right_svec_orth_error, res.app_c_time_us); + res.total_b_time_us = res.qr_time_us + res.app_b_time_us; + res.total_c_time_us = res.qr_time_us + res.app_c_time_us; } - res.total_b_time_us = res.qr_time_us + res.app_b_time_us; - res.total_c_time_us = res.qr_time_us + res.app_c_time_us; + // ---- IR-LSQ post-processing ---- + if (do_irlsq) { + std::cout << ". IR-LSQ ... " << std::flush; + const std::vector& b = *b_ptr; + auto ls_t0 = steady_clock::now(); + + // Sketch-and-solve initial guess x_0 (Alg. 1 line 3 of Epperly–Meier–Nakatsukasa 2025); + // fresh sparse sketch S_2 independent of CQRRT's S_1. + const int64_t d_init = (int64_t)(d_factor * (T)n); + RandBLAS::SparseDist DS_init(d_init, m, sketch_nnz); + auto x0_state = state; + x0_state.key.incr(0xA1B2C3D4u); + RandBLAS::SparseSkOp S2(DS_init, x0_state); + RandBLAS::fill_sparse(S2); + + std::vector SA(d_init * n, (T)0); + A_op(blas::Side::Right, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + d_init, n, m, (T)1.0, S2, (T)0.0, SA.data(), d_init); + + std::vector Sb(d_init, (T)0); + RandBLAS::sketch_general(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + d_init, (int64_t)1, m, (T)1.0, + S2, b.data(), m, (T)0.0, Sb.data(), d_init); + + std::vector x_ls(n, (T)0); + blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, d_init, n, + (T)1.0, SA.data(), d_init, Sb.data(), 1, + (T)0.0, x_ls.data(), 1); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, n, 1, + (T)1.0, R.data(), n, x_ls.data(), n); + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, n, 1, + (T)1.0, R.data(), n, x_ls.data(), n); + + RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, + /*max_inner=*/200, + /*n_steps=*/2, + /*timing=*/true, + /*verbose=*/false); + int ir_status = ir.call(A_op, R.data(), n, b.data(), m, x_ls.data(), n); + auto ls_t1 = steady_clock::now(); + if (ir_status != 0) { + std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; + } + + res.ir_total_us = duration_cast(ls_t1 - ls_t0).count(); + res.ir_outer_iters = ir.outer_iters_done; + res.ir_inner_iters_total = 0; + for (int v : ir.inner_iters_per_step) res.ir_inner_iters_total += v; + if (!ir.times.empty()) res.ir_breakdown = ir.times; + + // Higham normwise backward-error metric: + // ls_residual_norm = ||A x - b|| / (||A||_2 * ||x|| + ||b||) + // Drivable to machine epsilon for a backward-stable LS solver. + std::vector Ax(m, (T)0); + A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x_ls.data(), n, (T)0.0, Ax.data(), m); + T resid_sq = 0; + #pragma omp parallel for reduction(+:resid_sq) schedule(static) + for (int64_t i = 0; i < m; ++i) { T d = Ax[i] - b[i]; resid_sq += d * d; } + T resid_norm = std::sqrt(resid_sq); + T x_norm = blas::nrm2(n, x_ls.data(), 1); + T denom = A_2norm * x_norm + b_norm; + res.ls_residual_norm = (denom > 0) ? resid_norm / denom : (T)-1.0; + + if (x_true_ptr) { + T err_sq = 0; + for (int64_t i = 0; i < n; ++i) { + T d = x_ls[i] - (*x_true_ptr)[i]; + err_sq += d * d; + } + res.ls_solution_error = (x_true_norm > 0) ? std::sqrt(err_sq) / x_true_norm : (T)-1.0; + } else { + res.ls_solution_error = (T)-1.0; + } + std::cout << "done (" << res.ir_total_us << " us)\n"; + } else { + std::cout << "\n"; + } + + if (do_irlsq) print_irlsq_summary(res); + alg_results.push_back(res); all_results.push_back(res); } - print_summary(name, results); - }; - // ================================================================ - // CQRRT_linop (TRSM_IDENTITY preconditioner) - // ================================================================ - run_algo("CQRRT_linop", [&](gsvd_result& res, std::vector& R, int64_t r) { - auto state = run_states[r]; - RandLAPACK::CQRRT_linops algo(true, tol, false); - algo.nnz = sketch_nnz; algo.block_size = block_size; - algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; - RandLAPACK::PeakRSSTracker mem; mem.start(); - algo.call(A_op, R.data(), n, d_factor, state); - res.peak_rss_kb = mem.stop(); - res.qr_time_us = algo.times[10]; - // breakdown: alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total - res.qr_breakdown.assign(algo.times.begin(), algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); - }); - - // Free pre-materialized A + if (do_svd) print_svd_summary(alg_name, alg_results); + } + delete[] A_materialized; // ================================================================ - // Write CSV output + // CSV output (shared timestamp across modes) // ================================================================ - // Generate timestamped filenames char time_buf[64]; time_t now = time(nullptr); strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); - std::string results_file = output_dir + "/" + time_buf + "_gsvd_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_gsvd_breakdown.csv"; - - std::ofstream out(results_file); - write_csv_header(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - for (const auto& r : all_results) { - write_csv_row(out, r); + if (do_svd) { + std::string results_file = output_dir + "/" + time_buf + "_gsvd_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_gsvd_breakdown.csv"; + write_gsvd_results(results_file, all_results, m, n, num_runs, + K_file, V_file, d_factor, sketch_nnz, block_size, skip_svd, compute_cond); + std::cout << "\nGSVD results written to " << results_file << "\n"; + write_gsvd_breakdown(breakdown_file, all_results, m, n, num_runs, + K_file, V_file, d_factor, sketch_nnz, block_size, skip_svd, compute_cond); + std::cout << "GSVD breakdown written to " << breakdown_file << "\n"; + } + if (do_irlsq) { + std::string results_file = output_dir + "/" + time_buf + "_irlsq_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_irlsq_breakdown.csv"; + write_irlsq_results(results_file, all_results, m, n, input_nnz, input_label, + noise_level, d_factor, sketch_nnz, block_size, method_mask); + std::cout << "\nIR-LSQ results written to " << results_file << "\n"; + write_irlsq_breakdown(breakdown_file, all_results); + std::cout << "IR-LSQ breakdown written to " << breakdown_file << "\n"; } - out.close(); - std::cout << "\n\nResults written to " << results_file << "\n"; - - write_breakdown_csv(breakdown_file, all_results, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - std::cout << "Runtime breakdown written to " << breakdown_file << "\n"; return 0; } // ============================================================================ -// Main benchmark dispatcher +// Main dispatcher // ============================================================================ template int run_benchmark(int argc, char* argv[]) { - // Sparse mode: sparse [...] (argc >= 7) - // FEM mode: [...] (argc >= 8) - // - // FEM operator is the Petrov-Galerkin form J = L^{-1} * K * V, where L = chol(M) - // (mass-matrix factor applied via half-solve). Built as a doubly-nested - // CompositeOperator: - // J = CompositeOperator(L_inv_op, CompositeOperator(K_op, V_op)) - if (argc < 7) { + // ... → argc >= 5 to reach + if (argc < 8) { std::cerr << "Usage: " << argv[0] - << " \n" - << " sparse mode: 'sparse' [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" - << " FEM mode: [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; + << " \n" + << " sparse mode: 'sparse' " + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " FEM mode: " + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " mode = svd | irlsq | both\n"; return 1; } std::string output_dir = argv[2]; int64_t num_runs = std::stol(argv[3]); - std::string arg4 = argv[4]; - bool sparse_mode = (arg4 == "sparse"); + std::string mode = argv[4]; + if (mode != "svd" && mode != "irlsq" && mode != "both") { + std::cerr << "Error: must be one of {svd, irlsq, both}; got '" << mode << "'\n"; + return 1; + } + bool do_irlsq = (mode == "irlsq" || mode == "both"); + + std::string arg5 = argv[5]; + bool sparse_mode = (arg5 == "sparse"); std::string K_file, M_file, V_file, A_file; T d_factor; - int dfactor_idx; // index of d_factor in argv; subsequent optional args follow + int dfactor_idx; if (sparse_mode) { - if (argc < 7) { + if (argc < 8) { std::cerr << "Error: sparse mode needs \n"; return 1; } - A_file = argv[5]; - d_factor = std::stod(argv[6]); - dfactor_idx = 6; + A_file = argv[6]; + d_factor = std::stod(argv[7]); + dfactor_idx = 7; } else { - if (argc < 8) { + if (argc < 9) { std::cerr << "Error: FEM mode needs \n"; return 1; } - K_file = arg4; - M_file = argv[5]; - V_file = argv[6]; - d_factor = std::stod(argv[7]); - dfactor_idx = 7; + K_file = arg5; + M_file = argv[6]; + V_file = argv[7]; + d_factor = std::stod(argv[8]); + dfactor_idx = 8; } auto opt_long = [&](int rel, int64_t def) { int idx = dfactor_idx + rel; return (argc > idx) ? std::stol(argv[idx]) : def; }; + auto opt_double = [&](int rel, double def) { + int idx = dfactor_idx + rel; + return (argc > idx) ? std::stod(argv[idx]) : def; + }; int64_t sketch_nnz = opt_long(1, 4); int64_t block_size = opt_long(2, 0); - bool skip_apps = (opt_long(3, 0) != 0); + bool skip_svd = (opt_long(3, 0) != 0); bool compute_cond = (opt_long(4, 0) != 0); bool upcast_orth = (opt_long(5, 0) != 0); + int64_t method_mask = opt_long(6, 79); + T noise_level = (T)opt_double(7, 0.05); - std::cout << "=== GSVD/Generalized LS Benchmark ===\n"; + std::cout << "=== CQRRT linop benchmark ===\n"; + std::cout << " mode: " << mode << "\n"; if (sparse_mode) { - std::cout << " Mode: sparse (single-matrix SparseLinOp)\n" + std::cout << " Input mode: sparse (single-matrix SparseLinOp)\n" << " A file: " << A_file << "\n"; } else { - std::cout << " Mode: FEM composite (J = L^{-1} K V with L = chol(M))\n" + std::cout << " Input mode: FEM composite (J = L^{-1} K V with L = chol(M))\n" << " K file: " << K_file << "\n" << " M file: " << M_file << "\n" << " V file: " << V_file << "\n"; @@ -677,9 +938,16 @@ int run_benchmark(int argc, char* argv[]) { std::cout << " d_factor: " << d_factor << "\n" << " sketch_nnz: " << sketch_nnz << "\n" << " block_size: " << block_size << "\n" - << " skip_apps: " << (skip_apps ? "yes" : "no") << "\n" + << " skip_svd: " << (skip_svd ? "yes" : "no") << "\n" << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n" << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n" + << " method_mask: " << method_mask + << " (linop=" << (method_mask&1) + << " CholQR=" << ((method_mask>>1)&1) + << " sCholQR3=" << ((method_mask>>2)&1) + << " sCholQR3_basic=" << ((method_mask>>3)&1) + << " linop_bqrrp=" << ((method_mask>>6)&1) << ")\n" + << " noise_level: " << noise_level << "\n" << " num_runs: " << num_runs << "\n" #ifdef _OPENMP << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; @@ -688,7 +956,7 @@ int run_benchmark(int argc, char* argv[]) { #endif // ================================================================ - // Sparse mode: wrap A directly as SparseLinOp, skip Cholesky + // Sparse mode: SparseLinOp directly, no Cholesky. // ================================================================ if (sparse_mode) { std::cout << "Loading A from " << A_file << "... " << std::flush; @@ -701,17 +969,44 @@ int run_benchmark(int argc, char* argv[]) { std::cerr << "Error: matrix must be overdetermined (m >= n), got " << m << "x" << n << "\n"; return 1; } + + // Sparse irlsq b construction: x_true ~ U(-1,1)^n, b = A x_true + scaled Gaussian noise. + std::vector x_true, b; + if (do_irlsq) { + x_true.assign(n, (T)0); + { + std::mt19937 rng(42); + std::uniform_real_distribution dist((T)-1.0, (T)1.0); + for (auto& v : x_true) v = dist(rng); + } + std::vector b_clean(m, (T)0), noise_vec(m, (T)0); + A_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b_clean.data(), m); + T b_clean_norm = blas::nrm2(m, b_clean.data(), 1); + std::mt19937 noise_rng(13); + std::normal_distribution N01(0, 1); + for (auto& v : noise_vec) v = N01(noise_rng); + T raw_noise_norm = blas::nrm2(m, noise_vec.data(), 1); + T scale = noise_level * b_clean_norm / raw_noise_norm; + b.assign(m, (T)0); + for (int64_t i = 0; i < m; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; + std::cout << "Synthetic LS problem: ||x_true|| = " << blas::nrm2(n, x_true.data(), 1) + << ", ||b|| = " << blas::nrm2(m, b.data(), 1) << "\n\n"; + } + return run_benchmark_inner( - A_linop, m, n, output_dir, num_runs, + A_linop, m, n, nnz_A, output_dir, num_runs, mode, d_factor, sketch_nnz, block_size, - skip_apps, compute_cond, upcast_orth, + skip_svd, compute_cond, upcast_orth, + method_mask, noise_level, 0L /*chol_time_us*/, "sparse", A_file, - "A (" + A_file + ")"); + "A (" + A_file + ")", A_file, + do_irlsq ? &b : nullptr, + do_irlsq ? &x_true : nullptr); } // ================================================================ - // FEM mode: load K, V; Cholesky-factorize M; build J = L^{-1} * K * V - // as a doubly-nested CompositeOperator. + // FEM mode: load K, V; Cholesky-factorize M; build J = L^{-1} K V. // ================================================================ std::cout << "Loading K (stiffness) from " << K_file << "... " << std::flush; int64_t m_K, n_K, nnz_K; @@ -758,20 +1053,40 @@ int run_benchmark(int argc, char* argv[]) { J_op.block_size = block_size; std::cout << "Composite operator J = L^{-1} K V : " << m << " x " << n << "\n\n"; + // FEM irlsq b construction: b = L^{-1} * r, r ~ N(0, 1)^{m_K}. No ground truth x_true. + std::vector b; + if (do_irlsq) { + std::vector r(m_K, (T)0); + std::mt19937 rng_b(13); + std::normal_distribution N01(0, 1); + for (auto& v : r) v = N01(rng_b); + b.assign(m_K, (T)0); + L_inv_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m_K, 1, m_K, (T)1.0, r.data(), m_K, (T)0.0, b.data(), m_K); + std::cout << "FEM IR-LSQ b: ||b|| = " << blas::nrm2(m_K, b.data(), 1) + << " (b = L^{-1} r, r ~ N(0,1)^M)\n\n"; + } + return run_benchmark_inner( - J_op, m, n, output_dir, num_runs, + J_op, m, n, nnz_K, output_dir, num_runs, mode, d_factor, sketch_nnz, block_size, - skip_apps, compute_cond, upcast_orth, + skip_svd, compute_cond, upcast_orth, + method_mask, noise_level, chol_time_us, K_file, V_file, - "L^{-1} K V (M=" + M_file + ")"); + "L^{-1} K V (M=" + M_file + ")", K_file, + do_irlsq ? &b : nullptr, + nullptr /*x_true_ptr: FEM has no ground truth*/); } int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] - << " \n" - << " sparse mode: 'sparse' [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n" - << " FEM mode: [sketch_nnz] [block_size] [skip_apps] [compute_cond] [upcast_orth]\n"; + << " \n" + << " sparse mode: 'sparse' " + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " FEM mode: " + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " mode = svd | irlsq | both\n"; return 1; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc deleted file mode 100644 index d2bab2e4d..000000000 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_irlsq.cc +++ /dev/null @@ -1,467 +0,0 @@ -// Sparse-input IR-LSQ Benchmark — Q-less QR + sketch-and-solve x₀ + 2-step IR. -// -// Loads a tall sparse matrix A from a Matrix Market (.mtx) file (M ≥ N), -// wraps it as a SparseLinOp, generates a synthetic LS problem -// x_true ~ U(-1, 1)^N, b = A x_true + Gaussian noise (||noise||/||A x_true|| = noise_level), -// and for each Q-less QR variant selected by `method_mask`: -// 1. Run the QR variant on J → R (n × n upper triangular). -// 2. Draw a fresh sparse sketch S₂ (independent of CQRRT's S₁), form -// SA = S₂·J and Sb = S₂·b, then x₀ = R⁻¹ R⁻ᵀ (SA)ᵀ Sb (paper Alg. 1, line 3). -// 3. Run IterRefineLSQ(J, R, b) starting from x₀. -// 4. Record per-(algorithm, run) ||x − x_true||/||x_true||, ||b − Ax||/||b||, -// IR iter counts, timing, peak vs predicted memory. -// -// Usage: -// ./CQRRT_linop_irlsq -// [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask] -// where: -// prec = "double" | "float" -// mtx_path = path to a tall (M ≥ N) sparse matrix in Matrix Market format -// noise_level = ||noise|| / ||A x_true|| (default 0.05) -// d_factor = sketch oversampling for both CQRRT and x₀ (default 2.0) -// sketch_nnz = nonzeros per column of the SASO sketch (default 4) -// block_size = blocking parameter for CQRRT / sCholQR3 (default 0 = library default) -// method_mask = bitmask of Q-less QR variants (default 0b1001111 = 79) -// bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) -// bit 1 ( 2): CholQR -// bit 2 ( 4): sCholQR3 -// bit 3 ( 8): sCholQR3_basic -// bit 6 ( 64): CQRRT_linop_bqrrp (BQRRP) - -#include "RandLAPACK.hh" -#include "rl_blaspp.hh" -#include "rl_lapackpp.hh" -#include "RandLAPACK/testing/rl_memory_tracker.hh" -#include "cqrrt_bench_common.hh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _OPENMP -#include -#endif - - -using std::chrono::steady_clock; -using std::chrono::duration_cast; -using std::chrono::microseconds; - - -// ============================================================================= -// Result struct -// ============================================================================= -template -struct irlsq_result { - int64_t m, n; - int64_t run_idx; - std::string alg_name; // "CQRRT_linop", "CholQR", "sCholQR3", "sCholQR3_basic", "CQRRT_linop_bqrrp" - T noise_level; - int qr_status; // 0 on success; nonzero indicates QR breakdown (no IR-LSQ run) - - // Q-less QR - long qr_time_us; - T orth_error; - long peak_rss_kb; - long analytical_kb; - - // IR LSQ - long ir_total_us; // includes the sketch-and-solve x₀ computation - int ir_outer_iters; - int ir_inner_iters_total; - T ls_residual_norm; - T ls_solution_error; - - // Breakdowns - std::vector qr_breakdown; - std::vector ir_breakdown; -}; - - -template -static void print_summary(const irlsq_result& r) { - std::printf("\n [%s] Run %ld (noise=%.3f):\n", - r.alg_name.c_str(), (long)r.run_idx, (double)r.noise_level); - if (r.qr_status != 0) { - std::printf(" QR returned status %d — IR-LSQ skipped.\n", r.qr_status); - return; - } - std::printf(" QR: %ld us, peak_RSS=%ld KB, predicted=%ld KB\n", - r.qr_time_us, r.peak_rss_kb, r.analytical_kb); - if (r.orth_error >= 0) std::printf(" orth_err = %.3e\n", (double)r.orth_error); - std::printf(" IR-LSQ (with x_0): total=%ld us, outer=%d, inner_total=%d\n", - r.ir_total_us, r.ir_outer_iters, r.ir_inner_iters_total); - std::printf(" ||r||/||b|| = %.3e\n", (double)r.ls_residual_norm); - std::printf(" ||x-x_true||/||x_true|| = %.3e\n", (double)r.ls_solution_error); -} - - -// ============================================================================= -// Core templated runner -// ============================================================================= -template -int run_benchmark(int argc, char* argv[]) -{ - if (argc < 5) { - std::cerr << "Usage: " << argv[0] - << " " - << " [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask]\n" - << " mtx_path: tall (M >= N) sparse matrix in Matrix Market format\n" - << " noise_level: ||noise||/||A x_true|| (default 0.05)\n" - << " method_mask: bitmask of Q-less QR variants (default = 0b1001111 = 79)\n" - << " bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY)\n" - << " bit 1 ( 2): CholQR\n" - << " bit 2 ( 4): sCholQR3\n" - << " bit 3 ( 8): sCholQR3_basic\n" - << " bit 6 ( 64): CQRRT_linop_bqrrp (BQRRP)\n"; - return 1; - } - - std::string output_dir = argv[2]; - int64_t num_runs = std::stol(argv[3]); - std::string mtx_path = argv[4]; - T noise_level = (argc >= 6) ? (T)std::stod(argv[5]) : (T)0.05; - T d_factor = (argc >= 7) ? (T)std::stod(argv[6]) : (T)2.0; - int64_t sketch_nnz = (argc >= 8) ? std::stol(argv[7]) : 4; - int64_t block_size = (argc >= 9) ? std::stol(argv[8]) : 0; - int64_t method_mask = (argc >= 10) ? std::stol(argv[9]) : 0b1001111; - - std::cout << "=== Sparse IR-LSQ Benchmark (SparseLinOp + Q-less QR + IR-LSQ) ===\n" - << " mtx_path = " << mtx_path << "\n" - << " noise_lvl = " << noise_level << "\n" - << " method_mask= " << method_mask - << " (linop=" << (method_mask&1) - << " CholQR=" << ((method_mask>>1)&1) - << " sCholQR3=" << ((method_mask>>2)&1) - << " sCholQR3_basic=" << ((method_mask>>3)&1) - << " linop_bqrrp=" << ((method_mask>>6)&1) << ")\n" - << " d_factor = " << d_factor << "\n" - << " sketch_nnz = " << sketch_nnz << "\n" - << " block_size = " << block_size << "\n" - << " num_runs = " << num_runs << "\n" -#ifdef _OPENMP - << " OpenMP threads: " << omp_get_max_threads() << "\n"; -#else - << " OpenMP threads: 1\n"; -#endif - - // -------- Load matrix -------- - int64_t M = 0, N = 0, nnz = 0; - std::cout << "Loading " << mtx_path << " ... " << std::flush; - auto load_t0 = steady_clock::now(); - auto A_csr = load_csr(mtx_path, M, N, nnz); - auto load_t1 = steady_clock::now(); - std::cout << "done (" << duration_cast(load_t1 - load_t0).count() << " us)\n" - << " M=" << M << " N=" << N << " nnz=" << nnz - << " density=" << ((double)nnz / ((double)M * (double)N)) << "\n"; - - if (M < N) { - std::cerr << "Error: need tall matrix (M >= N), got M=" << M << " N=" << N << "\n"; - return 1; - } - - RandLAPACK::linops::SparseLinOp> J(M, N, A_csr); - - // -------- Synthetic LS problem: x_true ~ U(-1,1), b = J x_true + noise -------- - std::vector x_true(N), b_clean(M, (T)0), b(M, (T)0), noise_vec(M, (T)0); - { - std::mt19937 rng(42); - std::uniform_real_distribution dist((T)-1.0, (T)1.0); - for (auto& v : x_true) v = dist(rng); - } - T x_true_norm = blas::nrm2(N, x_true.data(), 1); - if (x_true_norm == 0) { std::cerr << "x_true is zero — aborting\n"; return 1; } - - J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - M, 1, N, (T)1.0, x_true.data(), N, (T)0.0, b_clean.data(), M); - T b_clean_norm = blas::nrm2(M, b_clean.data(), 1); - - { - std::mt19937 noise_rng(13); - std::normal_distribution N01(0, 1); - for (auto& v : noise_vec) v = N01(noise_rng); - T raw_noise_norm = blas::nrm2(M, noise_vec.data(), 1); - T scale = noise_level * b_clean_norm / raw_noise_norm; - for (int64_t i = 0; i < M; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; - } - T b_norm = blas::nrm2(M, b.data(), 1); - std::cout << "Synthetic LS problem: ||x_true|| = " << x_true_norm - << ", ||b|| = " << b_norm << "\n\n"; - - // -------- RNG states for runs -------- - RandBLAS::RNGState main_state(123); - std::vector> run_states(num_runs); - for (int64_t r = 0; r < num_runs; ++r) { - run_states[r] = main_state; - if (r > 0) run_states[r].key.incr(r); - } - T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); - - // -------- Warmup -------- - std::cout << "Running warmup... " << std::flush; - { - auto warm_state = run_states[0]; - std::vector R_warm(N * N, (T)0); - RandLAPACK::CQRRT_linops warm_algo(false, tol, false); - warm_algo.nnz = sketch_nnz; - warm_algo.block_size = block_size; - warm_algo.call(J, R_warm.data(), N, d_factor, warm_state); - } - std::cout << "done\n\n"; - - // -------- Per-(algorithm, run) lambda -------- - auto run_one = [&](const std::string& alg_name, int64_t r) -> irlsq_result - { - irlsq_result res{}; - res.m = M; res.n = N; - res.run_idx = r; - res.alg_name = alg_name; - res.noise_level = noise_level; - res.qr_status = 0; - - // ---- Q-less QR on J: dispatch on alg_name ---- - std::cout << "[Run " << r << ", " << alg_name << "] QR ... " << std::flush; - std::vector R(N * N, (T)0); - auto state = run_states[r]; - - RandLAPACK::PeakRSSTracker mem; mem.start(); - if (alg_name == "sCholQR3") { - RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(J, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[17]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(M, N, block_size); - } - } else if (alg_name == "sCholQR3_basic") { - RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); - res.qr_status = qr_algo.call(J, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[14]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(M, N); - } - } else if (alg_name == "CholQR") { - RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(J, R.data(), N); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[5]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 6); - res.qr_breakdown.resize(11, 0); - res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(M, N, block_size); - } - } else { - RandLAPACK::CQRRT_linops qr_algo(/*time_subroutines=*/true, tol); - qr_algo.nnz = sketch_nnz; - qr_algo.block_size = block_size; - if (alg_name == "CQRRT_linop") qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; - else /* CQRRT_linop_bqrrp */ qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; - res.qr_status = qr_algo.call(J, R.data(), N, d_factor, state); - res.peak_rss_kb = mem.stop(); - if (res.qr_status == 0) { - res.qr_time_us = qr_algo.times[10]; - res.qr_breakdown.assign(qr_algo.times.begin(), qr_algo.times.begin() + 11); - res.analytical_kb = (alg_name == "CQRRT_linop_bqrrp") - ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(M, N, d_factor, block_size) - : RandLAPACK::cqrrt_linops_analytical_kb(M, N, d_factor, block_size); - } - } - - if (res.qr_status != 0) { - std::cerr << "\n [" << alg_name << "] Run " << r - << ": QR returned status " << res.qr_status - << " (likely Cholesky breakdown).\n" - << " The input may be too ill-conditioned for the chosen variant;\n" - << " try a more stabilized variant (CQRRT_linop_bqrrp / sCholQR3) or a larger d_factor.\n"; - res.qr_time_us = -1; - res.qr_breakdown.assign(11, 0); - res.analytical_kb = 0; - res.orth_error = (T)-1.0; - res.ir_total_us = 0; - res.ir_outer_iters = 0; - res.ir_inner_iters_total = 0; - res.ls_residual_norm = (T)-1.0; - res.ls_solution_error = (T)-1.0; - print_summary(res); - return res; - } - res.orth_error = (T)-1.0; - std::cout << "done (" << res.qr_time_us << " us). IR-LSQ ... " << std::flush; - - // ---- Sketch-and-solve initial guess (Algorithm 1, line 3 of - // Epperly–Meier–Nakatsukasa 2025), with a fresh sparse sketch - // S₂ independent of CQRRT's S₁: - // SA = S₂·J (d_init × N) - // Sb = S₂·b (d_init × 1) - // x₀ = R⁻¹ R⁻ᵀ (SA)ᵀ Sb (Q-less form; R reused as preconditioner) - // Timing folded into ir_total_us alongside the IR steps. - auto ls_t0 = steady_clock::now(); - const int64_t d_init = (int64_t)(d_factor * (T)N); - RandBLAS::SparseDist DS_init(d_init, M, sketch_nnz); - auto x0_state = state; - x0_state.key.incr(0xA1B2C3D4u); - RandBLAS::SparseSkOp S2(DS_init, x0_state); - RandBLAS::fill_sparse(S2); - - std::vector SA(d_init * N, (T)0); - J(blas::Side::Right, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - d_init, N, M, (T)1.0, S2, (T)0.0, SA.data(), d_init); - - std::vector Sb(d_init, (T)0); - RandBLAS::sketch_general(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - d_init, (int64_t)1, M, (T)1.0, - S2, b.data(), M, (T)0.0, Sb.data(), d_init); - - std::vector x_ls(N, (T)0); - blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, d_init, N, - (T)1.0, SA.data(), d_init, Sb.data(), 1, - (T)0.0, x_ls.data(), 1); - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, - blas::Op::Trans, blas::Diag::NonUnit, N, 1, - (T)1.0, R.data(), N, x_ls.data(), N); - blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, - blas::Op::NoTrans, blas::Diag::NonUnit, N, 1, - (T)1.0, R.data(), N, x_ls.data(), N); - - // ---- IterRefineLSQ ---- - RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, - /*max_inner=*/200, - /*n_steps=*/2, - /*timing=*/true, - /*verbose=*/false); - int ir_status = ir.call(J, R.data(), N, b.data(), M, x_ls.data(), N); - auto ls_t1 = steady_clock::now(); - if (ir_status != 0) { - std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; - } - - res.ir_total_us = duration_cast(ls_t1 - ls_t0).count(); - res.ir_outer_iters = ir.outer_iters_done; - res.ir_inner_iters_total = 0; - for (int v : ir.inner_iters_per_step) res.ir_inner_iters_total += v; - res.ls_residual_norm = ir.final_residual_norm; - if (!ir.times.empty()) res.ir_breakdown = ir.times; - - T err_sq = 0; - for (int64_t i = 0; i < N; ++i) { - T d = x_ls[i] - x_true[i]; - err_sq += d * d; - } - res.ls_solution_error = std::sqrt(err_sq) / x_true_norm; - std::cout << "done (" << res.ir_total_us << " us)\n"; - - print_summary(res); - return res; - }; - - // Build the ordered list of selected algorithm names from the bitmask. - std::vector selected_algs; - if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); - if (method_mask & 2) selected_algs.push_back("CholQR"); - if (method_mask & 4) selected_algs.push_back("sCholQR3"); - if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); - if (method_mask & 64) selected_algs.push_back("CQRRT_linop_bqrrp"); - - if (selected_algs.empty()) { - std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; - return 1; - } - - std::vector> all_results; - for (const auto& alg_name : selected_algs) { - std::cout << "\n=== Algorithm: " << alg_name << " ===\n"; - for (int64_t r = 0; r < num_runs; ++r) { - all_results.push_back(run_one(alg_name, r)); - } - } - - // -------- CSV output -------- - char time_buf[64]; - time_t now = time(nullptr); - strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); - - std::string results_file = output_dir + "/" + time_buf + "_irlsq_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_irlsq_breakdown.csv"; - - { - std::ofstream out(results_file); - out << "# Sparse IR-LSQ Benchmark results\n" - << "# Date: " << ctime(&now) - << "# mtx_path=" << mtx_path << "\n" - << "# M=" << M << " N=" << N << " nnz=" << nnz << "\n" - << "# noise_level=" << noise_level << "\n" - << "# d_factor=" << d_factor << " sketch_nnz=" << sketch_nnz - << " block_size=" << block_size << "\n" - << "# method_mask=" << method_mask << "\n" -#ifdef _OPENMP - << "# OpenMP threads: " << omp_get_max_threads() << "\n" -#else - << "# OpenMP threads: 1\n" -#endif - ; - out << "algorithm,run,m,n,qr_status,qr_time_us,peak_rss_kb,analytical_kb," - "ir_total_us,ir_outer_iters,ir_inner_iters_total," - "ls_residual_norm,ls_solution_error\n"; - for (const auto& r : all_results) { - out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," - << r.qr_status << "," << r.qr_time_us << "," << r.peak_rss_kb << "," << r.analytical_kb << "," - << r.ir_total_us << "," << r.ir_outer_iters << "," << r.ir_inner_iters_total << "," - << std::scientific << std::setprecision(6) << r.ls_residual_norm << "," - << std::scientific << std::setprecision(6) << r.ls_solution_error - << "\n"; - } - std::cout << "\nResults written to " << results_file << "\n"; - } - - { - std::ofstream out(breakdown_file); - out << "# Sparse IR-LSQ Benchmark runtime breakdown (microseconds)\n" - << "# QR breakdown layout depends on algorithm (see CQRRT_linop_applications.cc).\n" - << "# IR-LSQ breakdown (6): outer_total, inner_cg_total, trsm_total, fwd_total, adj_total, other\n" - << "# (sketch-and-solve x_0 time is folded into the difference between ir_total_us\n" - << "# in the results CSV and outer_total here)\n" - << "algorithm,run,phase,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,t10\n"; - for (const auto& r : all_results) { - out << r.alg_name << "," << r.run_idx << ",QR"; - for (size_t i = 0; i < r.qr_breakdown.size(); ++i) out << "," << r.qr_breakdown[i]; - for (size_t i = r.qr_breakdown.size(); i < 11; ++i) out << ",0"; - out << "\n"; - out << r.alg_name << "," << r.run_idx << ",IR"; - for (size_t i = 0; i < r.ir_breakdown.size(); ++i) out << "," << r.ir_breakdown[i]; - for (size_t i = r.ir_breakdown.size(); i < 11; ++i) out << ",0"; - out << "\n"; - } - std::cout << "Breakdown written to " << breakdown_file << "\n"; - } - - return 0; -} - - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] - << " " - << " [noise_level] [d_factor] [sketch_nnz] [block_size] [method_mask]\n"; - return 1; - } - std::string prec = argv[1]; - if (prec == "double") return run_benchmark(argc, argv); - if (prec == "float") return run_benchmark(argc, argv); - std::cerr << "Unknown precision: " << prec << " (use 'double' or 'float')\n"; - return 1; -} From 527a0b5b4ec8169dd58626658a9b4fc0c54ee698 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 1 Jun 2026 11:58:18 -0700 Subject: [PATCH 20/47] Refactor CholQR family around shared PCholQR primitive (collaborator's spec) Introduces comps/rl_cholqr.hh with three free-function templates: - blocked_preconditioned_gram(A, R_pre, G, ...) : Layer 0; computes G = R_pre^T A^T A R_pre via blocked linop calls. nullptr R_pre handles the M=I case via a small per-block identity scratch. - cholqr_primitive(A, R, shift_factor, ...) : Algorithm 1 (with optional shift for sCholQR3 iter 1). - pcholqr_primitive(A, P, R, method, ...) : Algorithm 2; the unifying building block. Dispatches the P^{-1} step on PCholQRPrecondMethod (TRSM_IDENTITY / TRTRI / GEQP3 / BQRRP). Adds the new TRTRI method. Gram step in pcholqr_primitive dispatches on method: - TRSM_IDENTITY/TRTRI: per-block writes A^T A R_pre into G, then a single TRSM(P^T, G) applies the left factor. O(n^3/2) vs O(n^3), stable since P is preserved (the optimization CQRRT_linops used to have inline; now shared with sCholQR3 iters 2-3). - GEQP3/BQRRP: per-block GEMM with explicit R_pre^T, preserving the QRCP stability advantage. Driver refactor: - CholQR_linops, sCholQR3_linops (both variants), CQRRT_linops are now thin wrappers over the primitives. ~33% LOC reduction across the family (1946 -> 1310 lines). - rl_cqrrt.hh now holds both dense CQRRT and CQRRT_linops in one file; rl_cqrrt_linops.hh deleted (CMake had no separate target; 3 benchmark .cc files updated to drop the include). - Backwards-compat alias `using CQRRTLinopPrecond = PCholQRPrecondMethod;` keeps existing benchmark code unchanged. Memory tracker formulas updated to reflect the now-freed buffers: scholqr3_linops_analytical_kb: 6n^2 + (m+n)*b -> 2n^2 + (m+n)*b scholqr3_linops_basic_analytical_kb: m*n + 6n^2 -> m*n + 2n^2 Tests: - 3 new tests in test_orth_linop.cc cover TRTRI / GEQP3 / BQRRP precond paths (existing tests only exercised TRSM_IDENTITY). - All 26 CholQR/sCholQR3/CQRRT tests pass. --- RandLAPACK.hh | 4 +- RandLAPACK/comps/rl_cholqr.hh | 392 ++++++++++ RandLAPACK/drivers/rl_cholqr_linops.hh | 272 ++----- RandLAPACK/drivers/rl_cqrrt.hh | 348 ++++++--- RandLAPACK/drivers/rl_cqrrt_linops.hh | 556 -------------- RandLAPACK/drivers/rl_scholqr3_linops.hh | 720 ++++-------------- RandLAPACK/testing/rl_memory_tracker.hh | 16 +- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 1 - .../CQRRT_linop_applications.cc | 1 - .../bench_CQRRT_linops/CQRRT_linop_basic.cc | 1 - test/drivers/test_orth_linop.cc | 67 ++ 11 files changed, 902 insertions(+), 1476 deletions(-) create mode 100644 RandLAPACK/comps/rl_cholqr.hh delete mode 100644 RandLAPACK/drivers/rl_cqrrt_linops.hh diff --git a/RandLAPACK.hh b/RandLAPACK.hh index b840d73ee..fc35f1576 100644 --- a/RandLAPACK.hh +++ b/RandLAPACK.hh @@ -28,12 +28,12 @@ #include "RandLAPACK/comps/rl_syrf.hh" #include "RandLAPACK/comps/rl_orth.hh" #include "RandLAPACK/comps/rl_rpchol.hh" +#include "RandLAPACK/comps/rl_cholqr.hh" // Drivers #include "RandLAPACK/drivers/rl_rsvd.hh" -#include "RandLAPACK/drivers/rl_cqrrt.hh" +#include "RandLAPACK/drivers/rl_cqrrt.hh" // holds both dense CQRRT and CQRRT_linops #include "RandLAPACK/drivers/rl_cholqr_linops.hh" -#include "RandLAPACK/drivers/rl_cqrrt_linops.hh" #include "RandLAPACK/drivers/rl_iter_refine_lsq.hh" #include "RandLAPACK/drivers/rl_scholqr3_linops.hh" #include "RandLAPACK/drivers/rl_cqrrpt.hh" diff --git a/RandLAPACK/comps/rl_cholqr.hh b/RandLAPACK/comps/rl_cholqr.hh new file mode 100644 index 000000000..b60e63c5d --- /dev/null +++ b/RandLAPACK/comps/rl_cholqr.hh @@ -0,0 +1,392 @@ +#pragma once + +#include "rl_util.hh" +#include "rl_blaspp.hh" +#include "rl_lapackpp.hh" +#include "rl_linops.hh" +#include "rl_bqrrp.hh" + +#include +#include +#include +#include +#include + +namespace RandLAPACK { + +/// Method for inverting the upper-triangular preconditioner P inside pcholqr_primitive. +/// +/// TRSM_IDENTITY Solve P * R_pre = I via column-by-column TRSM(Side::Left). +/// Backward-stable for the subsequent product A * R_pre. O(n^3/2). +/// TRTRI Direct triangular inverse via LAPACK trtri. Same cost; less stable +/// than TRSM_IDENTITY when P is ill-conditioned in practice. +/// GEQP3 R_pre = P_perm * R_buf^{-1} * Q_buf^T via LAPACK geqp3 on P followed +/// by ungqr + TRSM + scatter. Stable for ill-conditioned P. O(11 n^3 / 6). +/// BQRRP Same output format as GEQP3 but via RandLAPACK::BQRRP (blocked + +/// sketched pivoting). Faster for large n; requires RNG state. +enum class PCholQRPrecondMethod { + TRSM_IDENTITY, + TRTRI, + GEQP3, + BQRRP +}; + + +// ============================================================================ +// Layer 0 — blocked_preconditioned_gram +// ============================================================================ +// +// Compute the Gram matrix G = R_pre^T * (A^T A) * R_pre via blocked linop calls. +// When R_pre is nullptr, computes the unpreconditioned Gram G = A^T A (M = I). +// +// Peak memory: O(n^2 + (m + n) * b_eff) +// - A_temp: m × b_eff scratch for A * R_pre[:, j:j+b] +// - Z_buf: n × b_eff scratch for A^T * A_temp (used only when R_pre != nullptr; +// when R_pre == nullptr we write A^T * A_temp directly into G) +// - When R_pre == nullptr an n × b_eff identity scratch is allocated internally. +// +// Timing accumulators are output parameters; ignored when timing == false. +template +void blocked_preconditioned_gram( + GLO& A, + const T* R_pre, + T* G, + int64_t m, int64_t n, int64_t b_eff, + T* A_temp, + T* Z_buf, + bool skip_left_factor, + long& fwd_us, long& adj_us, long& gemm_us, + bool timing) +{ + using std::chrono::steady_clock; + using std::chrono::duration_cast; + using std::chrono::microseconds; + steady_clock::time_point t0, t1; + long fwd_accum = 0, adj_accum = 0, gemm_accum = 0; + + T* I_block = nullptr; + if (R_pre == nullptr) { + I_block = new T[n * b_eff](); + } + + for (int64_t j = 0; j < n; j += b_eff) { + int64_t b_j = std::min(b_eff, n - j); + + const T* B_in; + if (R_pre) { + B_in = R_pre + j * n; + } else { + for (int64_t c = 0; c < b_j; ++c) + I_block[(j + c) + c * n] = (T)1.0; + B_in = I_block; + } + + // A_temp = A * B_in (m × b_j) + if (timing) t0 = steady_clock::now(); + A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, b_j, n, (T)1.0, B_in, n, (T)0.0, A_temp, m); + if (timing) { t1 = steady_clock::now(); fwd_accum += duration_cast(t1 - t0).count(); } + + if (R_pre && !skip_left_factor) { + // Z_buf = A^T * A_temp (n × b_j) + if (timing) t0 = steady_clock::now(); + A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, b_j, m, (T)1.0, A_temp, m, (T)0.0, Z_buf, n); + if (timing) { t1 = steady_clock::now(); adj_accum += duration_cast(t1 - t0).count(); } + + // G[:, j:j+b_j] = R_pre^T * Z_buf (n × b_j) + if (timing) t0 = steady_clock::now(); + blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, + n, b_j, n, (T)1.0, R_pre, n, Z_buf, n, (T)0.0, G + j * n, n); + if (timing) { t1 = steady_clock::now(); gemm_accum += duration_cast(t1 - t0).count(); } + } else if (R_pre) { + // skip_left_factor: write Z directly into G[:, j:j+b_j]. Caller applies the left + // factor (typically via TRSM with the original P) once the loop completes. + if (timing) t0 = steady_clock::now(); + A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, b_j, m, (T)1.0, A_temp, m, (T)0.0, G + j * n, n); + if (timing) { t1 = steady_clock::now(); adj_accum += duration_cast(t1 - t0).count(); } + } else { + // G[:, j:j+b_j] = A^T * A_temp (direct, M = I case) + if (timing) t0 = steady_clock::now(); + A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, b_j, m, (T)1.0, A_temp, m, (T)0.0, G + j * n, n); + if (timing) { t1 = steady_clock::now(); adj_accum += duration_cast(t1 - t0).count(); } + + // Restore I_block columns to zero for next iter + for (int64_t c = 0; c < b_j; ++c) + I_block[(j + c) + c * n] = (T)0.0; + } + } + + if (I_block) delete[] I_block; + + if (timing) { + fwd_us = fwd_accum; + adj_us = adj_accum; + gemm_us = gemm_accum; + } +} + + +// ============================================================================ +// Layer 1a — cholqr_primitive: Algorithm 1 / sCholQR3 iter-1 (shifted) +// ============================================================================ +// +// Computes R upper-triangular such that A * R^{-1} has orthonormal columns: +// G = A^T A (+ shift * I if shift_factor > 0) +// G = R^T R (Cholesky) +// +// shift_factor: caller-supplied multiplier on trace(G) = ||A||_F^2. The sCholQR3 +// iter-1 spec wants s = 11 * eps * n * ||A||_F^2 so the caller +// passes shift_factor = 11 * eps * n. Pass 0 for unshifted CholQR. +// +// Workspaces (caller-owned): +// R — n × n output buffer (lower triangle returned zeroed) +// G — n × n Gram scratch +// A_temp — m × b_eff +// +// Returns potrf info (0 on success; >0 on Cholesky breakdown). +template +int cholqr_primitive( + GLO& A, + T* R, int64_t ldr, + T shift_factor, + int64_t block_size, + T* G, + T* A_temp, + long& fwd_us, long& adj_us, long& chol_us, + bool timing) +{ + using std::chrono::steady_clock; + using std::chrono::duration_cast; + using std::chrono::microseconds; + int64_t m = A.n_rows; + int64_t n = A.n_cols; + int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; + + long gemm_unused = 0; + blocked_preconditioned_gram(A, (const T*)nullptr, G, m, n, b_eff, + A_temp, (T*)nullptr, + /*skip_left_factor=*/false, + fwd_us, adj_us, gemm_unused, timing); + + steady_clock::time_point t0, t1; + if (timing) t0 = steady_clock::now(); + + if (shift_factor > T(0)) { + T trace = 0; + for (int64_t i = 0; i < n; ++i) trace += G[i * (n + 1)]; + T shift = shift_factor * trace; + for (int64_t i = 0; i < n; ++i) G[i * (n + 1)] += shift; + } + + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), &G[1], n); + int info = lapack::potrf(Uplo::Upper, n, G, n); + if (info) return info; + + lapack::lacpy(MatrixType::Upper, n, n, G, n, R, ldr); + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), R + 1, ldr); + + if (timing) { + t1 = steady_clock::now(); + chol_us = duration_cast(t1 - t0).count(); + } + return 0; +} + + +// ============================================================================ +// Layer 1b — pcholqr_primitive: Algorithm 2 (preconditioned Q-less Cholesky QR) +// ============================================================================ +// +// Given upper-triangular P (n × n), computes R upper-triangular such that +// A * R^{-1} has orthonormal columns. Mirrors the collaborator's pseudocode: +// 1. R_pre = invert(P) via 'method' (TRSM_IDENTITY / TRTRI / GEQP3 / BQRRP) +// 2. G = R_pre^T * A^T * A * R_pre (blocked_preconditioned_gram) +// 3. G = (R^chol)^T R^chol (Cholesky) +// 4. R = R^chol * P (in-place trmm into R) +// +// Workspaces (caller-owned): +// R_pre — n × n (output of step 1; input to step 2) +// G — n × n (Gram scratch, then R^chol after potrf) +// A_temp — m × b_eff +// Z_buf — n × b_eff +// +// state: RNG state for the BQRRP method only. Pass &state for BQRRP; nullptr is OK +// for TRSM_IDENTITY / TRTRI / GEQP3. +// +// bqrrp_block_ratio: BQRRP block-size knob. Pass 1.0 to use the CQRRPT-style +// adaptive heuristic (1.0 for n<=2000, 0.5 for n<=8000, 1/32 else). +// +// Returns 0 on success; nonzero on diag-zero check failure / Cholesky breakdown. +template +int pcholqr_primitive( + GLO& A, + const T* P, + T* R, int64_t ldr, + PCholQRPrecondMethod method, + int64_t block_size, + T bqrrp_block_ratio, + T* R_pre, + T* G, + T* A_temp, + T* Z_buf, + RandBLAS::RNGState* state, + long& precond_inv_us, + long& fwd_us, long& adj_us, long& gemm_us, long& chol_us, long& update_us, + bool timing) +{ + using std::chrono::steady_clock; + using std::chrono::duration_cast; + using std::chrono::microseconds; + int64_t m = A.n_rows; + int64_t n = A.n_cols; + int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; + + steady_clock::time_point t0, t1; + if (timing) t0 = steady_clock::now(); + + // ---- Step 1: R_pre = invert(P) ---- + switch (method) { + case PCholQRPrecondMethod::TRSM_IDENTITY: { + if (!RandLAPACK::util::diag_is_nonzero(n, P, n)) return 1; + RandLAPACK::util::eye(n, n, R_pre); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, + Op::NoTrans, Diag::NonUnit, + n, n, T(1), P, n, R_pre, n); + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), R_pre + 1, n); + break; + } + case PCholQRPrecondMethod::TRTRI: { + if (!RandLAPACK::util::diag_is_nonzero(n, P, n)) return 1; + lapack::lacpy(MatrixType::Upper, n, n, P, n, R_pre, n); + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), R_pre + 1, n); + int trtri_info = lapack::trtri(Uplo::Upper, Diag::NonUnit, n, R_pre, n); + if (trtri_info) return 1; + break; + } + case PCholQRPrecondMethod::GEQP3: + case PCholQRPrecondMethod::BQRRP: { + T* P_copy = new T[n * n](); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + P_copy[i + j * n] = P[i + j * n]; + if (!RandLAPACK::util::diag_is_nonzero(n, P_copy, n)) { delete[] P_copy; return 1; } + + int64_t* jpiv = new int64_t[n](); + T* tau_qr = new T[n]; + + if (method == PCholQRPrecondMethod::GEQP3) { + lapack::geqp3(n, n, P_copy, n, jpiv, tau_qr); + } else { + if (state == nullptr) { + delete[] P_copy; delete[] jpiv; delete[] tau_qr; + return 1; + } + T ratio = bqrrp_block_ratio; + if (ratio == T(1.0)) { + if (n <= 2000) ratio = T(1.0); + else if (n <= 8000) ratio = T(0.5); + else ratio = T(1.0) / T(32); + } + int64_t bqrrp_b = std::max(int64_t(1), (int64_t)(n * ratio)); + RandLAPACK::BQRRP bqrrp(false, bqrrp_b); + bqrrp.call(n, n, P_copy, n, T(1), tau_qr, jpiv, *state); + } + + T* R_buf = new T[n * n](); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + R_buf[i + j * n] = P_copy[i + j * n]; + lapack::ungqr(n, n, n, P_copy, n, tau_qr); + + T* W = new T[n * n]; + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + W[i + j * n] = P_copy[j + i * n]; + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, + Op::NoTrans, Diag::NonUnit, + n, n, T(1), R_buf, n, W, n); + + std::fill(R_pre, R_pre + n * n, T(0)); + for (int64_t k = 0; k < n; ++k) + for (int64_t j = 0; j < n; ++j) + R_pre[(jpiv[k] - 1) + j * n] = W[k + j * n]; + + delete[] P_copy; + delete[] R_buf; + delete[] W; + delete[] jpiv; + delete[] tau_qr; + break; + } + } + + if (timing) { + t1 = steady_clock::now(); + precond_inv_us = duration_cast(t1 - t0).count(); + } + + // ---- Step 2: G = R_pre^T * A^T * A * R_pre ---- + // + // Dispatch by precond method on how to apply the *left* R_pre^T factor: + // + // TRSM_IDENTITY / TRTRI : per-block step writes A^T A R_pre directly into G, + // then a single TRSM(P^T, G) applies P^{-T} = R_pre^T at the end. + // Cheaper (O(n^3/2)) and equally stable since P is preserved. + // GEQP3 / BQRRP : per-block GEMM with the explicit R_pre^T preserves the + // QRCP stability advantage (R_pre is computed accurately by pivoting, + // but TRSM(P^T, ...) would re-amplify the kappa(P) error we just dodged). + bool use_trsm_at_end = (method == PCholQRPrecondMethod::TRSM_IDENTITY + || method == PCholQRPrecondMethod::TRTRI); + + blocked_preconditioned_gram(A, R_pre, G, m, n, b_eff, + A_temp, Z_buf, + /*skip_left_factor=*/use_trsm_at_end, + fwd_us, adj_us, gemm_us, timing); + + if (use_trsm_at_end) { + // G := P^{-T} * G (= R_pre^T * A^T * A * R_pre, since R_pre = P^{-1}) + if (timing) t0 = steady_clock::now(); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, + Diag::NonUnit, n, n, T(1), P, n, G, n); + if (timing) { + t1 = steady_clock::now(); + gemm_us += duration_cast(t1 - t0).count(); + } + } + + // ---- Step 3: G = (R^chol)^T R^chol ---- + if (timing) t0 = steady_clock::now(); + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), &G[1], n); + int info = lapack::potrf(Uplo::Upper, n, G, n); + if (info) return info; + if (timing) { + t1 = steady_clock::now(); + chol_us = duration_cast(t1 - t0).count(); + } + + // ---- Step 4: R = R^chol * P (in-place trmm; R starts as P) ---- + if (timing) t0 = steady_clock::now(); + lapack::lacpy(MatrixType::Upper, n, n, P, n, R, ldr); + if (n > 1) + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), R + 1, ldr); + blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, + Op::NoTrans, Diag::NonUnit, + n, n, T(1), G, n, R, ldr); + if (timing) { + t1 = steady_clock::now(); + update_us = duration_cast(t1 - t0).count(); + } + + return 0; +} + +} // namespace RandLAPACK diff --git a/RandLAPACK/drivers/rl_cholqr_linops.hh b/RandLAPACK/drivers/rl_cholqr_linops.hh index 5dc7a3494..b0dabfc74 100644 --- a/RandLAPACK/drivers/rl_cholqr_linops.hh +++ b/RandLAPACK/drivers/rl_cholqr_linops.hh @@ -4,6 +4,7 @@ #include "rl_blaspp.hh" #include "rl_lapackpp.hh" #include "rl_linops.hh" +#include "../comps/rl_cholqr.hh" #include #include @@ -16,20 +17,12 @@ namespace RandLAPACK { /// Cholesky QR factorization for abstract linear operators. /// -/// Computes A = QR where A is any type satisfying the LinearOperator concept -/// (dense, sparse, composite, etc.). Unlike standard Cholesky QR (rl_cqrrt.hh), -/// which requires A to be a dense matrix that can be modified in place, this -/// class works through the operator interface: it only calls A(NoTrans, ...) -/// and A(Trans, ...) to form the Gram matrix G = A^T A, then factors G = R^T R -/// via Cholesky. +/// Thin wrapper around `cholqr_primitive` (Algorithm 1 from the collaborator's spec): +/// G = A^T A, G = R^T R via Cholesky. /// -/// This is useful when A is too large to store densely, is only available as a -/// matrix-vector product (e.g., the product of two factors, or a sparse matrix), -/// or when the caller wants a uniform interface across operator types. -/// -/// The Q factor is not computed by default (Q-less factorization). When -/// test_mode is enabled, Q = A * R^{-1} is materialized explicitly for -/// verification. +/// A may be any type satisfying the LinearOperator concept (dense, sparse, composite, +/// etc.). Q is not computed by default; when test_mode is enabled, Q = A * R^{-1} +/// is materialized for verification. /// template class CholQR_linops { @@ -45,34 +38,9 @@ class CholQR_linops { int64_t Q_cols; // 6 entries: alloc, fwd, adj, chol, rest, total - // fwd = LinOp NoTrans: A * I (accumulated over blocks) - // adj = LinOp Trans: A^T * buf (accumulated over blocks) std::vector times; - // Column-block size for the materialize + Gram computation. - // - // When block_size > 0, the two expensive operations: - // (1) A_temp = A * I (m × n) - materialize the operator - // (2) R = A^T * A_temp (n × n) - Gram matrix - // are fused into a column-block loop that processes b columns at a time: - // for each column block j of width b: - // buf (m × b) = A * I[:, j*b : (j+1)*b] - // R[:, j*b : (j+1)*b] = A^T * buf - // - // This reduces peak memory from O(m*n) to O(m*b), which is significant - // when m is large and n is moderate. The result is mathematically - // identical — each column block of R is: - // R[:, j_block] = A^T * (A * I[:, j_block]) - // which equals the corresponding columns of A^T * A. - // - // When block_size <= 0 or block_size >= n, the full m × n buffer is - // allocated and the original (non-blocked) path is used. - // - // When test_mode is enabled and blocking is active, the Q-factor - // computation (which needs the full m × n A_temp) is handled by - // recomputing A_temp = A * I after the Gram loop. This - // recomputation is outside the timing region, so it does not - // affect benchmark results. + // Column-block size for Gram and Q materialization. <=0 or >=n means no blocking. int64_t block_size; CholQR_linops( @@ -95,227 +63,79 @@ class CholQR_linops { } } - /// Computes an R-factor of the unpivoted QR factorization using unpreconditioned Cholesky QR: - /// A = QR, - /// where Q and R are of size m-by-n and n-by-n. - /// - /// This is the baseline unpreconditioned version for comparison with CQRRT. - /// Algorithm: - /// 1. Compute Gram matrix: G = A^T * A - /// 2. Compute Cholesky factorization: G = R^T * R - /// 3. (Optional) Compute Q = A * R^{-1} - /// - /// @note This algorithm expects A to be full-rank (rank = n). Rank-deficient inputs may result - /// in loss of orthogonality in the Q-factor (when test_mode=true) and numerical instability - /// in the R-factor. - /// - /// @param[in] A - /// The m-by-n linear operator (m and n read from A.n_rows, A.n_cols). - /// - /// @param[out] R - /// Pre-allocated n-by-n buffer. On exit, stores the upper-triangular - /// R factor. Zero entries are not compressed. - /// - /// @param[in] ldr - /// Leading dimension of R. - /// - /// @return = 0: successful exit template int call( GLO& A, T* R, int64_t ldr ) { - ///--------------------TIMING VARS--------------------/ - steady_clock::time_point alloc_t_start; - steady_clock::time_point alloc_t_stop; - steady_clock::time_point potrf_t_start; - steady_clock::time_point potrf_t_stop; - steady_clock::time_point total_t_start; - steady_clock::time_point total_t_stop; - steady_clock::time_point t_start, t_stop; - long alloc_t_dur = 0; - long fwd_t_dur = 0; - long adj_t_dur = 0; - long potrf_t_dur = 0; - long total_t_dur = 0; - long q_t_dur = 0; - steady_clock::time_point q_t_start; - steady_clock::time_point q_t_stop; + steady_clock::time_point t0, t1, total_t_start, total_t_stop; + long alloc_dur = 0, fwd_dur = 0, adj_dur = 0, chol_dur = 0, total_dur = 0; + long q_dur = 0; - if(this->timing) - total_t_start = steady_clock::now(); + if (this->timing) total_t_start = steady_clock::now(); int64_t m = A.n_rows; int64_t n = A.n_cols; - - // Compute Gram matrix: R = A^T * A - // We cannot use syrk since A may be a sparse operator - // Instead, compute R = A^T * A via two matvec operations: - // 1. Create identity matrix I (n x n) - // 2. Compute A_temp = A * I (materializes A as m x n dense) - // 3. Compute R = A^T * A_temp - - if(this->timing) - alloc_t_start = steady_clock::now(); - - // Create identity matrix (needs zero-init for off-diagonal elements) - T* I_mat = new T[n * n](); - RandLAPACK::util::eye(n, n, I_mat); - - // Gram computation: R = A^T * A - - // Determine effective block width. - // block_size <= 0 or >= n means "no blocking" (full width). int64_t b_eff = (this->block_size > 0 && this->block_size < n) ? this->block_size : n; - // A_temp buffer: - // Full path: m × n (kept alive for Q-factor in test_mode) - // Block path: m × b_eff (temporary, freed after Gram loop; - // if test_mode, a full m × n buffer is - // allocated later for Q computation) - // No zero-init needed: first use is with beta=0.0 which overwrites all elements. + // Allocate workspaces for the primitive: G (n×n) and A_temp (m×b_eff). + if (this->timing) t0 = steady_clock::now(); + T* G = new T[n * n](); T* A_temp = new T[m * b_eff]; + if (this->timing) { t1 = steady_clock::now(); alloc_dur = duration_cast(t1 - t0).count(); } - if(this->timing) { - alloc_t_stop = steady_clock::now(); - } + int info = cholqr_primitive( + A, R, ldr, + /*shift_factor=*/T(0), + this->block_size, + G, A_temp, + fwd_dur, adj_dur, chol_dur, this->timing); - if (b_eff == n) { - // --- Full materialization path (original) --- + if (info != 0) { + delete[] G; + delete[] A_temp; + return info; + } - // Step 1: Materialize A by computing A_temp = A * I - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, I_mat, n, (T)0.0, A_temp, m); - if(this->timing) { t_stop = steady_clock::now(); fwd_t_dur = duration_cast(t_stop - t_start).count(); } + // Test mode: materialize Q = A * R^{-1}. Recompute A * I into a full m×n buffer + // (outside the timing region) then apply R^{-1} via trsm. + if (this->test_mode) { + if (this->timing) t0 = steady_clock::now(); - // Step 2: Compute R = A^T * A_temp (using the linear operator's transpose) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, n, n, m, (T)1.0, A_temp, m, (T)0.0, R, ldr); - if(this->timing) { t_stop = steady_clock::now(); adj_t_dur = duration_cast(t_stop - t_start).count(); } - } else { - // --- Column-block processing path (memory-efficient) --- - // - // Process n columns in blocks of width b_eff. - // The last block may be narrower if n is not divisible by b_eff. - // - // For each block starting at column j with width b_j: - // (1) buf (m × b_j) = A * I[:, j : j+b_j] - // I is n × n in ColMajor with ld = n. - // Column j starts at I + j * n. - // We multiply A (m × n) by this n × b_j slice. - // - // (2) R[:, j : j+b_j] (n × b_j) = A^T * buf - // A^T is n × m, buf is m × b_j, result is n × b_j. - // R is n × n with ld = ldr. - // Column j starts at R + j * ldr. - // - // Total FLOPs are the same as the full path; only memory differs. + T* Q_buf = new T[m * n]; + T* I_mat = new T[n * n](); + RandLAPACK::util::eye(n, n, I_mat); - long fwd_accum = 0, adj_accum = 0; for (int64_t j = 0; j < n; j += b_eff) { int64_t b_j = std::min(b_eff, n - j); - - // (1) buf = A * I[:, j : j+b_j] - if(this->timing) t_start = steady_clock::now(); A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, I_mat + j * n, n, (T)0.0, A_temp, m); - if(this->timing) { t_stop = steady_clock::now(); fwd_accum += duration_cast(t_stop - t_start).count(); } - - // (2) R[:, j : j+b_j] = A^T * buf - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, m, (T)1.0, A_temp, m, (T)0.0, R + j * ldr, ldr); - if(this->timing) { t_stop = steady_clock::now(); adj_accum += duration_cast(t_stop - t_start).count(); } - } - if(this->timing) { - fwd_t_dur = fwd_accum; - adj_t_dur = adj_accum; + m, b_j, n, (T)1.0, I_mat + j * n, n, (T)0.0, Q_buf + j * m, m); } - } - - // Zero out the lower triangle before Cholesky (potrf only uses upper triangle) - if (n > 1) { - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &R[1], ldr); - } - - if(this->timing) { - potrf_t_start = steady_clock::now(); - } + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_buf, m); - // Compute Cholesky factorization: G = R^T * R - // On exit, the upper triangle of R contains the R-factor - if (lapack::potrf(Uplo::Upper, n, R, ldr)) { delete[] I_mat; - delete[] A_temp; - return 1; - } - - if(this->timing) - potrf_t_stop = steady_clock::now(); - - // Compute Q-factor if test mode is enabled (NOT included in cholqr timing) - if(this->test_mode) { - if(this->timing) - q_t_start = steady_clock::now(); - - if (b_eff < n) { - // Column-block Gram was used: A_temp is only m × b_eff, - // too small for Q. Recompute A_temp = A * I in - // full. This is outside the timing region, so the extra - // operator application does not affect benchmark results. - delete[] A_temp; - // No zero-init: beta=0.0 overwrites all elements - A_temp = new T[m * n]; - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, I_mat, n, (T)0.0, A_temp, m); - } - this->Q_rows = m; this->Q_cols = n; - this->Q = A_temp; // Take ownership of A_temp buffer - - // Solve Q * R = A_temp for Q - // Q = A * R^{-1} - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, m, n, (T)1.0, R, ldr, this->Q, m); + this->Q = Q_buf; - if(this->timing) - q_t_stop = steady_clock::now(); + if (this->timing) { t1 = steady_clock::now(); q_dur = duration_cast(t1 - t0).count(); } } - if(this->timing) { - // Stop timing BEFORE cleanup operations to exclude deallocation costs + if (this->timing) { total_t_stop = steady_clock::now(); + total_dur = duration_cast(total_t_stop - total_t_start).count(); + // Subtract test-mode Q materialization (not part of algorithmic cost). + total_dur -= q_dur; - alloc_t_dur = duration_cast(alloc_t_stop - alloc_t_start).count(); - // fwd_t_dur and adj_t_dur already set (in both full and blocked paths) - potrf_t_dur = duration_cast(potrf_t_stop - potrf_t_start).count(); - total_t_dur = duration_cast(total_t_stop - total_t_start).count(); - - // Subtract Q-factor computation time if in test mode - if(this->test_mode) { - q_t_dur = duration_cast(q_t_stop - q_t_start).count(); - total_t_dur -= q_t_dur; - } - - long rest_t_dur = total_t_dur - (alloc_t_dur + fwd_t_dur + adj_t_dur + potrf_t_dur); - - // Fill the data vector: [alloc, fwd, adj, chol, rest, total] - this->times = {alloc_t_dur, fwd_t_dur, adj_t_dur, potrf_t_dur, rest_t_dur, total_t_dur}; - } - - // Cleanup - now outside the timing region to avoid timing artifacts - delete[] I_mat; - - // Only delete A_temp if not in test mode (otherwise Q owns it). - // When test_mode + blocking: the small block buffer was freed - // and replaced with a full m × n buffer in the Q section above. - if(!this->test_mode) { - delete[] A_temp; + long rest_dur = total_dur - (alloc_dur + fwd_dur + adj_dur + chol_dur); + this->times = {alloc_dur, fwd_dur, adj_dur, chol_dur, rest_dur, total_dur}; } + delete[] G; + delete[] A_temp; return 0; } }; diff --git a/RandLAPACK/drivers/rl_cqrrt.hh b/RandLAPACK/drivers/rl_cqrrt.hh index d1dbc55f1..73d6e980b 100644 --- a/RandLAPACK/drivers/rl_cqrrt.hh +++ b/RandLAPACK/drivers/rl_cqrrt.hh @@ -6,6 +6,8 @@ #include "rl_lapackpp.hh" #include "rl_hqrrp.hh" #include "rl_bqrrp.hh" +#include "rl_linops.hh" +#include "../comps/rl_cholqr.hh" #include #include @@ -17,6 +19,20 @@ using namespace std::chrono; namespace RandLAPACK { +/// Backwards-compatible alias for the precond-method enum, which now lives in +/// comps/rl_cholqr.hh as PCholQRPrecondMethod (shared across CholQR/sCholQR3/CQRRT). +using CQRRTLinopPrecond = PCholQRPrecondMethod; + + +// ============================================================================ +// CQRRT — dense Q-less Cholesky QR with sketch preconditioning. +// ============================================================================ +// +// Operates on a dense column-major A (m × n). Modifies A in place via TRSM +// (A := A * R_sk^{-1}) so the Q factor is materialized implicitly inside A. +// This is faster than the linop variant when A is already dense in memory. +// +// Reference: arXiv:2111.11148. template class CQRRTalg { public: @@ -53,41 +69,9 @@ class CQRRT : public CQRRTalg { /// Computes an unpivoted QR factorization of the form: /// A= QR, /// where Q and R are of size m-by-n and n-by-n. - /// Detailed description of this algorithm may be found in https://arxiv.org/pdf/2111.11148. /// /// @note This algorithm expects A to be full-rank (rank = n). Rank-deficient inputs may result /// in loss of orthogonality in the Q-factor and numerical instability in the R-factor. - /// - /// @param[in] m - /// The number of rows in the matrix A. - /// - /// @param[in] n - /// The number of columns in the matrix A. - /// - /// @param[in] A - /// The m-by-n matrix A, stored in a column-major format. - /// - /// @param[in] d - /// Embedding dimension of a sketch, m >= d >= n. - /// - /// @param[in] R - /// Represents the upper-triangular R factor of QR factorization. - /// On entry, is empty and may not have any space allocated for it. - /// - /// @param[in] state - /// RNG state parameter, required for sketching operator generation. - /// - /// @param[out] A - /// Overwritten by an m-by-n orthogonal Q factor. - /// Matrix is stored explicitly. - /// - /// @param[out] R - /// Stores n-by-n matrix with upper-triangular R factor. - /// Zero entries are not compressed. - /// - /// @return = 0: successful exit - /// - int call( int64_t m, int64_t n, @@ -107,15 +91,9 @@ class CQRRT : public CQRRTalg { // Matches CQRRT_linops timing indices for direct comparison. std::vector times; - // tuning SASOS int64_t nnz; - - // Mode of operation that allows to use CQRRT for orthogonalization of the input matrix. bool orthogonalization; - - // If false, skip the Q-factor computation (R-only mode). - // When false, A is NOT overwritten with Q on output. - bool compute_Q; + bool compute_Q; // skip Q materialization when false (R-only mode) }; // ----------------------------------------------------------------------------- @@ -150,131 +128,75 @@ int CQRRT::call( steady_clock::time_point q_t_start, q_t_stop; steady_clock::time_point finalize_t_start, finalize_t_stop; steady_clock::time_point total_t_start, total_t_stop; - long saso_t_dur = 0; - long qr_t_dur = 0; - long precond_t_dur = 0; - long gram_t_dur = 0; - long potrf_t_dur = 0; - long q_t_dur = 0; - long finalize_t_dur = 0; - long total_t_dur = 0; - - if(this -> timing) - total_t_start = steady_clock::now(); + long saso_t_dur = 0, qr_t_dur = 0, precond_t_dur = 0, gram_t_dur = 0; + long potrf_t_dur = 0, q_t_dur = 0, finalize_t_dur = 0, total_t_dur = 0; - int64_t d = d_factor * n; + if(this -> timing) total_t_start = steady_clock::now(); + int64_t d = d_factor * n; T* A_hat = new T[d * n](); T* tau = new T[n](); - if(this -> timing) - saso_t_start = steady_clock::now(); - - /// Generating a SASO + // Sketch + small QR + if(this -> timing) saso_t_start = steady_clock::now(); RandBLAS::SparseDist DS(d, m, this->nnz); RandBLAS::SparseSkOp S(DS, state); state = S.next_state; + RandBLAS::sketch_general(Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S, 0, 0, A, lda, (T)0.0, A_hat, d); + if(this -> timing) { saso_t_stop = steady_clock::now(); qr_t_start = steady_clock::now(); } - /// Applying a SASO - RandBLAS::sketch_general( - Layout::ColMajor, Op::NoTrans, Op::NoTrans, - d, n, m, (T) 1.0, S, 0, 0, A, lda, (T) 0.0, A_hat, d - ); - - if(this -> timing) { - saso_t_stop = steady_clock::now(); - qr_t_start = steady_clock::now(); - } - - /// Performing QR on a sketch lapack::geqrf(d, n, A_hat, d, tau); + if(this -> timing) qr_t_stop = steady_clock::now(); - if(this -> timing) - qr_t_stop = steady_clock::now(); - - /// Extracting a k by k R representation - T* R_sk = R; + T* R_sk = R; lapack::lacpy(MatrixType::Upper, n, n, A_hat, d, R_sk, ldr); - if(this -> timing) - precond_t_start = steady_clock::now(); - - // Precondition: A := A * R_sk^{-1} + if(this -> timing) precond_t_start = steady_clock::now(); if (!RandLAPACK::util::diag_is_nonzero(n, R_sk, ldr)) { - delete[] A_hat; - delete[] tau; - return 1; - } - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, 1.0, R_sk, ldr, A, lda); - - if(this -> timing) { - precond_t_stop = steady_clock::now(); - gram_t_start = steady_clock::now(); + delete[] A_hat; delete[] tau; return 1; } + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, + m, n, 1.0, R_sk, ldr, A, lda); + if(this -> timing) { precond_t_stop = steady_clock::now(); gram_t_start = steady_clock::now(); } - // Gram matrix: G = A^T * A (SYRK, upper triangle only) blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, n, m, 1.0, A, lda, 0.0, R_sk, ldr); + if(this -> timing) { gram_t_stop = steady_clock::now(); potrf_t_start = steady_clock::now(); } - if(this -> timing) { - gram_t_stop = steady_clock::now(); - potrf_t_start = steady_clock::now(); - } - - // Cholesky factorization if (lapack::potrf(Uplo::Upper, n, R_sk, ldr)) { - delete[] A_hat; - delete[] tau; - return 1; + delete[] A_hat; delete[] tau; return 1; } + if(this -> timing) potrf_t_stop = steady_clock::now(); - if(this -> timing) - potrf_t_stop = steady_clock::now(); - - // Obtain the output Q-factor (only if requested, timed separately and excluded from total) if (this->compute_Q) { - if(this -> timing) - q_t_start = steady_clock::now(); - - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, 1.0, R_sk, ldr, A, lda); - - if(this -> timing) - q_t_stop = steady_clock::now(); + if(this -> timing) q_t_start = steady_clock::now(); + blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, + m, n, 1.0, R_sk, ldr, A, lda); + if(this -> timing) q_t_stop = steady_clock::now(); } - if(this -> timing) - finalize_t_start = steady_clock::now(); - + if(this -> timing) finalize_t_start = steady_clock::now(); if (!this->orthogonalization) { - // Get the final R-factor - undoing the preconditioning - // R := R_chol * R_sk (returned by QR on sketch) - blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, 1.0, A_hat, d, R_sk, ldr); + blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, + n, n, 1.0, A_hat, d, R_sk, ldr); } + if(this -> timing) finalize_t_stop = steady_clock::now(); if(this -> timing) { - finalize_t_stop = steady_clock::now(); - total_t_stop = steady_clock::now(); - saso_t_dur = duration_cast(saso_t_stop - saso_t_start).count(); qr_t_dur = duration_cast(qr_t_stop - qr_t_start).count(); precond_t_dur = duration_cast(precond_t_stop - precond_t_start).count(); gram_t_dur = duration_cast(gram_t_stop - gram_t_start).count(); potrf_t_dur = duration_cast(potrf_t_stop - potrf_t_start).count(); finalize_t_dur = duration_cast(finalize_t_stop - finalize_t_start).count(); - - total_t_dur = duration_cast(total_t_stop - total_t_start).count(); - - // Subtract Q-factor computation time (excluded from total, matching CQRRT_linops test_mode) + total_t_dur = duration_cast(total_t_stop - total_t_start).count(); if (this->compute_Q) { q_t_dur = duration_cast(q_t_stop - q_t_start).count(); total_t_dur -= q_t_dur; } - long t_rest = total_t_dur - (saso_t_dur + qr_t_dur + precond_t_dur + gram_t_dur + potrf_t_dur + finalize_t_dur); - - // Fill the data vector (10 entries, matching CQRRT_linops indices) - // Index: 0=saso, 1=qr, 2=trtri(=0), 3=precond, 4=gram, 5=trmm_gram(=0), 6=potrf, 7=finalize, 8=rest, 9=total this -> times = {saso_t_dur, qr_t_dur, 0L, precond_t_dur, gram_t_dur, 0L, potrf_t_dur, finalize_t_dur, t_rest, total_t_dur}; @@ -282,7 +204,185 @@ int CQRRT::call( delete[] A_hat; delete[] tau; - return 0; } + + +// ============================================================================ +// CQRRT_linops — sketch-preconditioned Q-less Cholesky QR for abstract operators +// ============================================================================ +// +// Algorithm 4 from the collaborator's spec. Cannot modify the operator in place, +// so it forms R_sk explicitly and delegates to pcholqr_primitive (which handles +// the precondition-inversion strategy via PCholQRPrecondMethod). +// +template +class CQRRT_linops { + public: + + bool timing; + bool test_mode; + T eps; + + // Q-factor for test mode (only allocated if test_mode = true) + T* Q; + int64_t Q_rows; + int64_t Q_cols; + + // 11 entries (preserved for matlab plotter compatibility): + // [0] alloc, [1] sketch, [2] qr, [3] tri_inv, [4] fwd, [5] adj, [6] trsm_gram, + // [7] chol, [8] finalize, [9] rest, [10] total + std::vector times; + + int64_t nnz; + bool use_dense_sketch; + CQRRTLinopPrecond precond_method; + T bqrrp_block_ratio; + int64_t block_size; + + CQRRT_linops( + bool time_subroutines, + T ep, + bool enable_test_mode = false + ) { + timing = time_subroutines; + eps = ep; + nnz = 2; + use_dense_sketch = false; + block_size = 0; + precond_method = PCholQRPrecondMethod::TRSM_IDENTITY; + bqrrp_block_ratio = (T)1.0; + test_mode = enable_test_mode; + Q = nullptr; + Q_rows = 0; + Q_cols = 0; + } + + ~CQRRT_linops() { + if (Q != nullptr) { + delete[] Q; + } + } + + template + int call( + GLO& A, + T* R, + int64_t ldr, + T d_factor, + RandBLAS::RNGState &state + ) { + steady_clock::time_point t0, t1, total_t_start, total_t_stop; + long alloc_dur = 0, saso_dur = 0, qr_dur = 0; + long precond_inv_dur = 0, fwd_dur = 0, adj_dur = 0, gemm_dur = 0; + long chol_dur = 0, finalize_dur = 0, q_dur = 0, total_dur = 0; + + if (this->timing) total_t_start = steady_clock::now(); + + int64_t m = A.n_rows; + int64_t n = A.n_cols; + int64_t d = (int64_t)(d_factor * (T)n); + int64_t b_eff = (this->block_size > 0 && this->block_size < n) + ? this->block_size : n; + + // ---- Allocations ---- + if (this->timing) t0 = steady_clock::now(); + T* A_hat = new T[d * n]; + T* tau = new T[n]; + T* P = new T[n * n](); + T* R_pre = new T[n * n](); + T* G = new T[n * n](); + T* A_temp = new T[m * b_eff]; + T* Z_buf = new T[n * b_eff]; + if (this->timing) { t1 = steady_clock::now(); alloc_dur = duration_cast(t1 - t0).count(); } + + // ---- Step 1: Sketch M^sk = S * A ---- + if (this->timing) t0 = steady_clock::now(); + if (this->use_dense_sketch) { + RandBLAS::DenseDist DD(d, m); + RandBLAS::DenseSkOp S(DD, state); + state = S.next_state; + RandBLAS::fill_dense(S); + A(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S, (T)0.0, A_hat, d); + } else { + RandBLAS::SparseDist DS(d, m, this->nnz); + RandBLAS::SparseSkOp S(DS, state); + state = S.next_state; + RandBLAS::fill_sparse(S); + A(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + d, n, m, (T)1.0, S, (T)0.0, A_hat, d); + } + if (this->timing) { t1 = steady_clock::now(); saso_dur = duration_cast(t1 - t0).count(); } + + // ---- Step 2: [~, R^sk] = qr(M^sk) ---- + if (this->timing) t0 = steady_clock::now(); + lapack::geqrf(d, n, A_hat, d, tau); + for (int64_t j = 0; j < n; ++j) + for (int64_t i = 0; i <= j; ++i) + P[i + j * n] = A_hat[i + j * d]; + if (this->timing) { t1 = steady_clock::now(); qr_dur = duration_cast(t1 - t0).count(); } + + // ---- Step 3: PCholQR(A, P = R^sk) ---- + int info = pcholqr_primitive( + A, P, R, ldr, + this->precond_method, + this->block_size, + this->bqrrp_block_ratio, + R_pre, G, A_temp, Z_buf, + &state, + precond_inv_dur, fwd_dur, adj_dur, gemm_dur, chol_dur, finalize_dur, + this->timing); + if (info != 0) { + delete[] A_hat; delete[] tau; delete[] P; delete[] R_pre; + delete[] G; delete[] A_temp; delete[] Z_buf; + return 1; + } + + // ---- Test mode: materialize Q = A * R^{-1} ---- + if (this->test_mode) { + if (this->timing) t0 = steady_clock::now(); + + RandLAPACK::util::eye(n, n, R_pre); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, T(1), R, ldr, R_pre, n); + + T* Q_buf = new T[m * n]; + for (int64_t j = 0; j < n; j += b_eff) { + int64_t b_j = std::min(b_eff, n - j); + A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, b_j, n, (T)1.0, R_pre + j * n, n, (T)0.0, Q_buf + j * m, m); + } + this->Q_rows = m; + this->Q_cols = n; + this->Q = Q_buf; + + if (this->timing) { t1 = steady_clock::now(); q_dur = duration_cast(t1 - t0).count(); } + } + + // ---- Finalize timing ---- + if (this->timing) { + total_t_stop = steady_clock::now(); + total_dur = duration_cast(total_t_stop - total_t_start).count(); + total_dur -= q_dur; + + long rest_dur = total_dur - (alloc_dur + saso_dur + qr_dur + precond_inv_dur + + fwd_dur + adj_dur + gemm_dur + chol_dur + finalize_dur); + + this->times = {alloc_dur, saso_dur, qr_dur, precond_inv_dur, + fwd_dur, adj_dur, gemm_dur, + chol_dur, finalize_dur, rest_dur, total_dur}; + } + + delete[] A_hat; + delete[] tau; + delete[] P; + delete[] R_pre; + delete[] G; + delete[] A_temp; + delete[] Z_buf; + return 0; + } +}; + } // end namespace RandLAPACK diff --git a/RandLAPACK/drivers/rl_cqrrt_linops.hh b/RandLAPACK/drivers/rl_cqrrt_linops.hh deleted file mode 100644 index d84a63b8c..000000000 --- a/RandLAPACK/drivers/rl_cqrrt_linops.hh +++ /dev/null @@ -1,556 +0,0 @@ -#pragma once - -#include "rl_util.hh" -#include "rl_blaspp.hh" -#include "rl_lapackpp.hh" -#include "rl_linops.hh" -#include "rl_bqrrp.hh" - -#include -#include -#include -#include - -using namespace std::chrono; - -namespace RandLAPACK { - -/// Method used to form R_sk^{-1} in CQRRT_linops. -/// -/// TRSM_IDENTITY — original method: TRSM(I, R_sk) → R_sk^{-1}. -/// Fast (O(n³/2)) but unstable when κ(R_sk) is large. -/// GEQP3 — stabilized method: GEQP3(R_sk) = Q*R_buf*P^T, -/// then R_sk^{-1} = P * R_buf^{-1} * Q^T via ungqr + TRSM. -/// More expensive (O(11n³/6)) but accurate for ill-conditioned R_sk. -/// BQRRP — stabilized method using blocked randomized QRCP instead of GEQP3. -/// Same output format as GEQP3; may be faster for large n due to -/// cache-friendly blocked structure and sketched pivot selection. -/// Block size is chosen adaptively via bqrrp_block_ratio. -enum class CQRRTLinopPrecond { - TRSM_IDENTITY, - GEQP3, - BQRRP -}; - -/// Sketch-preconditioned Cholesky QR for abstract linear operators. -/// -/// Linop analogue of CQRRT (rl_cqrrt.hh). Computes A = QR where A is any -/// type satisfying the LinearOperator concept. The algorithm sketches A to -/// obtain a preconditioner R_sk, then computes the Gram matrix of the -/// preconditioned operator A * R_sk^{-1} via linop calls, and factors it -/// with Cholesky. -/// -/// Unlike rl_cqrrt.hh (which overwrites A in place with TRSM), this class -/// cannot modify the operator directly. Instead, it computes R_sk^{-1} -/// explicitly and multiplies through the operator interface. -/// -/// The Q factor is not computed by default (Q-less factorization). When -/// test_mode is enabled, Q is materialized for verification. -/// -template -class CQRRT_linops { - public: - - bool timing; - bool test_mode; - T eps; - - // Q-factor for test mode (only allocated if test_mode = true) - T* Q; - int64_t Q_rows; - int64_t Q_cols; - - // 11 entries: alloc, sketch, qr, tri_inv, fwd, adj, trsm_gram, chol, finalize, rest, total - // fwd = LinOp NoTrans: A * R_sk_inv (accumulated over blocks) - // adj = LinOp Trans: A^T * buf (accumulated over blocks) - // trsm_gram = (R^sk)^{-T} * G via TRSM on original R_sk (backward-stable left factor) - std::vector times; - - // tuning SASOS - int64_t nnz; - - // If true, use a dense Gaussian sketching operator instead of sparse SASO. - // Dense sketches avoid potential issues with rank-deficient sketches but - // require O(d*m) storage and O(d*m*n) work for application. - bool use_dense_sketch; - - // Method for computing R_sk^{-1}. See CQRRTLinopPrecond enum. - // Default: TRSM_IDENTITY (original behavior). - // Set to GEQP3 or BQRRP for the stabilized variants. - CQRRTLinopPrecond precond_method; - - // Block size ratio for BQRRP preconditioner (precond_method == BQRRP only). - // The BQRRP block size is set to max(1, n * bqrrp_block_ratio). - // When precond_method == BQRRP and bqrrp_block_ratio == 1.0 (default), - // the ratio is overridden adaptively inside call() using the same - // heuristic as CQRRPT: 1.0 for n ≤ 2000, 0.5 for n ≤ 8000, 1/32 otherwise. - T bqrrp_block_ratio; - - // Column-block size for the precondition + Gram computation. - // - // When block_size > 0, the two expensive linear operator calls: - // (1) A_pre = A * R_sk_inv (m × n) - // (2) R = A^T * A_pre (n × n) - // are fused into a column-block loop that processes b columns at a time: - // for each column block j of width b: - // buf (m × b) = A * R_sk_inv[:, j*b : (j+1)*b] - // R[:, j*b : (j+1)*b] = A^T * buf - // - // This reduces peak memory from O(m*n) to O(m*b), which is significant - // when m is large and n is moderate. The result is mathematically - // identical — each column block of R is: - // R[:, j_block] = A^T * (A * R_sk_inv[:, j_block]) - // which equals the corresponding columns of A^T * A * R_sk_inv. - // - // When block_size <= 0 or block_size >= n, the full m × n buffer is - // allocated and the original (non-blocked) path is used. - // - // When test_mode is enabled and blocking is active, the Q-factor - // computation (which needs the full m × n A_pre) is handled by - // recomputing A_pre = A * R_sk_inv after the Gram loop. This - // recomputation is outside the timing region, so it does not - // affect benchmark results. - int64_t block_size; - - CQRRT_linops( - bool time_subroutines, - T ep, - bool enable_test_mode = false - ) { - timing = time_subroutines; - eps = ep; - nnz = 2; - use_dense_sketch = false; - block_size = 0; - precond_method = CQRRTLinopPrecond::TRSM_IDENTITY; - bqrrp_block_ratio = (T)1.0; - test_mode = enable_test_mode; - Q = nullptr; - Q_rows = 0; - Q_cols = 0; - } - - ~CQRRT_linops() { - if (Q != nullptr) { - delete[] Q; - } - } - - /// Computes the R-factor of the unpivoted QR factorization A = QR, - /// where Q is m-by-n and R is n-by-n upper triangular. - /// - /// Operates similarly to rl_cqrrt.hh, but accepts any type satisfying - /// the LinearOperator concept and returns a Q-less factorization - /// (Q is only computed when test_mode is enabled). - /// - /// Algorithm: - /// 1. Sketch: S*A (d x n), where d = d_factor * n - /// 2. QR of sketch: S*A = Q_sk * R_sk - /// 3. Precondition: A_pre = A * R_sk^{-1} - /// 4. Gram matrix: G = (R_sk^{-1})^T * A^T * A * R_sk^{-1} - /// 5. Cholesky: G = R_chol^T * R_chol - /// 6. Final R = R_chol * R_sk - /// - /// @note This algorithm expects A to be full-rank (rank = n). Rank-deficient inputs may result - /// in loss of orthogonality in the Q-factor (when test_mode=true) and numerical instability - /// in the R-factor. - /// - /// @param[in] A - /// The m-by-n linear operator (m and n read from A.n_rows, A.n_cols). - /// - /// @param[out] R - /// Pre-allocated n-by-n buffer. On exit, stores the upper-triangular - /// R factor. Zero entries are not compressed. - /// - /// @param[in] ldr - /// Leading dimension of R. - /// - /// @param[in] d_factor - /// Sketch embedding factor. The sketch dimension is d = d_factor * n. - /// Typically d_factor >= 1; larger values improve numerical stability. - /// - /// @param[in,out] state - /// RNG state for sketching operator generation. Advanced on exit. - /// - /// @return = 0: successful exit - template - int call( - GLO& A, - T* R, - int64_t ldr, - T d_factor, - RandBLAS::RNGState &state - ) { - ///--------------------TIMING VARS--------------------/ - steady_clock::time_point t_start, t_stop; - steady_clock::time_point alloc_t_start, alloc_t_stop; - steady_clock::time_point saso_t_start, saso_t_stop; - steady_clock::time_point qr_t_start, qr_t_stop; - steady_clock::time_point trtri_t_start, trtri_t_stop; - steady_clock::time_point trsm_gram_t_start, trsm_gram_t_stop; - steady_clock::time_point potrf_t_start, potrf_t_stop; - steady_clock::time_point finalize_t_start, finalize_t_stop; - steady_clock::time_point total_t_start, total_t_stop; - steady_clock::time_point q_t_start, q_t_stop; - long alloc_t_dur = 0; - long saso_t_dur = 0; - long qr_t_dur = 0; - long trtri_t_dur = 0; - long fwd_t_dur = 0; - long adj_t_dur = 0; - long trsm_gram_t_dur = 0; - long potrf_t_dur = 0; - long finalize_t_dur = 0; - long total_t_dur = 0; - long q_t_dur = 0; - - if(this -> timing) - total_t_start = steady_clock::now(); - - int64_t m = A.n_rows; - int64_t n = A.n_cols; - - int64_t d = d_factor * n; - - if(this -> timing) - alloc_t_start = steady_clock::now(); - - // No zero-init: first use is with beta=0.0 which overwrites all elements - T* A_hat = new T[d * n]; - // No zero-init: geqrf writes the output - T* tau = new T[n]; - - if(this -> timing) { - alloc_t_stop = steady_clock::now(); - saso_t_start = steady_clock::now(); - } - - /// Generate and apply the sketching operator to the linear operator. - // Side::Right means the operator A is on the right side: C = op(S) * op(A) - if (this->use_dense_sketch) { - // Dense Gaussian sketch: allocate d x m buffer, fill with Gaussian entries. - RandBLAS::DenseDist DD(d, m); - RandBLAS::DenseSkOp S(DD, state); - state = S.next_state; - RandBLAS::fill_dense(S); - A(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, d, n, m, (T)1.0, S, (T)0.0, A_hat, d); - } else { - // Sparse SASO sketch: uses nnz nonzeros per column. - RandBLAS::SparseDist DS(d, m, this->nnz); - RandBLAS::SparseSkOp S(DS, state); - state = S.next_state; - RandBLAS::fill_sparse(S); - A(Side::Right, Layout::ColMajor, Op::NoTrans, Op::NoTrans, d, n, m, (T)1.0, S, (T)0.0, A_hat, d); - } - - if(this -> timing) { - saso_t_stop = steady_clock::now(); - qr_t_start = steady_clock::now(); - } - - /// Performing QR on a sketch - lapack::geqrf(d, n, A_hat, d, tau); - - if(this -> timing) - qr_t_stop = steady_clock::now(); - - // Compute R_sk^{-1}. Method selected by this->precond_method. - if(this -> timing) - trtri_t_start = steady_clock::now(); - - T* R_sk_inv = nullptr; - - if (this->precond_method == CQRRTLinopPrecond::TRSM_IDENTITY) { - // Solve R_sk * R_inv = I via Side::Left TRSM (column-by-column - // backward stable). Forming R_sk^{-1} by Side::Right TRSM - // (X * R_sk = I) is mathematically equivalent but exhibits - // significantly worse backward error in the subsequent - // product A * R_sk^{-1} when R_sk is ill-conditioned. - T* Eye = new T[n * n](); - RandLAPACK::util::eye(n, n, Eye); - if (!RandLAPACK::util::diag_is_nonzero(n, A_hat, d)) { - delete[] A_hat; - delete[] tau; - delete[] Eye; - return 1; - } - blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, A_hat, d, Eye, n); - if (n > 1) { - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &Eye[1], n); - } - R_sk_inv = Eye; - } else { - // Stabilized QRCP-based inversion (GEQP3 or BQRRP). - // Both methods produce the same output format: - // R_sk_copy: Householder reflectors for Q_buf in lower triangle, - // R_buf in upper triangle (1-based jpiv, same as LAPACK GEQP3) - // The ungqr + TRSM + scatter code below is identical for both. - // - // Result: R_sk^{-1} = P * R_buf^{-1} * Q_buf^T (dense, n×n) - - // Extract R_sk from A_hat upper triangle into n×n buffer - T* R_sk_copy = new T[n * n](); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_sk_copy[i + j*n] = A_hat[i + j*d]; - - if (!RandLAPACK::util::diag_is_nonzero(n, R_sk_copy, n)) { - delete[] A_hat; - delete[] tau; - delete[] R_sk_copy; - return 1; - } - - int64_t* jpiv = new int64_t[n](); - T* tau_qr = new T[n]; - - if (this->precond_method == CQRRTLinopPrecond::GEQP3) { - lapack::geqp3(n, n, R_sk_copy, n, jpiv, tau_qr); - } else { - // BQRRP: blocked randomized QRCP on R_sk (n×n). - // Adaptive block size matches CQRRPT's heuristic. - if (n <= 2000) this->bqrrp_block_ratio = (T)1.0; - else if (n <= 8000) this->bqrrp_block_ratio = (T)0.5; - else this->bqrrp_block_ratio = (T)1.0 / (T)32; - RandLAPACK::BQRRP bqrrp(false, (int64_t)(n * this->bqrrp_block_ratio)); - bqrrp.call(n, n, R_sk_copy, n, (T)1.0, tau_qr, jpiv, state); - } - - // Extract upper triangular R_buf before overwriting R_sk_copy with Q_buf - T* R_buf = new T[n * n](); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_buf[i + j*n] = R_sk_copy[i + j*n]; - - // Expand Q_buf from Householder reflectors (overwrites R_sk_copy) - lapack::ungqr(n, n, n, R_sk_copy, n, tau_qr); - - // W = Q_buf^T (explicit transpose), then solve R_buf * W = Q_buf^T in-place - T* W = new T[n * n]; - for (int64_t i = 0; i < n; ++i) - for (int64_t j = 0; j < n; ++j) - W[i + j*n] = R_sk_copy[j + i*n]; - blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, R_buf, n, W, n); - - // R_sk_inv = P * W: row (jpiv[k]-1) of R_sk_inv gets row k of W - R_sk_inv = new T[n * n](); - for (int64_t k = 0; k < n; ++k) - for (int64_t j = 0; j < n; ++j) - R_sk_inv[(jpiv[k]-1) + j*n] = W[k + j*n]; - - delete[] R_sk_copy; - delete[] R_buf; - delete[] W; - delete[] jpiv; - delete[] tau_qr; - } - - if(this -> timing) { - trtri_t_stop = steady_clock::now(); - } - - // Gram computation: R = (R_sk_inv)^T * A^T * A * R_sk_inv - // The (R_sk_inv)^T left-multiply is handled later via TRMM. - // Here we compute: R = A^T * (A * R_sk_inv). - - // Determine effective block width. - // block_size <= 0 or >= n means "no blocking" (full width). - int64_t b_eff = (this->block_size > 0 && this->block_size < n) - ? this->block_size : n; - - // A_pre buffer: - // Full path: m × n (kept alive for Q-factor in test_mode) - // Block path: m × b_eff (temporary, freed after Gram loop; - // if test_mode, a full m × n buffer is - // allocated later for Q computation) - // No zero-init: first use is with beta=0.0 which overwrites all elements - T* A_pre = new T[m * b_eff]; - - if (b_eff == n) { - // --- Full materialization path (original) --- - - // Step 1: A_pre (m × n) = A * R_sk_inv (m × n × n) - // A is m × n, R_sk_inv is n × n, A_pre is m × n. - // Side::Left: C = alpha * op(A) * op(B) + beta * C - // where op(A) = A (m × n), op(B) = R_sk_inv (n × n), C = A_pre (m × n). - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, R_sk_inv, n, (T)0.0, A_pre, m); - if(this->timing) { t_stop = steady_clock::now(); fwd_t_dur = duration_cast(t_stop - t_start).count(); } - - // Step 2: R (n × n) = A^T * A_pre (n × m × m × n = n × n) - // Since SYRK is not defined for non-dense operators, we use - // an explicit A^T * A_pre to form the Gram matrix. - // op(A) = A^T (n × m), op(B) = A_pre (m × n), C = R (n × n). - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, n, n, m, (T)1.0, A_pre, m, (T)0.0, R, ldr); - if(this->timing) { t_stop = steady_clock::now(); adj_t_dur = duration_cast(t_stop - t_start).count(); } - } else { - // --- Column-block processing path (memory-efficient) --- - // - // Process n columns in blocks of width b_eff. - // The last block may be narrower if n is not divisible by b_eff. - // - // For each block starting at column j with width b_j: - // (1) buf (m × b_j) = A * R_sk_inv[:, j : j+b_j] - // R_sk_inv is n × n in ColMajor with ld = n. - // Column j starts at R_sk_inv + j * n. - // We multiply A (m × n) by this n × b_j slice. - // - // (2) R[:, j : j+b_j] (n × b_j) = A^T * buf - // A^T is n × m, buf is m × b_j, result is n × b_j. - // R is n × n with ld = ldr. - // Column j starts at R + j * ldr. - // - // Total FLOPs are the same as the full path; only memory differs. - - long fwd_accum = 0, adj_accum = 0; - for (int64_t j = 0; j < n; j += b_eff) { - int64_t b_j = std::min(b_eff, n - j); - - // (1) buf = A * R_sk_inv[:, j : j+b_j] (NoTrans = fwd) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, R_sk_inv + j * n, n, (T)0.0, A_pre, m); - if(this->timing) { t_stop = steady_clock::now(); fwd_accum += duration_cast(t_stop - t_start).count(); } - - // (2) R[:, j : j+b_j] = A^T * buf (Trans = adj) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, m, (T)1.0, A_pre, m, (T)0.0, R + j * ldr, ldr); - if(this->timing) { t_stop = steady_clock::now(); adj_accum += duration_cast(t_stop - t_start).count(); } - } - - if(this -> timing) { - fwd_t_dur = fwd_accum; - adj_t_dur = adj_accum; - } - } - - if(this -> timing) { - trsm_gram_t_start = steady_clock::now(); - } - - // Complete Gram: R := (R^sk)^{-T} * R (backward-stable TRSM on original sketch R) - // Applying the left factor via TRSM on A_hat (which holds R^sk in its upper n×n triangle) - // is more stable than TRMM/GEMM with the explicit R_sk_inv^T, regardless of which - // precond_method was used to form R_sk_inv. A_hat is still alive here (deleted below). - blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::Trans, Diag::NonUnit, - n, n, (T)1.0, A_hat, d, R, ldr); - - if(this -> timing) { - trsm_gram_t_stop = steady_clock::now(); - potrf_t_start = steady_clock::now(); - } - - // Cholesky factorization (only reads/writes upper triangle) - if (lapack::potrf(Uplo::Upper, n, R, ldr)) { - delete[] A_hat; - delete[] tau; - delete[] R_sk_inv; - delete[] A_pre; - return 1; - } - - if(this -> timing) - potrf_t_stop = steady_clock::now(); - - // Compute Q-factor if test mode is enabled (NOT included in cholqr timing) - if(this->test_mode) { - if(this->timing) - q_t_start = steady_clock::now(); - - if (b_eff < n) { - // Column-block Gram was used: A_pre is only m × b_eff, - // too small for Q. Recompute A_pre = A * R_sk_inv in - // full. This is outside the timing region, so the extra - // operator application does not affect benchmark results. - delete[] A_pre; - // No zero-init: beta=0.0 overwrites all elements - A_pre = new T[m * n]; - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, R_sk_inv, n, (T)0.0, A_pre, m); - } - - // Reuse A_pre storage for Q (Q = A_pre * R_chol^{-1}) - this->Q_rows = m; - this->Q_cols = n; - this->Q = A_pre; // Take ownership of A_pre buffer - - // Solve Q * R_chol = A_pre for Q (R_chol is upper triangular from Cholesky) - // Q = A * (R_chol * R_sk)^{-1} = A * R_sk^{-1} * R_chol^{-1} = A_pre * R_chol^{-1} - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, m, n, (T)1.0, R, ldr, this->Q, m); - - if(this->timing) - q_t_stop = steady_clock::now(); - } - - // Zero out strictly lower triangle of R before final trmm - // trmm expects R to be upper triangular on input (it preserves triangular structure) - // The lower triangle may contain garbage from the Gram matrix computation - // Use laset with beta=1.0 to preserve diagonal while zeroing strictly lower triangle - if (n > 1) { - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &R[1], ldr); - } - - if(this -> timing) - finalize_t_start = steady_clock::now(); - - // Get the final R-factor - undoing the preconditioning - // R := R_chol * R_sk, where R_sk is the upper triangle of A_hat - // trmm with Uplo::Upper only reads upper triangle of A_hat (can use ld=d directly) - // and expects R to be upper triangular on input - blas::trmm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, 1.0, A_hat, d, R, ldr); - - if(this -> timing) - finalize_t_stop = steady_clock::now(); - - if(this -> timing) { - // Stop timing BEFORE cleanup operations to exclude deallocation costs. - // Note: First iteration may show inflated times due to cold cache effects. - total_t_stop = steady_clock::now(); - - alloc_t_dur = duration_cast(alloc_t_stop - alloc_t_start).count(); - saso_t_dur = duration_cast(saso_t_stop - saso_t_start).count(); - qr_t_dur = duration_cast(qr_t_stop - qr_t_start).count(); - trtri_t_dur = duration_cast(trtri_t_stop - trtri_t_start).count(); - // fwd_t_dur and adj_t_dur already set (in both full and blocked paths) - trsm_gram_t_dur = duration_cast(trsm_gram_t_stop - trsm_gram_t_start).count(); - potrf_t_dur = duration_cast(potrf_t_stop - potrf_t_start).count(); - finalize_t_dur = duration_cast(finalize_t_stop - finalize_t_start).count(); - - total_t_dur = duration_cast(total_t_stop - total_t_start).count(); - - // Subtract Q-factor computation time if in test mode - if(this->test_mode) { - q_t_dur = duration_cast(q_t_stop - q_t_start).count(); - total_t_dur -= q_t_dur; - } - - long t_rest = total_t_dur - (alloc_t_dur + saso_t_dur + qr_t_dur + trtri_t_dur + fwd_t_dur + - adj_t_dur + trsm_gram_t_dur + potrf_t_dur + finalize_t_dur); - - // Fill the data vector (11 entries) - // Index: 0=alloc, 1=sketch, 2=qr, 3=tri_inv, 4=fwd, 5=adj, 6=trmm, 7=chol, 8=finalize, 9=rest, 10=total - this -> times = {alloc_t_dur, saso_t_dur, qr_t_dur, trtri_t_dur, fwd_t_dur, - adj_t_dur, trsm_gram_t_dur, potrf_t_dur, finalize_t_dur, - t_rest, total_t_dur}; - } - - // Cleanup - now outside the timing region to avoid timing artifacts - delete[] A_hat; - delete[] tau; - delete[] R_sk_inv; - - // Only delete A_pre if not in test mode (otherwise Q owns it). - // When test_mode + blocking: the small block buffer was freed - // and replaced with a full m × n buffer in the Q section above. - if(!this->test_mode) { - delete[] A_pre; - } - - return 0; - } -}; -} // end namespace RandLAPACK diff --git a/RandLAPACK/drivers/rl_scholqr3_linops.hh b/RandLAPACK/drivers/rl_scholqr3_linops.hh index 131187bcd..296374fa6 100644 --- a/RandLAPACK/drivers/rl_scholqr3_linops.hh +++ b/RandLAPACK/drivers/rl_scholqr3_linops.hh @@ -4,6 +4,7 @@ #include "rl_blaspp.hh" #include "rl_lapackpp.hh" #include "rl_linops.hh" +#include "../comps/rl_cholqr.hh" #include #include @@ -16,33 +17,16 @@ using namespace std::chrono; namespace RandLAPACK { -/// Shifted Cholesky QR3 for abstract linear operators. +/// Shifted Cholesky QR3 for abstract linear operators (fully-blocked variant). /// -/// Linop analogue of shifted CholQR3: computes A = QR where A is any type -/// satisfying the LinearOperator concept. All Gram matrices are formed through -/// A(NoTrans, ...) and A(Trans, ...) calls, so A can be dense, sparse, -/// composite, or any other operator type without modification. +/// Algorithm 3 from the collaborator's spec: +/// iter 1: cholqr_primitive(A) with shift s = 11 * eps * n * ||A||_F^2 -> R_1 +/// iter i = 2, 3: pcholqr_primitive(A, R_{i-1}, TRSM_IDENTITY) -> R_i +/// return R_3 /// -/// Fully-blocked implementation: never materializes the full m x n operator product -/// during the QR iterations. All three iterations compute their Gram matrices through -/// blocked linop calls, using an accumulated right-factor M = R1^{-1} R2^{-1} ... -/// to avoid storing Q explicitly. -/// -/// Algorithm: -/// 1. Shifted CholQR1: G1 = A^T A + s*I, R1 = chol(G1), M <- R1^{-1} -/// 2. CholQR2: G2 = M^T A^T A M, R2 = chol(G2), R = R2*R1, M <- M*R2^{-1} -/// 3. CholQR3: G3 = M^T A^T A M, R3 = chol(G3), R = R3*R -/// -/// Blocked Gram computation for iteration k (M_k = accumulated R-inverse): -/// for each column block j of width b: -/// W = A * M_k[:, j:j+b] (linop NoTrans, m x b) -/// Z = A^T * W (linop Trans, n x b) -/// G[:, j:j+b] = M_k^T * Z (gemm, n x b) -/// -/// Peak memory: O(m*b + n^2) -- no m x n buffer needed during QR iterations. -/// If test_mode is enabled, Q = A * R^{-1} is materialized at the end (m x n). -/// -/// The shift is computed as: shift = 11 * eps * n * ||A||_F^2 +/// Peak memory O(n^2 + (m+n)*b_eff) — never materializes the m × n operator product +/// during the QR iterations. If test_mode is enabled, Q = A * R^{-1} is materialized +/// at the end (m × n, outside the timing region). /// /// Reference: Shifted Cholesky QR from Fukaya et al. (SISC, 2020). /// @@ -59,40 +43,14 @@ class sCholQR3_linops { int64_t Q_rows; int64_t Q_cols; - // Individual Cholesky factors from each iteration (n x n upper triangular). - std::vector G1_factor; - std::vector G2_factor; - std::vector G3_factor; - - // Timing breakdown (18 entries): - // [0] alloc - buffer allocation - // [1] fwd1 - Iter 1 NoTrans: A * M[:, block] - // [2] adj1 - Iter 1 Trans: A^T * W (direct to G since M=I) - // [3] chol1 - Iter 1 Cholesky (potrf) - // [4] upd1 - M = R1^{-1} (n x n trsm) - // [5] fwd2 - Iter 2 NoTrans: A * M[:, block] - // [6] adj2 - Iter 2 Trans: A^T * W - // [7] gemm2 - Iter 2 M^T * Z - // [8] chol2 - Iter 2 Cholesky - // [9] upd2 - R = R2*R1, M *= R2^{-1} - // [10] fwd3 - Iter 3 NoTrans - // [11] adj3 - Iter 3 Trans - // [12] gemm3 - Iter 3 M^T * Z - // [13] chol3 - Iter 3 Cholesky - // [14] upd3 - R = R3*R - // [15] q_mat - Q materialization for test mode (0 if not test_mode) - // [16] rest - unaccounted time - // [17] total - wall-clock total + // Timing breakdown (18 entries; layout preserved for matlab plotters): + // [0] alloc + // [1] fwd1 [2] adj1 [3] chol1 [4] upd1 + // [5] fwd2 [6] adj2 [7] gemm2 [8] chol2 [9] upd2 + // [10] fwd3 [11] adj3 [12] gemm3 [13] chol3 [14] upd3 + // [15] q_mat [16] rest [17] total std::vector times; - // Column-block size for blocked Gram computations. - // - // Controls the width of column blocks used in all three iterations' - // Gram matrix computations. Smaller values reduce peak memory - // (O(m*block_size + n^2) instead of O(m*n)), at the cost of - // more linop calls (2 * ceil(n/block_size) per iteration). - // - // When block_size <= 0 or >= n, uses b_eff = n (single block per loop). int64_t block_size; sCholQR3_linops( @@ -115,310 +73,138 @@ class sCholQR3_linops { } } - /// Computes the QR factorization A = QR using shifted Cholesky QR3. - /// - /// @param[in] A - /// The m-by-n linear operator (m and n read from A.n_rows, A.n_cols). - /// - /// @param[out] R - /// Pre-allocated n-by-n buffer. On exit, stores the upper-triangular - /// R factor. Zero entries are not compressed. - /// - /// @param[in] ldr - /// Leading dimension of R. - /// - /// @return = 0: successful exit template int call( GLO& A, T* R, int64_t ldr ) { - ///--------------------TIMING VARS--------------------/ - steady_clock::time_point t_start, t_stop; - steady_clock::time_point total_t_start, total_t_stop; + steady_clock::time_point t0, t1, total_t_start, total_t_stop; long alloc_dur = 0; long fwd1_dur = 0, adj1_dur = 0, chol1_dur = 0, upd1_dur = 0; long fwd2_dur = 0, adj2_dur = 0, gemm2_dur = 0, chol2_dur = 0, upd2_dur = 0; long fwd3_dur = 0, adj3_dur = 0, gemm3_dur = 0, chol3_dur = 0, upd3_dur = 0; long q_mat_dur = 0, total_dur = 0; - if(this->timing) - total_t_start = steady_clock::now(); + if (this->timing) total_t_start = steady_clock::now(); int64_t m = A.n_rows; int64_t n = A.n_cols; - - // Determine effective block width. int64_t b_eff = (this->block_size > 0 && this->block_size < n) ? this->block_size : n; - if(this->timing) - t_start = steady_clock::now(); - - // ---- Allocate buffers ---- - // G: n x n Gram matrix / Cholesky workspace (zero-init for lower triangle) - T* G = new T[n * n](); - - // R_temp: n x n workspace for R accumulation via trmm - T* R_temp = new T[n * n](); - - // M: n x n accumulated R-inverse product (starts as identity) - T* M = new T[n * n](); - RandLAPACK::util::eye(n, n, M); - - // A_temp: m x b_eff buffer for linop NoTrans output - T* A_temp = new T[m * b_eff]; - - // Z_buf: n x b_eff buffer for linop Trans output (used in iterations 2-3) - T* Z_buf = new T[n * b_eff]; - - if(this->timing) { - t_stop = steady_clock::now(); - alloc_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Iteration 1: Shifted Cholesky QR - //================================================================ - // Blocked Gram: G = A^T A (since M = I, no M^T multiply needed) - long fwd1_accum = 0, adj1_accum = 0; - for (int64_t j = 0; j < n; j += b_eff) { - int64_t b_j = std::min(b_eff, n - j); - - // W = A * M[:, j:j+b] (= A * I[:, j:j+b] since M = I) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, M + j * n, n, (T)0.0, A_temp, m); - if(this->timing) { t_stop = steady_clock::now(); fwd1_accum += duration_cast(t_stop - t_start).count(); } - - // G[:, j:j+b] = A^T * W (direct to G since M = I) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, m, (T)1.0, A_temp, m, (T)0.0, G + j * n, n); - if(this->timing) { t_stop = steady_clock::now(); adj1_accum += duration_cast(t_stop - t_start).count(); } - } - - // Compute shift from ||A||_F^2 = trace(G) - T norm_A_sq = 0; - for (int64_t i = 0; i < n; ++i) - norm_A_sq += G[i * (n + 1)]; - T shift = 11 * std::numeric_limits::epsilon() * n * norm_A_sq; - - // Add shift to diagonal: G = G + shift * I - for (int64_t i = 0; i < n; ++i) - G[i * (n + 1)] += shift; - - if(this->timing) { - fwd1_dur = fwd1_accum; - adj1_dur = adj1_accum; - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R1^T * R1 - if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); - if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] G; delete[] R_temp; delete[] M; + // ---- Workspaces shared across all 3 iterations ---- + if (this->timing) t0 = steady_clock::now(); + T* G = new T[n * n](); // Gram / Cholesky workspace + T* R_pre = new T[n * n](); // preconditioner inverse (used in iters 2, 3) + T* P_prev = new T[n * n](); // previous R_{i-1}, fed as P to pcholqr_primitive + T* A_temp = new T[m * b_eff]; // m × b_eff scratch for linop NoTrans + T* Z_buf = new T[n * b_eff]; // n × b_eff scratch for linop Trans + if (this->timing) { t1 = steady_clock::now(); alloc_dur = duration_cast(t1 - t0).count(); } + + // ============================================================ + // Iter 1: shifted CholQR -> R_1, written into R + // shift_factor = 11 * eps * n so the shift becomes 11*eps*n*||A||_F^2 + // (per the collaborator's Alg 3 step 3). + // ============================================================ + T shift_factor_iter1 = (T)11.0 * std::numeric_limits::epsilon() * (T)n; + int info = cholqr_primitive( + A, R, ldr, + shift_factor_iter1, + this->block_size, + G, A_temp, + fwd1_dur, adj1_dur, chol1_dur, this->timing); + if (info != 0) { + delete[] G; delete[] R_pre; delete[] P_prev; delete[] A_temp; delete[] Z_buf; return 1; } + // upd1 placeholder kept for matlab CSV compatibility (no inverse formed in iter 1). + upd1_dur = 0; - // Save G1 factor - this->G1_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G1_factor.data(), n); - - // Initialize R = R1 - lapack::lacpy(MatrixType::Upper, n, n, G, n, R, ldr); - - if(this->timing) { - t_stop = steady_clock::now(); - chol1_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // Initialize M = R1^{-1} by solving R1 * M = I via Side::Left TRSM - // (column-by-column backward stable). Using Side::Right (X * R1 = I) - // is mathematically equivalent but produces an M whose subsequent - // product A * M has significantly worse backward error when R1 is - // ill-conditioned -- see rl_cqrrt_linops.hh for the same pattern. - blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, M, n); - - if(this->timing) { - t_stop = steady_clock::now(); - upd1_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Iteration 2: Cholesky QR - //================================================================ - // Blocked Gram: G2 = M^T * A^T * A * M where M = R1^{-1} - long fwd2_accum = 0, adj2_accum = 0, gemm2_accum = 0; - for (int64_t j = 0; j < n; j += b_eff) { - int64_t b_j = std::min(b_eff, n - j); - - // W = A * M[:, j:j+b] - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, M + j * n, n, (T)0.0, A_temp, m); - if(this->timing) { t_stop = steady_clock::now(); fwd2_accum += duration_cast(t_stop - t_start).count(); } - - // Z = A^T * W - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, m, (T)1.0, A_temp, m, (T)0.0, Z_buf, n); - if(this->timing) { t_stop = steady_clock::now(); adj2_accum += duration_cast(t_stop - t_start).count(); } - - // G[:, j:j+b] = M^T * Z - if(this->timing) t_start = steady_clock::now(); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, n, (T)1.0, M, n, Z_buf, n, (T)0.0, G + j * n, n); - if(this->timing) { t_stop = steady_clock::now(); gemm2_accum += duration_cast(t_stop - t_start).count(); } - } - - if(this->timing) { - fwd2_dur = fwd2_accum; - adj2_dur = adj2_accum; - gemm2_dur = gemm2_accum; - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R2^T * R2 + // ============================================================ + // Iter 2: pcholqr_primitive(A, P = R_1) + // ============================================================ + // Stash R_1 in P_prev before pcholqr_primitive overwrites R with R_2. + lapack::lacpy(MatrixType::Upper, n, n, R, ldr, P_prev, n); if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); - if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] G; delete[] R_temp; delete[] M; + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), P_prev + 1, n); + + long precond_inv2 = 0, update2 = 0; + info = pcholqr_primitive( + A, P_prev, R, ldr, + PCholQRPrecondMethod::TRSM_IDENTITY, + this->block_size, + /*bqrrp_block_ratio=*/T(1.0), + R_pre, G, A_temp, Z_buf, + /*state=*/(RandBLAS::RNGState*)nullptr, + precond_inv2, fwd2_dur, adj2_dur, gemm2_dur, chol2_dur, update2, + this->timing); + if (info != 0) { + delete[] G; delete[] R_pre; delete[] P_prev; delete[] A_temp; delete[] Z_buf; return 2; } + // Fold pcholqr's precond_inv into upd2 alongside the trmm update (matches the prior + // sCholQR3 upd2 semantics = "M update + R update" in one slot). + upd2_dur = precond_inv2 + update2; - // Save G2 factor - this->G2_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G2_factor.data(), n); - - if(this->timing) { - t_stop = steady_clock::now(); - chol2_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // R = R2 * R1 - lapack::lacpy(MatrixType::Upper, n, n, R, ldr, R_temp, n); - blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, R_temp, n); - lapack::lacpy(MatrixType::Upper, n, n, R_temp, n, R, ldr); - - // M = M * R2^{-1} - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, M, n); - - if(this->timing) { - t_stop = steady_clock::now(); - upd2_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Iteration 3: Cholesky QR - //================================================================ - // Blocked Gram: G3 = M^T * A^T * A * M where M = R1^{-1} * R2^{-1} - long fwd3_accum = 0, adj3_accum = 0, gemm3_accum = 0; - for (int64_t j = 0; j < n; j += b_eff) { - int64_t b_j = std::min(b_eff, n - j); - - // W = A * M[:, j:j+b] - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, M + j * n, n, (T)0.0, A_temp, m); - if(this->timing) { t_stop = steady_clock::now(); fwd3_accum += duration_cast(t_stop - t_start).count(); } - - // Z = A^T * W - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, m, (T)1.0, A_temp, m, (T)0.0, Z_buf, n); - if(this->timing) { t_stop = steady_clock::now(); adj3_accum += duration_cast(t_stop - t_start).count(); } - - // G[:, j:j+b] = M^T * Z - if(this->timing) t_start = steady_clock::now(); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, b_j, n, (T)1.0, M, n, Z_buf, n, (T)0.0, G + j * n, n); - if(this->timing) { t_stop = steady_clock::now(); gemm3_accum += duration_cast(t_stop - t_start).count(); } - } - - if(this->timing) { - fwd3_dur = fwd3_accum; - adj3_dur = adj3_accum; - gemm3_dur = gemm3_accum; - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R3^T * R3 + // ============================================================ + // Iter 3: pcholqr_primitive(A, P = R_2) + // ============================================================ + lapack::lacpy(MatrixType::Upper, n, n, R, ldr, P_prev, n); if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); - if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] G; delete[] R_temp; delete[] M; + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), P_prev + 1, n); + + long precond_inv3 = 0, update3 = 0; + info = pcholqr_primitive( + A, P_prev, R, ldr, + PCholQRPrecondMethod::TRSM_IDENTITY, + this->block_size, + /*bqrrp_block_ratio=*/T(1.0), + R_pre, G, A_temp, Z_buf, + /*state=*/(RandBLAS::RNGState*)nullptr, + precond_inv3, fwd3_dur, adj3_dur, gemm3_dur, chol3_dur, update3, + this->timing); + if (info != 0) { + delete[] G; delete[] R_pre; delete[] P_prev; delete[] A_temp; delete[] Z_buf; return 3; } + upd3_dur = precond_inv3 + update3; - // Save G3 factor - this->G3_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G3_factor.data(), n); - - if(this->timing) { - t_stop = steady_clock::now(); - chol3_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // R = R3 * R - lapack::lacpy(MatrixType::Upper, n, n, R, ldr, R_temp, n); - blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, R_temp, n); - lapack::lacpy(MatrixType::Upper, n, n, R_temp, n, R, ldr); - - if(this->timing) { - t_stop = steady_clock::now(); - upd3_dur = duration_cast(t_stop - t_start).count(); - } + // ============================================================ + // Test mode: materialize Q = A * R^{-1} (outside timing region). + // R_pre currently holds R_2^{-1} from iter 3's pcholqr_primitive precond step. + // We need R^{-1} = R_3^{-1} = R^{chol_3}^{-1} * R_2^{-1}. + // Simpler: recompute R^{-1} from R via trsm(R, I), then materialize Q via blocked linop. + // ============================================================ + if (this->test_mode) { + if (this->timing) t0 = steady_clock::now(); - //================================================================ - // Test mode: materialize Q = A * R^{-1} = A * M * R3^{-1} - //================================================================ - if(this->test_mode) { - if(this->timing) - t_start = steady_clock::now(); + RandLAPACK::util::eye(n, n, R_pre); + blas::trsm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, + Diag::NonUnit, n, n, T(1), R, ldr, R_pre, n); - // M currently holds R1^{-1} R2^{-1}; update to R^{-1} = R1^{-1} R2^{-1} R3^{-1} - blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, M, n); - - // Materialize Q = A * M in blocks - T* Q_buf = new T[m * n](); + T* Q_buf = new T[m * n]; for (int64_t j = 0; j < n; j += b_eff) { int64_t b_j = std::min(b_eff, n - j); A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, b_j, n, (T)1.0, M + j * n, n, (T)0.0, Q_buf + j * m, m); + m, b_j, n, (T)1.0, R_pre + j * n, n, (T)0.0, Q_buf + j * m, m); } - this->Q_rows = m; this->Q_cols = n; this->Q = Q_buf; - if(this->timing) { - t_stop = steady_clock::now(); - q_mat_dur = duration_cast(t_stop - t_start).count(); - } + if (this->timing) { t1 = steady_clock::now(); q_mat_dur = duration_cast(t1 - t0).count(); } } - //================================================================ + // ============================================================ // Finalize timing - //================================================================ - if(this->timing) { + // ============================================================ + if (this->timing) { total_t_stop = steady_clock::now(); total_dur = duration_cast(total_t_stop - total_t_start).count(); - - // Subtract Q materialization from total (test overhead, not algorithmic cost) total_dur -= q_mat_dur; long rest_dur = total_dur - (alloc_dur + @@ -426,7 +212,6 @@ class sCholQR3_linops { fwd2_dur + adj2_dur + gemm2_dur + chol2_dur + upd2_dur + fwd3_dur + adj3_dur + gemm3_dur + chol3_dur + upd3_dur); - // 18 entries this->times = {alloc_dur, fwd1_dur, adj1_dur, chol1_dur, upd1_dur, fwd2_dur, adj2_dur, gemm2_dur, chol2_dur, upd2_dur, @@ -434,36 +219,21 @@ class sCholQR3_linops { q_mat_dur, rest_dur, total_dur}; } - // Cleanup delete[] G; - delete[] R_temp; - delete[] M; + delete[] R_pre; + delete[] P_prev; delete[] A_temp; delete[] Z_buf; - return 0; } }; -/// Non-blocked (basic) sCholQR3 algorithm for computing QR factorization via linear operators. -/// -/// Matches the standard sCholQR3 pseudocode from Fukaya et al. (SISC, 2020) exactly: -/// 1. Compute G1 = A^T A via linop, add shift, Cholesky → R1 -/// 2. Materialize Q = A * R1^{-1} via linop -/// 3. Iterations 2-3: G = Q^T Q via dense syrk, Cholesky, Q *= R_k^{-1} via dense trsm -/// -/// Accesses the linear operator exactly 3 times: -/// - NoTrans: W = A * I (materialization for Gram computation) -/// - Trans: G1 = A^T * W (Gram matrix) -/// - NoTrans: Q = A * R1^{-1} (first Q-factor) -/// -/// After the first Q-factor, iterations 2-3 use dense syrk on Q (no further linop calls). -/// This is theoretically distinct from sCholQR3_linops (fully-blocked), which recomputes -/// each Gram through the linop and never materializes the m x n operator product. -/// -/// Peak memory: O(m*n + n^2) — Q is explicitly stored as m x n dense. +/// Non-blocked (basic) sCholQR3 — materializes Q = A * R_1^{-1} after iter 1 and uses +/// dense syrk for iters 2, 3. Iter 1 still routes through cholqr_primitive; iters 2-3 +/// stay inline because they operate on the dense Q buffer (not the linop). /// -/// Reference: Shifted Cholesky QR from Fukaya et al. (SISC, 2020). +/// Linop accesses: exactly 3 (NoTrans for Gram-step, Trans for Gram-step, NoTrans to +/// materialize Q after iter 1). /// template class sCholQR3_linops_basic { @@ -473,32 +243,15 @@ class sCholQR3_linops_basic { bool test_mode; T eps; - // Q-factor for test mode (only allocated if test_mode = true) T* Q; int64_t Q_rows; int64_t Q_cols; - // Individual Cholesky factors from each iteration (n x n upper triangular). - std::vector G1_factor; - std::vector G2_factor; - std::vector G3_factor; - - // Timing breakdown (15 entries): - // [0] alloc - buffer allocation - // [1] fwd1 - NoTrans: W = A * I (m x n) - // [2] adj1 - Trans: G = A^T * W (n x n) - // [3] chol1 - Iter 1 Cholesky - // [4] trsm1 - M = R1^{-1} (n x n trsm) - // [5] fwd_q - NoTrans: Q = A * M (m x n) - // [6] syrk2 - G = Q^T Q - // [7] chol2 - Iter 2 Cholesky - // [8] upd2 - Q *= R2^{-1}, R = R2*R1 - // [9] syrk3 - G = Q^T Q - // [10] chol3 - Iter 3 Cholesky - // [11] upd3 - R = R3*R - // [12] q_mat - test mode: Q_buf *= R3^{-1} (m x n trsm), 0 otherwise - // [13] rest - unaccounted time - // [14] total - wall-clock total + // Timing breakdown (15 entries; layout preserved for matlab plotters): + // [0] alloc [1] fwd1 [2] adj1 [3] chol1 [4] trsm1 [5] fwd_q + // [6] syrk2 [7] chol2 [8] upd2 + // [9] syrk3 [10] chol3 [11] upd3 + // [12] q_mat [13] rest [14] total std::vector times; sCholQR3_linops_basic( @@ -520,269 +273,126 @@ class sCholQR3_linops_basic { } } - /// Computes the QR factorization A = QR using shifted Cholesky QR3 (basic variant). - /// - /// @param[in] A - /// The m-by-n linear operator (m and n read from A.n_rows, A.n_cols). - /// - /// @param[out] R - /// Pre-allocated n-by-n buffer. On exit, stores the upper-triangular - /// R factor. Zero entries are not compressed. - /// - /// @param[in] ldr - /// Leading dimension of R. - /// - /// @return = 0: successful exit template int call( GLO& A, T* R, int64_t ldr ) { - ///--------------------TIMING VARS--------------------/ - steady_clock::time_point t_start, t_stop; - steady_clock::time_point total_t_start, total_t_stop; + steady_clock::time_point t0, t1, total_t_start, total_t_stop; long alloc_dur = 0; long fwd1_dur = 0, adj1_dur = 0, chol1_dur = 0, trsm1_dur = 0, fwd_q_dur = 0; long syrk2_dur = 0, chol2_dur = 0, upd2_dur = 0; long syrk3_dur = 0, chol3_dur = 0, upd3_dur = 0; long q_mat_dur = 0, total_dur = 0; - if(this->timing) - total_t_start = steady_clock::now(); + if (this->timing) total_t_start = steady_clock::now(); int64_t m = A.n_rows; int64_t n = A.n_cols; - if(this->timing) - t_start = steady_clock::now(); - - // ---- Allocate buffers ---- - // Q_buf: m x n — materialized operator, updated in-place through iterations - T* Q_buf = new T[m * n]; - - // G: n x n Gram matrix / Cholesky workspace (zero-init for lower triangle) - T* G = new T[n * n](); - - // R_temp: n x n workspace for R accumulation via trmm - T* R_temp = new T[n * n](); - - // M: n x n — starts as identity, becomes R1^{-1} for Q materialization - T* M = new T[n * n](); - RandLAPACK::util::eye(n, n, M); - - if(this->timing) { - t_stop = steady_clock::now(); - alloc_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Iteration 1: Shifted Cholesky QR - //================================================================ - // Gram: G1 = A^T * A via linop (2 of 3 total linop accesses) - - // Linop access 1: W = A * I (NoTrans, materializes operator as m x n dense) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, M, n, (T)0.0, Q_buf, m); - if(this->timing) { t_stop = steady_clock::now(); fwd1_dur = duration_cast(t_stop - t_start).count(); } - - // Linop access 2: G1 = A^T * W (Trans, n x n Gram matrix) - if(this->timing) t_start = steady_clock::now(); - A(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, Q_buf, m, (T)0.0, G, n); - if(this->timing) { t_stop = steady_clock::now(); adj1_dur = duration_cast(t_stop - t_start).count(); } - - // Compute shift from ||A||_F^2 = trace(G) - T norm_A_sq = 0; - for (int64_t i = 0; i < n; ++i) - norm_A_sq += G[i * (n + 1)]; - T shift = 11 * std::numeric_limits::epsilon() * n * norm_A_sq; - - // Add shift to diagonal: G = G + shift * I - for (int64_t i = 0; i < n; ++i) - G[i * (n + 1)] += shift; - - if(this->timing) { - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R1^T * R1 - if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); - if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] Q_buf; delete[] G; delete[] R_temp; delete[] M; + if (this->timing) t0 = steady_clock::now(); + T* Q_buf = new T[m * n]; // materialized operator, updated in-place through iters + T* G = new T[n * n](); // Gram workspace + T* M = new T[n * n](); // n × n — R1^{-1} for Q materialization + if (this->timing) { t1 = steady_clock::now(); alloc_dur = duration_cast(t1 - t0).count(); } + + // ---- Iter 1: shifted CholQR via cholqr_primitive (no Q_buf yet) ---- + T shift_factor_iter1 = (T)11.0 * std::numeric_limits::epsilon() * (T)n; + int info = cholqr_primitive( + A, R, ldr, + shift_factor_iter1, + /*block_size=*/0, // basic variant is non-blocked + G, Q_buf, // re-use Q_buf as the m × b_eff scratch for cholqr_primitive + fwd1_dur, adj1_dur, chol1_dur, this->timing); + if (info != 0) { + delete[] Q_buf; delete[] G; delete[] M; return 1; } - // Save G1 factor - this->G1_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G1_factor.data(), n); - - // Initialize R = R1 - lapack::lacpy(MatrixType::Upper, n, n, G, n, R, ldr); - - if(this->timing) { - t_stop = steady_clock::now(); - chol1_dur = duration_cast(t_stop - t_start).count(); - } - - // Compute M = I * R1^{-1} = R1^{-1} - if(this->timing) t_start = steady_clock::now(); + // ---- Materialize Q_buf = A * R1^{-1} (NoTrans linop call #3) ---- + // First compute M = I * R_1^{-1} = R_1^{-1} via Side::Right TRSM(I, R_1). + if (this->timing) t0 = steady_clock::now(); + RandLAPACK::util::eye(n, n, M); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, M, n); - if(this->timing) { t_stop = steady_clock::now(); trsm1_dur = duration_cast(t_stop - t_start).count(); } + Diag::NonUnit, n, n, T(1), R, ldr, M, n); + if (this->timing) { t1 = steady_clock::now(); trsm1_dur = duration_cast(t1 - t0).count(); } - // Linop access 3: Q_buf = A * M = A * R1^{-1} (NoTrans, m x n) - if(this->timing) t_start = steady_clock::now(); + if (this->timing) t0 = steady_clock::now(); A(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, M, n, (T)0.0, Q_buf, m); - if(this->timing) { t_stop = steady_clock::now(); fwd_q_dur = duration_cast(t_stop - t_start).count(); } - - //================================================================ - // Iteration 2: Cholesky QR (dense syrk on Q_buf) - //================================================================ - if(this->timing) - t_start = steady_clock::now(); + if (this->timing) { t1 = steady_clock::now(); fwd_q_dur = duration_cast(t1 - t0).count(); } - // G2 = Q_buf^T * Q_buf (dense syrk, upper triangle only) + // ---- Iter 2: dense syrk on Q_buf -> G2 = R_2^T R_2, then Q_buf *= R_2^{-1}, R = R_2 * R ---- + if (this->timing) t0 = steady_clock::now(); blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, n, m, (T)1.0, Q_buf, m, (T)0.0, G, n); + if (this->timing) { t1 = steady_clock::now(); syrk2_dur = duration_cast(t1 - t0).count(); t0 = t1; } - if(this->timing) { - t_stop = steady_clock::now(); - syrk2_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R2^T * R2 if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), &G[1], n); if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] Q_buf; delete[] G; delete[] R_temp; delete[] M; + delete[] Q_buf; delete[] G; delete[] M; return 2; } + if (this->timing) { t1 = steady_clock::now(); chol2_dur = duration_cast(t1 - t0).count(); t0 = t1; } - // Save G2 factor - this->G2_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G2_factor.data(), n); - - if(this->timing) { - t_stop = steady_clock::now(); - chol2_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // Q_buf *= R2^{-1} (m x n trsm — update Q in-place) blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, G, n, Q_buf, m); - - // R = R2 * R1 - lapack::lacpy(MatrixType::Upper, n, n, R, ldr, R_temp, n); blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, R_temp, n); - lapack::lacpy(MatrixType::Upper, n, n, R_temp, n, R, ldr); - - if(this->timing) { - t_stop = steady_clock::now(); - upd2_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Iteration 3: Cholesky QR (dense syrk on Q_buf) - //================================================================ - if(this->timing) - t_start = steady_clock::now(); + Diag::NonUnit, n, n, (T)1.0, G, n, R, ldr); + if (this->timing) { t1 = steady_clock::now(); upd2_dur = duration_cast(t1 - t0).count(); } - // G3 = Q_buf^T * Q_buf (dense syrk, upper triangle only) + // ---- Iter 3: same pattern ---- + if (this->timing) t0 = steady_clock::now(); blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, n, m, (T)1.0, Q_buf, m, (T)0.0, G, n); + if (this->timing) { t1 = steady_clock::now(); syrk3_dur = duration_cast(t1 - t0).count(); t0 = t1; } - if(this->timing) { - t_stop = steady_clock::now(); - syrk3_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // Zero lower triangle, Cholesky: G = R3^T * R3 if (n > 1) - lapack::laset(MatrixType::Lower, n-1, n-1, (T)0.0, (T)0.0, &G[1], n); + lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), &G[1], n); if (lapack::potrf(Uplo::Upper, n, G, n)) { - delete[] Q_buf; delete[] G; delete[] R_temp; delete[] M; + delete[] Q_buf; delete[] G; delete[] M; return 3; } + if (this->timing) { t1 = steady_clock::now(); chol3_dur = duration_cast(t1 - t0).count(); t0 = t1; } - // Save G3 factor - this->G3_factor.resize(n * n, (T)0.0); - lapack::lacpy(MatrixType::Upper, n, n, G, n, this->G3_factor.data(), n); - - if(this->timing) { - t_stop = steady_clock::now(); - chol3_dur = duration_cast(t_stop - t_start).count(); - t_start = steady_clock::now(); - } - - // R = R3 * R - lapack::lacpy(MatrixType::Upper, n, n, R, ldr, R_temp, n); blas::trmm(Layout::ColMajor, Side::Left, Uplo::Upper, Op::NoTrans, - Diag::NonUnit, n, n, (T)1.0, G, n, R_temp, n); - lapack::lacpy(MatrixType::Upper, n, n, R_temp, n, R, ldr); - - if(this->timing) { - t_stop = steady_clock::now(); - upd3_dur = duration_cast(t_stop - t_start).count(); - } - - //================================================================ - // Test mode: Q = Q_buf * R3^{-1} - //================================================================ - if(this->test_mode) { - if(this->timing) - t_start = steady_clock::now(); - - // Q_buf currently holds A * R1^{-1} * R2^{-1} - // Apply R3^{-1}: Q_buf = Q_buf * R3^{-1} = A * R^{-1} = Q + Diag::NonUnit, n, n, (T)1.0, G, n, R, ldr); + if (this->timing) { t1 = steady_clock::now(); upd3_dur = duration_cast(t1 - t0).count(); } + + // ---- Test mode: Q_buf *= R_3^{-1} so Q = A * R^{-1} ---- + if (this->test_mode) { + if (this->timing) t0 = steady_clock::now(); + // Iter 3 didn't update Q_buf (we skip the m × n trsm there since iter 3 is the last + // Cholesky polish; the m × n trsm for R_3^{-1} only matters if we want Q). blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, G, n, Q_buf, m); - this->Q_rows = m; this->Q_cols = n; - this->Q = Q_buf; // Take ownership of Q_buf - - if(this->timing) { - t_stop = steady_clock::now(); - q_mat_dur = duration_cast(t_stop - t_start).count(); - } + this->Q = Q_buf; + if (this->timing) { t1 = steady_clock::now(); q_mat_dur = duration_cast(t1 - t0).count(); } } - //================================================================ - // Finalize timing - //================================================================ - if(this->timing) { + // ---- Finalize timing ---- + if (this->timing) { total_t_stop = steady_clock::now(); total_dur = duration_cast(total_t_stop - total_t_start).count(); - - // Subtract Q materialization from total (test overhead, not algorithmic cost) total_dur -= q_mat_dur; long rest_dur = total_dur - (alloc_dur + fwd1_dur + adj1_dur + chol1_dur + trsm1_dur + fwd_q_dur + syrk2_dur + chol2_dur + upd2_dur + syrk3_dur + chol3_dur + upd3_dur); - - // 15 entries this->times = {alloc_dur, fwd1_dur, adj1_dur, chol1_dur, trsm1_dur, fwd_q_dur, syrk2_dur, chol2_dur, upd2_dur, syrk3_dur, chol3_dur, upd3_dur, q_mat_dur, rest_dur, total_dur}; } - // Cleanup delete[] G; - delete[] R_temp; delete[] M; - if(!this->test_mode) + if (!this->test_mode) delete[] Q_buf; - return 0; } }; diff --git a/RandLAPACK/testing/rl_memory_tracker.hh b/RandLAPACK/testing/rl_memory_tracker.hh index 9040c2be8..6f4bbd977 100644 --- a/RandLAPACK/testing/rl_memory_tracker.hh +++ b/RandLAPACK/testing/rl_memory_tracker.hh @@ -121,25 +121,21 @@ static inline long cholqr_linops_analytical_kb(int64_t m, int64_t n, int64_t blo } // sCholQR3_linops (fully-blocked): -// local: G(n*n) + R_temp(n*n) + M(n*n) + A_temp(m*b) + Z_buf(n*b) -// member: G1_factor(n*n) + G2_factor(n*n) + G3_factor(n*n) -// Peak is right after G3_factor.resize() at the end of iteration 3, where all 6 n*n -// buffers (G + R_temp + M + G1 + G2 + G3) and the block buffers (A_temp + Z_buf) are live. +// local: G(n*n) + M(n*n) + A_temp(m*b) + Z_buf(n*b) +// In-place trmm for R-updates (no R_temp scratch); no persisted G_k_factor members. template static inline long scholqr3_linops_analytical_kb(int64_t m, int64_t n, int64_t block_size = 0) { int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; - long bytes = static_cast(sizeof(T)) * (6L * n * n + (long)(m + n) * b_eff); + long bytes = static_cast(sizeof(T)) * (2L * n * n + (long)(m + n) * b_eff); return bytes / 1024; } -// sCholQR3_linops_basic: Q_buf(m*n) + G(n*n) + R_temp(n*n) + M(n*n) -// + G1_factor(n*n) + G2_factor(n*n) + G3_factor(n*n) +// sCholQR3_linops_basic: Q_buf(m*n) + G(n*n) + M(n*n). // Materializes Q = A * R1^{-1} after iteration 1, then uses dense syrk for iterations 2-3. -// Peak is right after G3_factor.resize() in iteration 3, where Q_buf, the three local -// n*n workspaces, and all three persisted Cholesky factors are simultaneously live. +// In-place trmm for R-updates (no R_temp scratch); no persisted G_k_factor members. template static inline long scholqr3_linops_basic_analytical_kb(int64_t m, int64_t n) { - long bytes = static_cast(sizeof(T)) * ((long)m * n + 6L * n * n); + long bytes = static_cast(sizeof(T)) * ((long)m * n + 2L * n * n); return bytes / 1024; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index f911da60b..ec6b6c074 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -59,7 +59,6 @@ #include "rl_blaspp.hh" #include "rl_lapackpp.hh" #include "rl_gen.hh" -#include "rl_cqrrt_linops.hh" #include #include diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index b421a2a8d..fd88898cd 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -55,7 +55,6 @@ #include "cqrrt_bench_common.hh" // Linops algorithms -#include "rl_cqrrt_linops.hh" #include "rl_cholqr_linops.hh" #include "rl_scholqr3_linops.hh" #include "RandLAPACK/testing/rl_memory_tracker.hh" diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc index 4f250b5ea..233301a28 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc @@ -25,7 +25,6 @@ #include "cqrrt_bench_common.hh" // Linops algorithms (now in main RandLAPACK) -#include "rl_cqrrt_linops.hh" #include "rl_cholqr_linops.hh" #include "rl_scholqr3_linops.hh" #include "RandLAPACK/testing/rl_memory_tracker.hh" diff --git a/test/drivers/test_orth_linop.cc b/test/drivers/test_orth_linop.cc index a5eed91f2..ab9ff76bd 100644 --- a/test/drivers/test_orth_linop.cc +++ b/test/drivers/test_orth_linop.cc @@ -339,6 +339,73 @@ TEST_F(TestCQRRTLinops, block_vs_full_agreement) { ASSERT_LE(norm_diff / norm_R, 1000 * std::numeric_limits::epsilon()); } +// --- Precond-method coverage: TRTRI / GEQP3 / BQRRP all should produce a +// valid Q-less QR (Q = A * R^{-1} has orthonormal columns). Exercises +// the dispatch in pcholqr_primitive via the CQRRT_linops wrapper. + +TEST_F(TestCQRRTLinops, precond_method_TRTRI) { + int64_t m = 100, n = 50; + double d_factor = 2.0; + + std::vector A_data(m * n); + RandBLAS::DenseDist D(m, n); + RandBLAS::RNGState<> state(7); + RandBLAS::fill_dense(D, A_data.data(), state); + std::vector A_copy = A_data; + + RandLAPACK::linops::DenseLinOp A_linop(m, n, A_data.data(), m, Layout::ColMajor); + + std::vector R(n * n, 0.0); + RandLAPACK::CQRRT_linops algo(false, default_tol(), true); + algo.precond_method = RandLAPACK::PCholQRPrecondMethod::TRTRI; + state = RandBLAS::RNGState<>(11); + ASSERT_EQ(algo.call(A_linop, R.data(), n, d_factor, state), 0); + + assert_qr_ok(A_copy.data(), algo.Q, R.data(), m, n, n); +} + +TEST_F(TestCQRRTLinops, precond_method_GEQP3) { + int64_t m = 100, n = 50; + double d_factor = 2.0; + + std::vector A_data(m * n); + RandBLAS::DenseDist D(m, n); + RandBLAS::RNGState<> state(7); + RandBLAS::fill_dense(D, A_data.data(), state); + std::vector A_copy = A_data; + + RandLAPACK::linops::DenseLinOp A_linop(m, n, A_data.data(), m, Layout::ColMajor); + + std::vector R(n * n, 0.0); + RandLAPACK::CQRRT_linops algo(false, default_tol(), true); + algo.precond_method = RandLAPACK::PCholQRPrecondMethod::GEQP3; + state = RandBLAS::RNGState<>(11); + ASSERT_EQ(algo.call(A_linop, R.data(), n, d_factor, state), 0); + + assert_qr_ok(A_copy.data(), algo.Q, R.data(), m, n, n); +} + +TEST_F(TestCQRRTLinops, precond_method_BQRRP) { + int64_t m = 100, n = 50; + double d_factor = 2.0; + + std::vector A_data(m * n); + RandBLAS::DenseDist D(m, n); + RandBLAS::RNGState<> state(7); + RandBLAS::fill_dense(D, A_data.data(), state); + std::vector A_copy = A_data; + + RandLAPACK::linops::DenseLinOp A_linop(m, n, A_data.data(), m, Layout::ColMajor); + + std::vector R(n * n, 0.0); + RandLAPACK::CQRRT_linops algo(false, default_tol(), true); + algo.precond_method = RandLAPACK::PCholQRPrecondMethod::BQRRP; + state = RandBLAS::RNGState<>(11); + ASSERT_EQ(algo.call(A_linop, R.data(), n, d_factor, state), 0); + + assert_qr_ok(A_copy.data(), algo.Q, R.data(), m, n, n); +} + // ============================================================================ // sCholQR3_linops (fully-blocked) // ============================================================================ From 5c5025560e0bc7cce3875a10aa0292e8db1b75b8 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 1 Jun 2026 15:46:28 -0700 Subject: [PATCH 21/47] Add PowerOp linop and sparse_axpby helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PowerOp (RandLAPACK/linops/rl_power_linop.hh) — generic wrapper representing A^j for a square base linear operator A. Chains j calls to the base op with two ping-pong scratch buffers; A^j is never materialized. j == 1 takes a no-scratch fast path; j == 2 allocates one scratch; j >= 3 allocates two. Op::Trans dispatches base(Op::Trans, ...) j times. Side::Left only (the only consumer pattern we have so far). sparse_axpby_shared_pattern (extras/misc/ext_sparse_axpy.hh) — computes C := alpha*A + beta*B for two CSRMatrix inputs whose sparsity patterns are bit-identical (rowptr + colidxs equal). O(nnz) value-only path. The target consumer is X = K - omega*M in the reduced-spectral application, where K and M from a single FEM mesh always share sparsity exactly. A general-purpose sparse_axpby for different patterns belongs upstream (RandBLAS issue; MKL has mkl_sparse_d_add, cuSPARSE has cusparseDcsrgeam2). Tests: 6 new PowerOp tests (j=1, j=3, multi-RHS, Op::Trans, alpha/beta, PowerOp wrapping CompositeOperator — the rspec usage pattern). 3 new sparse_axpby tests (tridiagonal, shifted-inverse pattern, float type). All 9 pass; full test suite still passes. --- RandLAPACK/linops/rl_linops.hh | 1 + RandLAPACK/linops/rl_power_linop.hh | 111 +++++++++++ extras/misc/ext_sparse_axpy.hh | 66 +++++++ extras/test/CMakeLists.txt | 1 + extras/test/misc/test_ext_sparse_axpy.cc | 102 ++++++++++ test/CMakeLists.txt | 1 + test/linops/test_power_linop.cc | 225 +++++++++++++++++++++++ 7 files changed, 507 insertions(+) create mode 100644 RandLAPACK/linops/rl_power_linop.hh create mode 100644 extras/misc/ext_sparse_axpy.hh create mode 100644 extras/test/misc/test_ext_sparse_axpy.cc create mode 100644 test/linops/test_power_linop.cc diff --git a/RandLAPACK/linops/rl_linops.hh b/RandLAPACK/linops/rl_linops.hh index b981f2c35..c9ff29cdb 100644 --- a/RandLAPACK/linops/rl_linops.hh +++ b/RandLAPACK/linops/rl_linops.hh @@ -14,5 +14,6 @@ #include "rl_dense_linop.hh" #include "rl_sparse_linop.hh" #include "rl_composite_linop.hh" +#include "rl_power_linop.hh" #include "rl_sym_linops.hh" #include "rl_materialize.hh" diff --git a/RandLAPACK/linops/rl_power_linop.hh b/RandLAPACK/linops/rl_power_linop.hh new file mode 100644 index 000000000..5873d313b --- /dev/null +++ b/RandLAPACK/linops/rl_power_linop.hh @@ -0,0 +1,111 @@ +#pragma once + +// Public API: PowerOp — implicit j-th power of a square linear operator. + +#include "rl_concepts.hh" +#include "rl_blaspp.hh" + +#include +#include +#include + + +namespace RandLAPACK::linops { + +/*********************************************************/ +/* */ +/* PowerOp */ +/* */ +/*********************************************************/ +// Generic LinearOperator wrapper that represents A^j for a square base operator A. +// +// Template parameter: +// InnerOp - Square base operator satisfying LinearOperator concept (dense, sparse, +// composite, sparse-LU-inverse, etc.) +// +// Strategy: +// Each application chains j calls to the base operator with two ping-pong scratch +// buffers. A^j is never materialized. +// +// j == 1: single base call, no scratch. +// j >= 2: one scratch for the first apply; a second scratch for the middle applies +// (j == 2 only allocates the first). The final apply writes to C and +// respects the user's alpha/beta. +// +// Restrictions: +// - base.n_rows must equal base.n_cols. PowerOp is only well-defined for square base. +// - Side::Left only (square A^j on the left of B). Side::Right could be added by +// mirroring the loop, but no current consumer needs it. +// +// Op::Trans semantics: +// trans_A == Op::Trans applies (A^T)^j == (A^j)^T — each iteration dispatches the +// base op with Op::Trans. Intermediate scratch dispatches always use Op::NoTrans. +// +template +struct PowerOp { + using T = typename InnerOp::scalar_t; + using scalar_t = T; + + InnerOp& base; + const int j; + const int64_t n_rows; + const int64_t n_cols; + + PowerOp(InnerOp& base_op, int power) + : base(base_op), j(power), + n_rows(base_op.n_rows), n_cols(base_op.n_cols) + { + randblas_require(base.n_rows == base.n_cols); // PowerOp requires square base + randblas_require(power >= 1); // identity (j=0) not supported; caller can lacpy + } + + // C := alpha * (base^j)^{trans_A} * op_{trans_B}(B) + beta * C + // + // Since base is square (N x N), m == k == N for any valid Side::Left call. + // op_{trans_B}(B) has shape N x n; C has shape N x n. + void operator()( + Side side, Layout layout, + Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, T* const B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + randblas_require(side == Side::Left); + randblas_require(m == n_rows); + randblas_require(k == n_rows); + + if (j == 1) { + base(side, layout, trans_A, trans_B, + m, n, k, alpha, B, ldb, beta, C, ldc); + return; + } + + // j >= 2: ping-pong scratch. + // Layout-aware leading dimension: m for ColMajor (m x n stored), n for RowMajor. + int64_t ldt = (layout == Layout::ColMajor) ? m : n; + T* buf_a = new T[(size_t)m * (size_t)n](); + T* buf_b = (j >= 3) ? new T[(size_t)m * (size_t)n]() : nullptr; + + // 1st apply: buf_a := base^{trans_A} * op_{trans_B}(B) + base(side, layout, trans_A, trans_B, + m, n, k, (T)1.0, B, ldb, (T)0.0, buf_a, ldt); + + // Middle applies (only run when j >= 3): ping-pong buf_a <-> buf_b. + T* in_buf = buf_a; + T* out_buf = buf_b; + for (int it = 1; it < j - 1; ++it) { + base(side, layout, trans_A, Op::NoTrans, + m, n, m, (T)1.0, in_buf, ldt, (T)0.0, out_buf, ldt); + std::swap(in_buf, out_buf); + } + + // Last apply writes to C with the user's alpha/beta. + base(side, layout, trans_A, Op::NoTrans, + m, n, m, alpha, in_buf, ldt, beta, C, ldc); + + delete[] buf_a; + if (buf_b) delete[] buf_b; + } +}; + +} // namespace RandLAPACK::linops diff --git a/extras/misc/ext_sparse_axpy.hh b/extras/misc/ext_sparse_axpy.hh new file mode 100644 index 000000000..2e5598c8b --- /dev/null +++ b/extras/misc/ext_sparse_axpy.hh @@ -0,0 +1,66 @@ +#pragma once + +// RandLAPACK Extras - Sparse axpy +// +// Helper: C := alpha * A + beta * B for two RandBLAS::CSRMatrix inputs whose +// nonzero patterns (rowptr + colidxs) are bit-identical. +// +// Use case: forming X = K - omega*M in the reduced spectral application, where +// K and M come from a single FEM mesh and therefore share sparsity exactly. +// +// This is the fast O(nnz) value-only path. A general-purpose `sparse_axpby` +// supporting different sparsity patterns belongs upstream in RandBLAS (or +// punts to MKL `mkl_sparse_d_add` / cuSPARSE `cusparseDcsrgeam2`); we don't +// need it for the current use case and would just be replicating those libs. + +#include +#include +#include +#include + +namespace RandLAPACK_extras { + +/// Compute C := alpha * A + beta * B with A, B sharing sparsity pattern. +/// +/// Requirements: +/// - A.n_rows == B.n_rows, A.n_cols == B.n_cols, A.nnz == B.nnz +/// - A.index_base == B.index_base +/// - A.rowptr[i] == B.rowptr[i] for all i in [0, n_rows] +/// - A.colidxs[i] == B.colidxs[i] for all i in [0, nnz) +/// +/// Returns a newly-owned CSRMatrix C whose sparsity pattern is copied from A. +template +RandBLAS::sparse_data::CSRMatrix sparse_axpby_shared_pattern( + T alpha, + const RandBLAS::sparse_data::CSRMatrix& A, + T beta, + const RandBLAS::sparse_data::CSRMatrix& B) +{ + randblas_require(A.n_rows == B.n_rows); + randblas_require(A.n_cols == B.n_cols); + randblas_require(A.nnz == B.nnz); + randblas_require(A.index_base == B.index_base); + + // Verify the rowptr arrays match. This catches user error early when + // someone forgets the "shared sparsity" precondition. + for (int64_t i = 0; i <= A.n_rows; ++i) + randblas_require(A.rowptr[i] == B.rowptr[i]); + // colidxs comparison: O(nnz) but cheap relative to any downstream sparse op. + for (int64_t i = 0; i < A.nnz; ++i) + randblas_require(A.colidxs[i] == B.colidxs[i]); + + RandBLAS::sparse_data::CSRMatrix C(A.n_rows, A.n_cols); + C.reserve(A.nnz); + C.index_base = A.index_base; + + std::memcpy(C.rowptr, A.rowptr, sizeof(sint_t) * (A.n_rows + 1)); + std::memcpy(C.colidxs, A.colidxs, sizeof(sint_t) * A.nnz); + + #pragma omp parallel for schedule(static) + for (int64_t i = 0; i < A.nnz; ++i) + C.vals[i] = alpha * A.vals[i] + beta * B.vals[i]; + + return C; +} + +} // namespace RandLAPACK_extras diff --git a/extras/test/CMakeLists.txt b/extras/test/CMakeLists.txt index 798e1f398..ac041b62e 100644 --- a/extras/test/CMakeLists.txt +++ b/extras/test/CMakeLists.txt @@ -15,6 +15,7 @@ if (GTest_FOUND) set(RandLAPACK_extras_test_srcs linops/test_ext_solver_linop_unified.cc linops/test_ext_composite_linop.cc + misc/test_ext_sparse_axpy.cc ) # Create test executable diff --git a/extras/test/misc/test_ext_sparse_axpy.cc b/extras/test/misc/test_ext_sparse_axpy.cc new file mode 100644 index 000000000..047d304e6 --- /dev/null +++ b/extras/test/misc/test_ext_sparse_axpy.cc @@ -0,0 +1,102 @@ +// Tests for sparse_axpby_shared_pattern in extras/misc/ext_sparse_axpy.hh. + +#include "../../misc/ext_sparse_axpy.hh" + +#include +#include +#include +#include + +using std::vector; + + +// Build a small CSR matrix (n_rows × n_cols, nnz given) by deep-copying user-provided +// rowptr/colidxs/vals arrays. The matrix owns its memory and frees on destruction. +template +RandBLAS::sparse_data::CSRMatrix make_owned_csr( + int64_t n_rows, int64_t n_cols, int64_t nnz, + const sint_t* rowptr_src, const sint_t* colidxs_src, const T* vals_src) +{ + RandBLAS::sparse_data::CSRMatrix M(n_rows, n_cols); + M.reserve(nnz); + for (int64_t i = 0; i <= n_rows; ++i) M.rowptr[i] = rowptr_src[i]; + for (int64_t i = 0; i < nnz; ++i) M.colidxs[i] = colidxs_src[i]; + for (int64_t i = 0; i < nnz; ++i) M.vals[i] = vals_src[i]; + return M; +} + + +TEST(TestSparseAxpby, shared_pattern_simple) { + // 3x3 tridiagonal pattern. + // row 0: cols 0, 1 + // row 1: cols 0, 1, 2 + // row 2: cols 1, 2 + // nnz = 7 + int rowptr[4] = {0, 2, 5, 7}; + int colidxs[7] = {0, 1, 0, 1, 2, 1, 2}; + double A_vals[7] = {1, 2, 3, 4, 5, 6, 7}; + double B_vals[7] = {10, 20, 30, 40, 50, 60, 70}; + + auto A = make_owned_csr(3, 3, 7, rowptr, colidxs, A_vals); + auto B = make_owned_csr(3, 3, 7, rowptr, colidxs, B_vals); + + // C := 2.0 * A + (-3.0) * B + auto C = RandLAPACK_extras::sparse_axpby_shared_pattern( + 2.0, A, -3.0, B); + + ASSERT_EQ(C.n_rows, 3); + ASSERT_EQ(C.n_cols, 3); + ASSERT_EQ(C.nnz, 7); + + for (int i = 0; i <= 3; ++i) EXPECT_EQ(C.rowptr[i], rowptr[i]); + for (int i = 0; i < 7; ++i) EXPECT_EQ(C.colidxs[i], colidxs[i]); + + double expected[7]; + for (int i = 0; i < 7; ++i) + expected[i] = 2.0 * A_vals[i] + (-3.0) * B_vals[i]; + + for (int i = 0; i < 7; ++i) + EXPECT_NEAR(C.vals[i], expected[i], 1e-14); +} + + +TEST(TestSparseAxpby, shifted_inverse_pattern) { + // Mimics the rspec use case: X = K - omega * M with K, M sharing sparsity. + // Symmetric 4x4 pattern with 10 nonzeros. + int rowptr[5] = {0, 3, 6, 8, 10}; + int colidxs[10] = {0, 1, 2, 0, 1, 3, 0, 2, 1, 3}; + double K_vals[10] = {4, 1, 1, 1, 3, 1, 1, 2, 1, 5}; + double M_vals[10] = {2, 0.5, 0.5, 0.5, 2, 0.5, 0.5, 1, 0.5, 2}; + + auto K = make_owned_csr(4, 4, 10, rowptr, colidxs, K_vals); + auto M = make_owned_csr(4, 4, 10, rowptr, colidxs, M_vals); + + double omega = 0.7; + auto X = RandLAPACK_extras::sparse_axpby_shared_pattern( + 1.0, K, -omega, M); + + for (int i = 0; i < 10; ++i) { + double expected = K_vals[i] - omega * M_vals[i]; + EXPECT_NEAR(X.vals[i], expected, 1e-14); + } + ASSERT_EQ(X.nnz, 10); + for (int i = 0; i <= 4; ++i) EXPECT_EQ(X.rowptr[i], rowptr[i]); + for (int i = 0; i < 10; ++i) EXPECT_EQ(X.colidxs[i], colidxs[i]); +} + + +TEST(TestSparseAxpby, float_type) { + int rowptr[3] = {0, 1, 2}; + int colidxs[2] = {0, 1}; + float A_vals[2] = {1.5f, 2.5f}; + float B_vals[2] = {0.5f, 1.0f}; + + auto A = make_owned_csr(2, 2, 2, rowptr, colidxs, A_vals); + auto B = make_owned_csr(2, 2, 2, rowptr, colidxs, B_vals); + + auto C = RandLAPACK_extras::sparse_axpby_shared_pattern( + 2.0f, A, 4.0f, B); + + EXPECT_NEAR(C.vals[0], 2.0f * 1.5f + 4.0f * 0.5f, 1e-6f); + EXPECT_NEAR(C.vals[1], 2.0f * 2.5f + 4.0f * 1.0f, 1e-6f); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2bbd5dba0..42279eca2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,6 +26,7 @@ if (GTest_FOUND) misc/test_util.cc misc/test_pdkernels.cc linops/test_linops.cc + linops/test_power_linop.cc linops/test_linop_unified.cc misc/test_gen.cc misc/test_memory_tracker.cc diff --git a/test/linops/test_power_linop.cc b/test/linops/test_power_linop.cc new file mode 100644 index 000000000..b837df00f --- /dev/null +++ b/test/linops/test_power_linop.cc @@ -0,0 +1,225 @@ +// Tests for PowerOp: implicit j-th power of a square LinearOperator. + +#include +#include +#include +#include +#include +#include + +using std::vector; +using blas::Layout; +using blas::Op; +using blas::Side; +using RandBLAS::RNGState; + +class TestPowerOp : public ::testing::Test { +protected: + virtual void SetUp() {} + virtual void TearDown() {} + + // Reference apply: y = A^j * x via dense GEMV iteration. + template + void apply_power_reference(const T* A, int64_t n, int j, + const T* x_in, T* x_out) + { + vector buf_a(n), buf_b(n); + std::copy(x_in, x_in + n, buf_a.data()); + T* in = buf_a.data(); + T* out = buf_b.data(); + for (int it = 0; it < j; ++it) { + blas::gemv(Layout::ColMajor, Op::NoTrans, n, n, + (T)1.0, A, n, in, 1, (T)0.0, out, 1); + std::swap(in, out); + } + std::copy(in, in + n, x_out); + } + + template + double rel_err(const T* a, const T* b, int64_t n) { + double err = 0.0, ref = 0.0; + for (int64_t i = 0; i < n; ++i) { + double d = (double)a[i] - (double)b[i]; + err += d * d; + ref += (double)b[i] * (double)b[i]; + } + return (ref > 0) ? std::sqrt(err / ref) : std::sqrt(err); + } +}; + +// PowerOp(A, 1) · v should equal a single base apply. +TEST_F(TestPowerOp, j_equals_1_single_apply) { + int64_t n = 50; + vector A(n * n); + RNGState<> state(42); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A_pow(A_op, 1); + + vector x(n); + state = RNGState<>(1); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + vector y_pow(n, 0.0); + A_pow(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, x.data(), n, 0.0, y_pow.data(), n); + + vector y_ref(n); + apply_power_reference(A.data(), n, 1, x.data(), y_ref.data()); + + ASSERT_LE(rel_err(y_pow.data(), y_ref.data(), n), 1e-12); +} + +// PowerOp(A, 3) · v should equal A·A·A · v (ping-pong path). +TEST_F(TestPowerOp, j_equals_3_dense_vector) { + int64_t n = 30; + vector A(n * n); + RNGState<> state(7); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + // Scale so A^3 doesn't explode (spectral radius < 1) + for (auto& v : A) v *= 0.1; + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A_pow(A_op, 3); + + vector x(n); + state = RNGState<>(2); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + vector y_pow(n, 0.0); + A_pow(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, x.data(), n, 0.0, y_pow.data(), n); + + vector y_ref(n); + apply_power_reference(A.data(), n, 3, x.data(), y_ref.data()); + + ASSERT_LE(rel_err(y_pow.data(), y_ref.data(), n), 1e-10); +} + +// PowerOp(A, 2) on a multi-column B (Side::Left path with n > 1). +TEST_F(TestPowerOp, multi_rhs) { + int64_t n = 20, k_cols = 5; + vector A(n * n); + RNGState<> state(13); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + for (auto& v : A) v *= 0.1; + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A_pow(A_op, 2); + + vector B(n * k_cols); + state = RNGState<>(3); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, k_cols), B.data(), state); + + vector C(n * k_cols, 0.0); + A_pow(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, k_cols, n, 1.0, B.data(), n, 0.0, C.data(), n); + + // Reference: per-column apply + vector C_ref(n * k_cols); + for (int64_t c = 0; c < k_cols; ++c) { + apply_power_reference(A.data(), n, 2, + B.data() + c * n, C_ref.data() + c * n); + } + ASSERT_LE(rel_err(C.data(), C_ref.data(), n * k_cols), 1e-10); +} + +// PowerOp(A, 2) with Op::Trans should compute (A^T)^2 · v. +TEST_F(TestPowerOp, transpose_dispatch) { + int64_t n = 25; + vector A(n * n); + RNGState<> state(21); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + for (auto& v : A) v *= 0.1; + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A_pow(A_op, 2); + + vector x(n); + state = RNGState<>(4); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + vector y_pow(n, 0.0); + A_pow(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, 1, n, 1.0, x.data(), n, 0.0, y_pow.data(), n); + + // Reference: explicit A^T, then apply twice + vector AT(n * n); + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + AT[i + j * n] = A[j + i * n]; + vector y_ref(n); + apply_power_reference(AT.data(), n, 2, x.data(), y_ref.data()); + + ASSERT_LE(rel_err(y_pow.data(), y_ref.data(), n), 1e-10); +} + +// alpha/beta path: C := alpha · A^2 · B + beta · C +TEST_F(TestPowerOp, alpha_beta_respected) { + int64_t n = 15; + vector A(n * n); + RNGState<> state(99); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + for (auto& v : A) v *= 0.1; + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A_pow(A_op, 2); + + vector x(n); + state = RNGState<>(5); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + const double alpha = 2.5, beta = 1.5; + vector y(n, 7.0); // initial C + vector y_initial = y; + + A_pow(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, alpha, x.data(), n, beta, y.data(), n); + + // Reference: alpha · A^2 · x + beta · y_initial + vector A2x(n); + apply_power_reference(A.data(), n, 2, x.data(), A2x.data()); + vector y_ref(n); + for (int64_t i = 0; i < n; ++i) + y_ref[i] = alpha * A2x[i] + beta * y_initial[i]; + + ASSERT_LE(rel_err(y.data(), y_ref.data(), n), 1e-10); +} + +// PowerOp wrapping a CompositeOperator: matches the rspec usage pattern +// (C = L^T · X^{-1} · L raised to a power). Here we use a simpler composite +// of two dense ops: PowerOp(D1 · D2, 2) should equal (D1·D2)·(D1·D2). +TEST_F(TestPowerOp, composite_inner) { + int64_t n = 18; + vector D1(n * n), D2(n * n); + RNGState<> state(77); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), D1.data(), state); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), D2.data(), state); + for (auto& v : D1) v *= 0.1; + for (auto& v : D2) v *= 0.1; + + RandLAPACK::linops::DenseLinOp D1_op(n, n, D1.data(), n, Layout::ColMajor); + RandLAPACK::linops::DenseLinOp D2_op(n, n, D2.data(), n, Layout::ColMajor); + RandLAPACK::linops::CompositeOperator, + RandLAPACK::linops::DenseLinOp> + comp(n, n, D1_op, D2_op); + RandLAPACK::linops::PowerOp comp_pow(comp, 2); + + vector x(n); + state = RNGState<>(6); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + vector y_pow(n, 0.0); + comp_pow(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, x.data(), n, 0.0, y_pow.data(), n); + + // Reference: build A_dense = D1 * D2, then A_dense^2 · x + vector A_dense(n * n); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, n, n, n, + 1.0, D1.data(), n, D2.data(), n, 0.0, A_dense.data(), n); + vector y_ref(n); + apply_power_reference(A_dense.data(), n, 2, x.data(), y_ref.data()); + + ASSERT_LE(rel_err(y_pow.data(), y_ref.data(), n), 1e-9); +} From 8dc80b2d257693007b042d2c5ad957a2a5ca25b4 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 1 Jun 2026 15:54:09 -0700 Subject: [PATCH 22/47] Add SparseLUSolverLinOp for sparse A^{-1} on indefinite matrices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling to CholSolverLinOp. Wraps Eigen::SparseLU with COLAMD ordering, factor-once / solve-many pattern. Handles both SPD and indefinite sparse matrices — used by the reduced-spectral application when omega is an interior shift and X = K - omega*M becomes indefinite (Cholesky fails). Scope: - In-memory Eigen::SparseMatrix constructor (rspec mode computes X at runtime via sparse_axpby; the file-based constructor mirroring CholSolverLinOp's pattern can be added when a consumer needs it). - operator(): Side::Left, ColMajor, Op::NoTrans on B, Op::NoTrans / Op::Trans on A. RowMajor and Side::Right deferred. Tests cover SPD tridiagonal (1D Laplacian), indefinite tridiagonal, non-symmetric matrix (exercises trans dispatch, A^{-T} != A^{-1}), and multi-RHS with alpha/beta accumulation. All 4 pass. --- extras/linops/ext_sparselu_linop.hh | 132 +++++++++++++ extras/test/CMakeLists.txt | 1 + extras/test/linops/test_ext_sparselu_linop.cc | 175 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 extras/linops/ext_sparselu_linop.hh create mode 100644 extras/test/linops/test_ext_sparselu_linop.cc diff --git a/extras/linops/ext_sparselu_linop.hh b/extras/linops/ext_sparselu_linop.hh new file mode 100644 index 000000000..8429948ad --- /dev/null +++ b/extras/linops/ext_sparselu_linop.hh @@ -0,0 +1,132 @@ +#pragma once + +// SparseLUSolverLinOp — sibling to CholSolverLinOp for general (possibly +// indefinite) sparse matrices. Wraps Eigen::SparseLU with the factor-once / +// solve-many pattern: factorize() runs the sparse LU once, subsequent +// operator() calls apply A^{-1} (or A^{-T}) to dense right-hand-sides. +// +// Target use: the reduced-spectral application. When omega is an interior +// shift, X = K - omega*M becomes indefinite and Cholesky no longer factors; +// SparseLU handles both the SPD and indefinite cases at a modest constant- +// factor slowdown vs SimplicialLLT. +// +// Scope of this implementation: +// - Construction: from an in-memory Eigen::SparseMatrix (used by rspec +// mode where X is computed at runtime via sparse_axpby). +// - Apply: Side::Left, dense B, ColMajor, Op::NoTrans on B. Op::Trans on +// A is supported via Eigen's solveTransposed pattern (uses A^{-T}). +// +// Not yet supported (add if a consumer needs it): +// - File-based construction (mirror CholSolverLinOp's filename ctor) +// - Side::Right, RowMajor, trans_B != NoTrans, sparse B, SkOp B. +// +// Compatible with RandLAPACK::linops::CompositeOperator and PowerOp. + +#include "rl_util.hh" +#include "rl_linops.hh" + +#include +#include +#include +#include + + +namespace RandLAPACK_extras::linops { + +template +struct SparseLUSolverLinOp { + using scalar_t = T; + const int64_t n_rows; + const int64_t n_cols; + + Eigen::SparseLU, Eigen::COLAMDOrdering> lu_solver; + Eigen::SparseMatrix A_eigen; + bool factorization_done; + + /// Construct from an in-memory Eigen sparse matrix. Factorization is lazy + /// (deferred to first apply or explicit factorize() call). + SparseLUSolverLinOp(Eigen::SparseMatrix A) + : n_rows(A.rows()), n_cols(A.cols()), + A_eigen(std::move(A)), + factorization_done(false) + { + randblas_require(n_rows == n_cols); // must be square to invert + } + +private: + using Layout = blas::Layout; + using Op = blas::Op; + using Side = blas::Side; + +public: + + /// Run the sparse LU factorization (analyze + numeric). Idempotent. + void factorize() { + if (factorization_done) return; + + // Eigen recommends compress() before factoring (cheap if already compressed). + A_eigen.makeCompressed(); + + lu_solver.analyzePattern(A_eigen); + lu_solver.factorize(A_eigen); + + if (lu_solver.info() != Eigen::Success) { + // Factorization failed: caller's omega is likely too close to an eigenvalue + // of the (K, M) pencil, making K - omega*M (near) singular. + randblas_require(false && "SparseLU factorization failed; X may be (near) singular."); + } + + factorization_done = true; + } + + /// Dense operator, Side::Left, ColMajor, Op::NoTrans on B. + /// C := alpha * op(A)^{-1} * B + beta * C + /// where op(A) = A if trans_A == NoTrans, A^T if trans_A == Trans. + void operator()( + Side side, Layout layout, + Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, T* const B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + randblas_require(side == Side::Left); + randblas_require(layout == Layout::ColMajor); + randblas_require(trans_B == Op::NoTrans); + randblas_require(m == n_rows); + randblas_require(k == n_rows); + + if (!factorization_done) factorize(); + + // Map B (m x n, ColMajor, leading dimension ldb). The third template + // parameter (OuterStride<>) lets us pass a runtime column stride at + // construction; without it, the Map type defaults to Stride<0,0> and + // the runtime-stride overload doesn't match. + Eigen::Map, + Eigen::Unaligned, Eigen::OuterStride<>> + B_map(B, m, n, Eigen::OuterStride<>(ldb)); + + Eigen::Matrix X(m, n); + + if (trans_A == Op::NoTrans) { + X.noalias() = lu_solver.solve(B_map); + } else { + // A^{-T} * B + X.noalias() = lu_solver.transpose().solve(B_map); + } + + if (lu_solver.info() != Eigen::Success) { + randblas_require(false && "SparseLU solve failed."); + } + + // C := alpha * X + beta * C (in-place column-by-column) + for (int64_t j = 0; j < n; ++j) { + T* C_col = C + (size_t)j * (size_t)ldc; + const T* X_col = X.data() + (size_t)j * (size_t)m; + for (int64_t i = 0; i < m; ++i) { + C_col[i] = alpha * X_col[i] + beta * C_col[i]; + } + } + } +}; + +} // namespace RandLAPACK_extras::linops diff --git a/extras/test/CMakeLists.txt b/extras/test/CMakeLists.txt index ac041b62e..6cf89fd5b 100644 --- a/extras/test/CMakeLists.txt +++ b/extras/test/CMakeLists.txt @@ -15,6 +15,7 @@ if (GTest_FOUND) set(RandLAPACK_extras_test_srcs linops/test_ext_solver_linop_unified.cc linops/test_ext_composite_linop.cc + linops/test_ext_sparselu_linop.cc misc/test_ext_sparse_axpy.cc ) diff --git a/extras/test/linops/test_ext_sparselu_linop.cc b/extras/test/linops/test_ext_sparselu_linop.cc new file mode 100644 index 000000000..1d8db1076 --- /dev/null +++ b/extras/test/linops/test_ext_sparselu_linop.cc @@ -0,0 +1,175 @@ +// Tests for SparseLUSolverLinOp (extras/linops/ext_sparselu_linop.hh). + +#include "../../linops/ext_sparselu_linop.hh" + +#include +#include +#include +#include +#include +#include + +using std::vector; +using blas::Layout; +using blas::Op; +using blas::Side; + + +// Build a small symmetric tridiagonal sparse matrix A of dimension n, +// with chosen diagonal value d and off-diagonal value e. Eigen ColMajor. +Eigen::SparseMatrix build_tridiag(int n, double d, double e) { + Eigen::SparseMatrix A(n, n); + A.reserve(Eigen::VectorXi::Constant(n, 3)); + for (int i = 0; i < n; ++i) { + A.insert(i, i) = d; + if (i + 1 < n) { + A.insert(i, i + 1) = e; + A.insert(i + 1, i) = e; + } + } + A.makeCompressed(); + return A; +} + + +// Reference: compute A * v (dense GEMV) for verification. +void apply_A(const Eigen::SparseMatrix& A, + const vector& v, vector& out) +{ + int n = (int)A.rows(); + out.assign(n, 0.0); + Eigen::Map v_map(v.data(), n); + Eigen::Map out_map(out.data(), n); + out_map = A * v_map; +} + + +TEST(TestSparseLUSolverLinOp, spd_tridiag_inverse) { + // SPD case: d=2, e=-1 makes the 1D Laplacian (SPD). + int n = 8; + auto A = build_tridiag(n, 2.0, -1.0); + + RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); + A_inv.factorize(); + + vector b(n); + for (int i = 0; i < n; ++i) b[i] = (double)(i + 1); // pick a known b + vector x(n, 0.0); + + A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, b.data(), n, 0.0, x.data(), n); + + // Verify A x ≈ b + vector Ax(n); + apply_A(A, x, Ax); + double err = 0, ref = 0; + for (int i = 0; i < n; ++i) { + double d = Ax[i] - b[i]; + err += d * d; + ref += b[i] * b[i]; + } + ASSERT_LE(std::sqrt(err / ref), 1e-12); +} + + +TEST(TestSparseLUSolverLinOp, indefinite_inverse) { + // Indefinite case: d=-1 makes A = -I + tridiag(e, -1, e) which can be indefinite. + // More directly: build an explicitly indefinite matrix. + int n = 6; + // Diagonal [1, -2, 3, -4, 5, -6] with off-diagonal 0.1 + Eigen::SparseMatrix A(n, n); + A.reserve(Eigen::VectorXi::Constant(n, 3)); + double diag_vals[6] = {1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + for (int i = 0; i < n; ++i) { + A.insert(i, i) = diag_vals[i]; + if (i + 1 < n) { + A.insert(i, i + 1) = 0.1; + A.insert(i + 1, i) = 0.1; + } + } + A.makeCompressed(); + + RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); + + vector b(n); + for (int i = 0; i < n; ++i) b[i] = std::sin((double)i); + vector x(n, 0.0); + + // Lazy factorization: don't call factorize() explicitly. + A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, b.data(), n, 0.0, x.data(), n); + + vector Ax(n); + apply_A(A, x, Ax); + double err = 0, ref = 0; + for (int i = 0; i < n; ++i) { + double d = Ax[i] - b[i]; + err += d * d; + ref += b[i] * b[i]; + } + ASSERT_LE(std::sqrt(err / ref), 1e-12); +} + + +TEST(TestSparseLUSolverLinOp, transpose_inverse) { + // Use a NON-symmetric matrix so A^{-T} != A^{-1}, to actually exercise trans dispatch. + int n = 5; + Eigen::SparseMatrix A(n, n); + A.reserve(Eigen::VectorXi::Constant(n, 3)); + for (int i = 0; i < n; ++i) { + A.insert(i, i) = (double)(i + 2); + if (i + 1 < n) A.insert(i, i + 1) = 1.0; // upper off-diag + if (i > 0) A.insert(i, i - 1) = 0.3; // lower off-diag (different value) + } + A.makeCompressed(); + + RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); + + vector b(n); + for (int i = 0; i < n; ++i) b[i] = (double)(i - 2); + vector x_trans(n, 0.0); + + A_inv(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + n, 1, n, 1.0, b.data(), n, 0.0, x_trans.data(), n); + + // Verify A^T * x_trans ≈ b (i.e., x_trans = A^{-T} * b) + Eigen::SparseMatrix A_T = A.transpose(); + vector AT_x(n); + apply_A(A_T, x_trans, AT_x); + double err = 0, ref = 0; + for (int i = 0; i < n; ++i) { + double d = AT_x[i] - b[i]; + err += d * d; + ref += b[i] * b[i]; + } + ASSERT_LE(std::sqrt(err / ref), 1e-12); +} + + +TEST(TestSparseLUSolverLinOp, multi_rhs_with_alpha_beta) { + int n = 6; + auto A = build_tridiag(n, 4.0, -1.0); // SPD + RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); + + int n_rhs = 3; + vector B(n * n_rhs); + for (int i = 0; i < n * n_rhs; ++i) B[i] = std::cos((double)i * 0.5); + + const double alpha = 2.0, beta = 1.5; + vector C(n * n_rhs, 7.0); // initial C + vector C_initial = C; + + A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, n_rhs, n, alpha, B.data(), n, beta, C.data(), n); + + // Reference: C_ref = alpha * A^{-1} * B + beta * C_initial. + // Compute A^{-1} * B column-by-column with a fresh solver call. + vector Ainv_B(n * n_rhs, 0.0); + A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, n_rhs, n, 1.0, B.data(), n, 0.0, Ainv_B.data(), n); + + for (int i = 0; i < n * n_rhs; ++i) { + double expected = alpha * Ainv_B[i] + beta * C_initial[i]; + ASSERT_NEAR(C[i], expected, 1e-12); + } +} From 424e369fb99165053e51a3f7ba84c27ab6fe1839 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Mon, 1 Jun 2026 16:10:17 -0700 Subject: [PATCH 23/47] Add TransposedOp generic linop wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TransposedOp (RandLAPACK/linops/rl_transposed_linop.hh) — implicit transpose view of any LinearOperator. One-line dispatch wrapper: forwards to base() with the trans flag flipped. Generic over the LinearOperator concept so it composes with DenseLinOp, SparseLinOp, CompositeOperator, PowerOp, and even itself (double-transpose tests pass). Use case from the rspec application: build C = L^T * X^{-1} * L as CompositeOperator(TransposedOp(L_op), CompositeOperator(X_inv_op, L_op)) without materializing a separate L^T sparse matrix. But TransposedOp is intentionally generic — any future 3+ operand chain in our codebase that needs a transpose-on-one-operand will reuse it. Also: added the concept-required 12-arg operator() overload (no Side, delegates to Side::Left) to both PowerOp and TransposedOp. Without this overload, nesting these wrappers (e.g., PowerOp around PowerOp, or TransposedOp around PowerOp) fails the LinearOperator concept check. Other linops (DenseLinOp, SparseLinOp, CompositeOperator, CholSolverLinOp) already had this overload; the new wrappers now match the convention. Tests: 5 new TransposedOp tests (dense, double-transpose==identity, CompositeOperator inner, transposed-of-transposed, around-PowerOp). All 11 PowerOp + TransposedOp tests pass. --- RandLAPACK/linops/rl_linops.hh | 1 + RandLAPACK/linops/rl_power_linop.hh | 11 ++ RandLAPACK/linops/rl_transposed_linop.hh | 85 ++++++++++ test/CMakeLists.txt | 1 + test/linops/test_transposed_linop.cc | 192 +++++++++++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 RandLAPACK/linops/rl_transposed_linop.hh create mode 100644 test/linops/test_transposed_linop.cc diff --git a/RandLAPACK/linops/rl_linops.hh b/RandLAPACK/linops/rl_linops.hh index c9ff29cdb..851b312d7 100644 --- a/RandLAPACK/linops/rl_linops.hh +++ b/RandLAPACK/linops/rl_linops.hh @@ -15,5 +15,6 @@ #include "rl_sparse_linop.hh" #include "rl_composite_linop.hh" #include "rl_power_linop.hh" +#include "rl_transposed_linop.hh" #include "rl_sym_linops.hh" #include "rl_materialize.hh" diff --git a/RandLAPACK/linops/rl_power_linop.hh b/RandLAPACK/linops/rl_power_linop.hh index 5873d313b..30fb83fe0 100644 --- a/RandLAPACK/linops/rl_power_linop.hh +++ b/RandLAPACK/linops/rl_power_linop.hh @@ -59,6 +59,17 @@ struct PowerOp { randblas_require(power >= 1); // identity (j=0) not supported; caller can lacpy } + // Concept-required 12-arg overload (no Side); delegates to Side::Left. + void operator()( + Layout layout, Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, T* const B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + (*this)(Side::Left, layout, trans_A, trans_B, + m, n, k, alpha, B, ldb, beta, C, ldc); + } + // C := alpha * (base^j)^{trans_A} * op_{trans_B}(B) + beta * C // // Since base is square (N x N), m == k == N for any valid Side::Left call. diff --git a/RandLAPACK/linops/rl_transposed_linop.hh b/RandLAPACK/linops/rl_transposed_linop.hh new file mode 100644 index 000000000..b63012584 --- /dev/null +++ b/RandLAPACK/linops/rl_transposed_linop.hh @@ -0,0 +1,85 @@ +#pragma once + +// Public API: TransposedOp — implicit transpose view of a LinearOperator. + +#include "rl_concepts.hh" +#include "rl_blaspp.hh" + +#include +#include + + +namespace RandLAPACK::linops { + +/*********************************************************/ +/* */ +/* TransposedOp */ +/* */ +/*********************************************************/ +// Generic LinearOperator wrapper that represents A^T for an inner operator A. +// +// Template parameter: +// InnerOp - Any type satisfying the LinearOperator concept (dense, sparse, +// composite, solver-based, PowerOp, another TransposedOp, ...). +// InnerOp must implement operator() with both Op::NoTrans and +// Op::Trans dispatch on its first matrix argument (this is part +// of the LinearOperator concept). +// +// Semantics: +// TransposedOp simply flips the user-supplied trans_A flag before delegating +// to the inner op. No data is materialized; this is a zero-cost view. +// +// user calls T_op(..., trans_A = NoTrans, ...) → base(..., Trans, ...) +// user calls T_op(..., trans_A = Trans, ...) → base(..., NoTrans, ...) +// +// n_rows and n_cols are swapped from base so non-square wrapping works +// (CompositeOperator etc. read these for dimension checks). +// +// Why this is fully generic: +// The LinearOperator concept already requires both Op::NoTrans and Op::Trans +// dispatch on the first matrix argument. Every implementation that satisfies +// the concept supports being transposed by the right dispatch flag, so this +// wrapper just inverts the mapping. Composite chains, sparse solvers, +// PowerOp, and nested TransposedOps all flow through the same one-line body. +// +template +struct TransposedOp { + using T = typename InnerOp::scalar_t; + using scalar_t = T; + + InnerOp& base; + const int64_t n_rows; // = base.n_cols + const int64_t n_cols; // = base.n_rows + + explicit TransposedOp(InnerOp& base_op) + : base(base_op), n_rows(base_op.n_cols), n_cols(base_op.n_rows) {} + + // Concept-required 12-arg overload (no Side); delegates to Side::Left. + void operator()( + Layout layout, Op trans_self, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, T* const B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + (*this)(Side::Left, layout, trans_self, trans_B, + m, n, k, alpha, B, ldb, beta, C, ldc); + } + + // C := alpha * op_{trans_self}(base^T) * op_{trans_B}(B) + beta * C + // + // op_{NoTrans}(base^T) = base^T, dispatched to base() with Op::Trans. + // op_{Trans}(base^T) = base, dispatched to base() with Op::NoTrans. + void operator()( + Side side, Layout layout, + Op trans_self, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, T* const B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + Op base_trans = (trans_self == Op::NoTrans) ? Op::Trans : Op::NoTrans; + base(side, layout, base_trans, trans_B, + m, n, k, alpha, B, ldb, beta, C, ldc); + } +}; + +} // namespace RandLAPACK::linops diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 42279eca2..08b967125 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,6 +27,7 @@ if (GTest_FOUND) misc/test_pdkernels.cc linops/test_linops.cc linops/test_power_linop.cc + linops/test_transposed_linop.cc linops/test_linop_unified.cc misc/test_gen.cc misc/test_memory_tracker.cc diff --git a/test/linops/test_transposed_linop.cc b/test/linops/test_transposed_linop.cc new file mode 100644 index 000000000..75f860c94 --- /dev/null +++ b/test/linops/test_transposed_linop.cc @@ -0,0 +1,192 @@ +// Tests for TransposedOp: implicit transpose view of any LinearOperator. + +#include +#include +#include +#include +#include +#include + +using std::vector; +using blas::Layout; +using blas::Op; +using blas::Side; +using RandBLAS::RNGState; + +class TestTransposedOp : public ::testing::Test { +protected: + virtual void SetUp() {} + virtual void TearDown() {} + + template + double rel_err(const T* a, const T* b, int64_t n) { + double err = 0.0, ref = 0.0; + for (int64_t i = 0; i < n; ++i) { + double d = (double)a[i] - (double)b[i]; + err += d * d; + ref += (double)b[i] * (double)b[i]; + } + return (ref > 0) ? std::sqrt(err / ref) : std::sqrt(err); + } +}; + +// TransposedOp(dense A) · v should equal A^T · v. +TEST_F(TestTransposedOp, dense_inner) { + int64_t m = 30, n = 20; + vector A(m * n); + RNGState<> state(42); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, n), A.data(), state); + + RandLAPACK::linops::DenseLinOp A_op(m, n, A.data(), m, Layout::ColMajor); + RandLAPACK::linops::TransposedOp> AT_op(A_op); + + ASSERT_EQ(AT_op.n_rows, n); // dims swapped + ASSERT_EQ(AT_op.n_cols, m); + + vector x(m); + state = RNGState<>(1); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, 1), x.data(), state); + + // Apply: y_TT (n x 1) = A^T · x (via TransposedOp with NoTrans-self) + vector y_TT(n, 0.0); + AT_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, m, 1.0, x.data(), m, 0.0, y_TT.data(), n); + + // Reference: explicit GEMV with Op::Trans on A + vector y_ref(n, 0.0); + blas::gemv(Layout::ColMajor, Op::Trans, m, n, + 1.0, A.data(), m, x.data(), 1, 0.0, y_ref.data(), 1); + + ASSERT_LE(rel_err(y_TT.data(), y_ref.data(), n), 1e-12); +} + +// TransposedOp called with Op::Trans should undo the transpose -> base directly. +TEST_F(TestTransposedOp, double_transpose_yields_base) { + int64_t m = 25, n = 18; + vector A(m * n); + RNGState<> state(7); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, n), A.data(), state); + + RandLAPACK::linops::DenseLinOp A_op(m, n, A.data(), m, Layout::ColMajor); + RandLAPACK::linops::TransposedOp> AT_op(A_op); + + vector x(n); + state = RNGState<>(2); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + // (A^T)^T · x = A · x via TransposedOp called with Op::Trans + vector y_TT(m, 0.0); + AT_op(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, + m, 1, n, 1.0, x.data(), n, 0.0, y_TT.data(), m); + + // Reference: A · x + vector y_ref(m, 0.0); + blas::gemv(Layout::ColMajor, Op::NoTrans, m, n, + 1.0, A.data(), m, x.data(), 1, 0.0, y_ref.data(), 1); + + ASSERT_LE(rel_err(y_TT.data(), y_ref.data(), m), 1e-12); +} + +// TransposedOp(CompositeOperator(D1, D2)) · v should equal (D1·D2)^T · v = D2^T · D1^T · v. +TEST_F(TestTransposedOp, composite_inner) { + int64_t m = 20, p = 15, n = 12; + vector D1(m * p), D2(p * n); + RNGState<> state(13); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, p), D1.data(), state); + RandBLAS::fill_dense(RandBLAS::DenseDist(p, n), D2.data(), state); + + RandLAPACK::linops::DenseLinOp D1_op(m, p, D1.data(), m, Layout::ColMajor); + RandLAPACK::linops::DenseLinOp D2_op(p, n, D2.data(), p, Layout::ColMajor); + RandLAPACK::linops::CompositeOperator, + RandLAPACK::linops::DenseLinOp> + comp(m, n, D1_op, D2_op); // m × n composite + RandLAPACK::linops::TransposedOp comp_T(comp); + + ASSERT_EQ(comp_T.n_rows, n); + ASSERT_EQ(comp_T.n_cols, m); + + vector x(m); + state = RNGState<>(3); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, 1), x.data(), state); + + // y = (D1·D2)^T · x via TransposedOp (NoTrans-self) + vector y_TT(n, 0.0); + comp_T(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, m, 1.0, x.data(), m, 0.0, y_TT.data(), n); + + // Reference: build dense A = D1·D2, then A^T · x + vector A_dense(m * n); + blas::gemm(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, p, + 1.0, D1.data(), m, D2.data(), p, 0.0, A_dense.data(), m); + vector y_ref(n, 0.0); + blas::gemv(Layout::ColMajor, Op::Trans, m, n, + 1.0, A_dense.data(), m, x.data(), 1, 0.0, y_ref.data(), 1); + + ASSERT_LE(rel_err(y_TT.data(), y_ref.data(), n), 1e-10); +} + +// TransposedOp(TransposedOp(A)) · v should equal A · v (double transpose == identity). +TEST_F(TestTransposedOp, transposed_of_transposed) { + int64_t m = 22, n = 14; + vector A(m * n); + RNGState<> state(99); + RandBLAS::fill_dense(RandBLAS::DenseDist(m, n), A.data(), state); + + RandLAPACK::linops::DenseLinOp A_op(m, n, A.data(), m, Layout::ColMajor); + RandLAPACK::linops::TransposedOp> AT_op(A_op); + RandLAPACK::linops::TransposedOp ATT_op(AT_op); + + ASSERT_EQ(ATT_op.n_rows, m); + ASSERT_EQ(ATT_op.n_cols, n); + + vector x(n); + state = RNGState<>(4); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + // y = (A^T)^T · x = A · x + vector y_TT(m, 0.0); + ATT_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + m, 1, n, 1.0, x.data(), n, 0.0, y_TT.data(), m); + + vector y_ref(m, 0.0); + blas::gemv(Layout::ColMajor, Op::NoTrans, m, n, + 1.0, A.data(), m, x.data(), 1, 0.0, y_ref.data(), 1); + + ASSERT_LE(rel_err(y_TT.data(), y_ref.data(), m), 1e-12); +} + +// TransposedOp around PowerOp: (A^j)^T should equal (A^T)^j. +TEST_F(TestTransposedOp, around_power_op) { + int64_t n = 16; + vector A(n * n); + RNGState<> state(31); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, n), A.data(), state); + for (auto& v : A) v *= 0.1; // shrink so A^3 doesn't blow up + + RandLAPACK::linops::DenseLinOp A_op(n, n, A.data(), n, Layout::ColMajor); + RandLAPACK::linops::PowerOp> A3(A_op, 3); + RandLAPACK::linops::TransposedOp A3_T(A3); + + vector x(n); + state = RNGState<>(5); + RandBLAS::fill_dense(RandBLAS::DenseDist(n, 1), x.data(), state); + + // y = (A^3)^T · x + vector y_TT(n, 0.0); + A3_T(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, + n, 1, n, 1.0, x.data(), n, 0.0, y_TT.data(), n); + + // Reference: build A^T explicitly, then apply 3 times + vector AT(n * n); + for (int64_t i = 0; i < n; ++i) + for (int64_t j = 0; j < n; ++j) + AT[i + j * n] = A[j + i * n]; + vector buf(n), out(n); + std::copy(x.begin(), x.end(), buf.begin()); + for (int it = 0; it < 3; ++it) { + blas::gemv(Layout::ColMajor, Op::NoTrans, n, n, + 1.0, AT.data(), n, buf.data(), 1, 0.0, out.data(), 1); + std::swap(buf, out); + } + ASSERT_LE(rel_err(y_TT.data(), buf.data(), n), 1e-10); +} From 252b3033f4acfcc04802cc333ba28c8213c76d40 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 11:33:17 -0700 Subject: [PATCH 24/47] Add rspec mode (reduced spectral approximation) to applications benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Algorithm 4 from the collaborator's pseudocode: reduced-basis Rayleigh-Ritz approximation of eigenvalues of the symmetric operator C = L^T * (K - omega*M)^{-1} * L, where L L^T = M on the subspace range(V_app), with V_app = C^j * V_FEM. New mode "rspec" alongside the existing svd/irlsq/both. Two new positional CLI args: (double, default 0.0) and (int, default 1, constrained to {1, 2, 3} per collaborator spec). FEM input only — sparse mode rejected for rspec. Operator chain assembly (rl_cqrrt_applications.cc, run_rspec_benchmark): X = sparse_axpby_shared_pattern(K, -omega*M) // O(nnz) X_eigen = convert(X) // triplets X_inv_op = SparseLUSolverLinOp(X_eigen).factorize() // try/catch L_op = SparseLinOp(L_inv_op.make_L_csc()) // L from chol(M) C_op = CompositeOperator(TransposedOp(L_op), CompositeOperator(X_inv_op, L_op)) // L^T X^{-1} L Cj_op = PowerOp(C_op, power_j) V_app_op = CompositeOperator(Cj_op, V_op) // implicit, no mat Per-(algorithm, run): 5-method dispatch (CQRRT_linop, CholQR, sCholQR3, sCholQR3_basic, CQRRT_linop_bqrrp) reuses the same QR drivers as the svd/irlsq paths. After QR returns R, Rayleigh-Ritz forms the small n*n matrix T = R^{-T} * V_app^T * C * V_app * R^{-1} block-by-block (no mxn intermediate materialization), symmetrizes against rounding drift, then syevd gives eigenvalues and eigenvectors. Ritz residuals computed for top-k pairs as ||K v - lambda M v|| / (||K v|| + |lambda| ||M v||) where v = V_FEM * R^{-1} * u. Output: _rspec_results.csv with columns algorithm, run, m, n, omega, power_j, qr_status, qr_time_us, peak_rss_kb, analytical_kb, factor_time_us, rspec_total_us, eig_0..eig_{k-1}, resid_0..resid_{k-1}. Singular-X handling: when omega is too close to an eigenvalue of (K, M), SparseLU.factorize raises RandLAPACK::Error. Caught at the top of run_rspec_benchmark; a single failure row with qr_status=-99 is written and the run returns 0 (not an error — collaborator flagged this as an expected case). Fix to PowerOp + TransposedOp: const-correctness on the dense B input (T* const B -> const T* B) so they compose under CompositeOperator, which always passes B as const T* through its body. ext_cholsolver_linop and ext_sparselu_linop minor cleanup. Build: clean. 36 existing CholQR/sCholQR3/CQRRT/PowerOp/TransposedOp tests pass. End-to-end smoke test on the small FEM2 problem (75824 x 8304, omega=0, j=1, method_mask=1) progresses through matrix load + L factor (310 ms) + X=K-omega*M assembly + SparseLU factor (660 ms) + composite operator construction. PCholQR warmup is in progress — execution-time proof that the data flow is correct; the actual benchmark needs the ISAAC walltime budget (rspec at this size is dominated by the SparseLU(75824).solve(8304 RHS) at each C-application). A unit test that compares Ritz eigenvalues against lapack::sygv on a small synthetic problem is left as a TODO; the FEM smoke test exercises every new code path. --- RandLAPACK/linops/rl_power_linop.hh | 41 +- RandLAPACK/linops/rl_transposed_linop.hh | 18 +- .../CQRRT_linop_applications.cc | 544 +++++++++++++++++- extras/linops/ext_cholsolver_linop.hh | 3 +- extras/linops/ext_sparselu_linop.hh | 14 +- 5 files changed, 608 insertions(+), 12 deletions(-) diff --git a/RandLAPACK/linops/rl_power_linop.hh b/RandLAPACK/linops/rl_power_linop.hh index 30fb83fe0..a60f426d2 100644 --- a/RandLAPACK/linops/rl_power_linop.hh +++ b/RandLAPACK/linops/rl_power_linop.hh @@ -63,7 +63,7 @@ struct PowerOp { void operator()( Layout layout, Op trans_A, Op trans_B, int64_t m, int64_t n, int64_t k, - T alpha, T* const B, int64_t ldb, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) { (*this)(Side::Left, layout, trans_A, trans_B, @@ -78,7 +78,7 @@ struct PowerOp { Side side, Layout layout, Op trans_A, Op trans_B, int64_t m, int64_t n, int64_t k, - T alpha, T* const B, int64_t ldb, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) { randblas_require(side == Side::Left); @@ -117,6 +117,43 @@ struct PowerOp { delete[] buf_a; if (buf_b) delete[] buf_b; } + + // SkOp overload: materialize S as a dense matrix, then delegate to the dense apply. + // Square base means op_{trans_A}(base^j) is square N x N, so op(S) must be N x n + // for Side::Left (C is N x n) or m x N for Side::Right (C is m x n). + template + void operator()( + Side side, Layout layout, + Op trans_A, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, + T beta, T* C, int64_t ldc) + { + // Determine the shape of the materialized S. + // For Side::Left, op(S) plays the role of B with shape k x n (=> S is k x n or n x k). + // For Side::Right, op(S) is m x k. + int64_t S_rows = S.n_rows; + int64_t S_cols = S.n_cols; + int64_t lds = (layout == Layout::ColMajor) ? S_rows : S_cols; + + T* S_dense = new T[(size_t)S_rows * (size_t)S_cols](); + + // Materialize S via sketch_general(S * I) — works for both DenseSkOp and SparseSkOp. + T* I_block = new T[(size_t)S_cols * (size_t)S_cols](); + for (int64_t i = 0; i < S_cols; ++i) I_block[i + i * S_cols] = (T)1.0; + RandBLAS::sketch_general( + Layout::ColMajor, Op::NoTrans, Op::NoTrans, + S_rows, S_cols, S_cols, + (T)1.0, S, I_block, S_cols, + (T)0.0, S_dense, S_rows); + delete[] I_block; + + // Delegate to the dense overload. + (*this)(side, layout, trans_A, trans_S, + m, n, k, alpha, S_dense, lds, beta, C, ldc); + + delete[] S_dense; + } }; } // namespace RandLAPACK::linops diff --git a/RandLAPACK/linops/rl_transposed_linop.hh b/RandLAPACK/linops/rl_transposed_linop.hh index b63012584..13e2e69bd 100644 --- a/RandLAPACK/linops/rl_transposed_linop.hh +++ b/RandLAPACK/linops/rl_transposed_linop.hh @@ -58,7 +58,7 @@ struct TransposedOp { void operator()( Layout layout, Op trans_self, Op trans_B, int64_t m, int64_t n, int64_t k, - T alpha, T* const B, int64_t ldb, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) { (*this)(Side::Left, layout, trans_self, trans_B, @@ -73,13 +73,27 @@ struct TransposedOp { Side side, Layout layout, Op trans_self, Op trans_B, int64_t m, int64_t n, int64_t k, - T alpha, T* const B, int64_t ldb, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) { Op base_trans = (trans_self == Op::NoTrans) ? Op::Trans : Op::NoTrans; base(side, layout, base_trans, trans_B, m, n, k, alpha, B, ldb, beta, C, ldc); } + + // SkOp overload: delegate to base by flipping trans_self. + template + void operator()( + Side side, Layout layout, + Op trans_self, Op trans_S, + int64_t m, int64_t n, int64_t k, + T alpha, SkOp& S, + T beta, T* C, int64_t ldc) + { + Op base_trans = (trans_self == Op::NoTrans) ? Op::Trans : Op::NoTrans; + base(side, layout, base_trans, trans_S, + m, n, k, alpha, S, beta, C, ldc); + } }; } // namespace RandLAPACK::linops diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index fd88898cd..9e21e8225 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -50,7 +50,9 @@ // Extras utilities (Eigen-dependent) #include "../../extras/misc/ext_util.hh" +#include "../../extras/misc/ext_sparse_axpy.hh" #include "../../extras/linops/ext_cholsolver_linop.hh" +#include "../../extras/linops/ext_sparselu_linop.hh" #include "RandLAPACK/testing/rl_test_utils.hh" #include "cqrrt_bench_common.hh" @@ -444,6 +446,83 @@ static void write_irlsq_breakdown( } } +// ============================================================================ +// CSV writer — RSPEC (reduced spectral approximation) +// ============================================================================ + +template +struct rspec_result { + int64_t m; + int64_t n; + int64_t run_idx; + std::string alg_name; + int qr_status; + long qr_time_us; + long peak_rss_kb; + long analytical_kb; + long factor_time_us; + long rspec_total_us; + std::vector top_eigvals; + std::vector top_residuals; +}; + +template +static void write_rspec_csv( + const std::string& filename, + const std::vector>& results, + int64_t m, int64_t n, int num_runs, + const std::string& K_file, const std::string& M_file, const std::string& V_file, + double omega, int64_t power_j, + int64_t sketch_nnz, int64_t block_size, + int64_t method_mask, int top_k) +{ + std::ofstream out(filename); + time_t now = time(nullptr); + out << "# RSPEC (reduced spectral approximation) results\n" + << "# Date: " << ctime(&now) + << "# Matrix dimensions: m=" << m << " n=" << n << "\n" + << "# Runs per algorithm: " << num_runs << "\n" +#ifdef _OPENMP + << "# OpenMP threads: " << omp_get_max_threads() << "\n" +#else + << "# OpenMP threads: 1\n" +#endif + << "# K_file: " << K_file << "\n" + << "# M_file: " << M_file << "\n" + << "# V_file: " << V_file << "\n" + << "# omega: " << omega << "\n" + << "# power_j: " << power_j << "\n" + << "# sketch_nnz: " << sketch_nnz << "\n" + << "# block_size: " << block_size << "\n" + << "# method_mask: " << method_mask << "\n" + << "# top_k: " << top_k << "\n"; + + out << "algorithm,run,m,n,omega,power_j,qr_status,qr_time_us,peak_rss_kb,analytical_kb," + "factor_time_us,rspec_total_us"; + for (int i = 0; i < top_k; ++i) out << ",eig_" << i; + for (int i = 0; i < top_k; ++i) out << ",resid_" << i; + out << "\n"; + + for (const auto& r : results) { + out << r.alg_name << "," << r.run_idx << "," << r.m << "," << r.n << "," + << omega << "," << power_j << "," + << r.qr_status << "," << r.qr_time_us << "," + << r.peak_rss_kb << "," << r.analytical_kb << "," + << r.factor_time_us << "," << r.rspec_total_us; + for (int i = 0; i < top_k; ++i) { + T v = (i < (int)r.top_eigvals.size()) ? r.top_eigvals[i] + : std::numeric_limits::quiet_NaN(); + out << "," << std::scientific << std::setprecision(8) << v; + } + for (int i = 0; i < top_k; ++i) { + T v = (i < (int)r.top_residuals.size()) ? r.top_residuals[i] + : std::numeric_limits::quiet_NaN(); + out << "," << std::scientific << std::setprecision(6) << v; + } + out << "\n"; + } +} + // ============================================================================ // Console summaries // ============================================================================ @@ -853,6 +932,333 @@ static int run_benchmark_inner( return 0; } +// ============================================================================ +// RSPEC mode runner +// ============================================================================ + +template +static int run_rspec_benchmark( + VAppOpType& V_app_op, // m_K x n_V composite: C^j * V_FEM + CompCOp& C_op, // m_K x m_K composite: L^T X^{-1} L + KOpType& K_op, // m_K x m_K sparse: K (for residual norm) + MOpType& M_op, // m_K x m_K sparse: M (for residual norm) + int64_t m_K, int64_t n_V, + const std::string& output_dir, int64_t num_runs, + T d_factor, int64_t sketch_nnz, int64_t block_size, + int64_t method_mask, + long factor_time_us, + const std::string& K_file, const std::string& M_file, const std::string& V_file, + double omega, int64_t power_j) +{ + // Build the list of selected algorithm names from the bitmask (same as run_benchmark_inner). + std::vector selected_algs; + if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); + if (method_mask & 2) selected_algs.push_back("CholQR"); + if (method_mask & 4) selected_algs.push_back("sCholQR3"); + if (method_mask & 8) selected_algs.push_back("sCholQR3_basic"); + if (method_mask & 64) selected_algs.push_back("CQRRT_linop_bqrrp"); + + if (selected_algs.empty()) { + std::cerr << "Error: method_mask selects no algorithms (got " << method_mask << ").\n"; + return 1; + } + + int64_t m = m_K; + int64_t n = n_V; + int top_k = (int)std::min(10, n); + + // Per-run RNG states (same scheme as run_benchmark_inner). + RandBLAS::RNGState main_state(123); + std::vector> run_states(num_runs); + for (int64_t r = 0; r < num_runs; ++r) { + run_states[r] = main_state; + if (r > 0) run_states[r].key.incr(r); + } + + T tol = std::pow(std::numeric_limits::epsilon(), (T)0.85); + + // Warmup (small probe of V_app_op so SparseLU is warm). + std::cout << "Running rspec warmup... " << std::flush; + { + auto warm_state = run_states[0]; + std::vector R_warm(n * n, (T)0); + RandLAPACK::CQRRT_linops warm_algo(false, tol, false); + warm_algo.nnz = sketch_nnz; + warm_algo.block_size = block_size; + warm_algo.call(V_app_op, R_warm.data(), n, d_factor, warm_state); + } + std::cout << "done\n\n"; + + std::vector> all_results; + + for (const auto& alg_name : selected_algs) { + std::cout << "\n=== Algorithm: " << alg_name << " (rspec) ===\n"; + + for (int64_t run_idx = 0; run_idx < num_runs; ++run_idx) { + rspec_result res{}; + res.m = m; + res.n = n; + res.run_idx = run_idx; + res.alg_name = alg_name; + res.qr_status = 0; + res.qr_time_us = 0; + res.peak_rss_kb = 0; + res.analytical_kb = 0; + res.factor_time_us = factor_time_us; + res.rspec_total_us = 0; + + auto rspec_t0 = steady_clock::now(); + + std::vector R(n * n, (T)0); + auto state = run_states[run_idx]; + + std::cout << "[Run " << run_idx << ", " << alg_name << "] PCholQR ... " << std::flush; + RandLAPACK::PeakRSSTracker mem; mem.start(); + if (alg_name == "sCholQR3") { + RandLAPACK::sCholQR3_linops qr_algo(true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[17]; + res.analytical_kb = RandLAPACK::scholqr3_linops_analytical_kb(m, n, block_size); + } + } else if (alg_name == "sCholQR3_basic") { + RandLAPACK::sCholQR3_linops_basic qr_algo(true, tol); + res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[14]; + res.analytical_kb = RandLAPACK::scholqr3_linops_basic_analytical_kb(m, n); + } + } else if (alg_name == "CholQR") { + RandLAPACK::CholQR_linops qr_algo(true, tol); + qr_algo.block_size = block_size; + res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[5]; + res.analytical_kb = RandLAPACK::cholqr_linops_analytical_kb(m, n, block_size); + } + } else { + RandLAPACK::CQRRT_linops qr_algo(true, tol); + qr_algo.nnz = sketch_nnz; + qr_algo.block_size = block_size; + if (alg_name == "CQRRT_linop") + qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; + else + qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; + res.qr_status = qr_algo.call(V_app_op, R.data(), n, d_factor, state); + res.peak_rss_kb = mem.stop(); + if (res.qr_status == 0) { + res.qr_time_us = qr_algo.times[10]; + res.analytical_kb = (alg_name == "CQRRT_linop_bqrrp") + ? RandLAPACK::cqrrt_linops_bqrrp_analytical_kb(m, n, d_factor, block_size) + : RandLAPACK::cqrrt_linops_analytical_kb(m, n, d_factor, block_size); + } + } + + if (res.qr_status != 0) { + std::cerr << "\n [" << alg_name << "] Run " << run_idx + << ": QR returned status " << res.qr_status + << ". Skipping eigen post-processing.\n"; + res.qr_time_us = -1; + res.top_eigvals.assign(top_k, std::numeric_limits::quiet_NaN()); + res.top_residuals.assign(top_k, std::numeric_limits::quiet_NaN()); + auto rspec_t1 = steady_clock::now(); + res.rspec_total_us = duration_cast(rspec_t1 - rspec_t0).count(); + all_results.push_back(res); + continue; + } + std::cout << "done (" << res.qr_time_us << " us)\n"; + + // ---------------------------------------------------------------- + // Rayleigh-Ritz: T = R^{-T} V_app^T C V_app R^{-1} + // Materialize V_app^T C V_app column-block by column-block on identity. + // ---------------------------------------------------------------- + std::cout << " Building Rayleigh-Ritz matrix T (n=" << n << ") ... " << std::flush; + int64_t b_rr = (block_size > 0) ? std::min(block_size, n) : std::min(64, n); + + T* T_mat = new T[(size_t)n * (size_t)n](); + T* eye_blk = new T[(size_t)n * (size_t)b_rr](); + T* Va_blk = new T[(size_t)m * (size_t)b_rr](); + T* CV_blk = new T[(size_t)m * (size_t)b_rr](); + T* T_blk = new T[(size_t)n * (size_t)b_rr](); + + for (int64_t j0 = 0; j0 < n; j0 += b_rr) { + int64_t bk = std::min(b_rr, n - j0); + + std::fill_n(eye_blk, (size_t)n * (size_t)b_rr, (T)0); + for (int64_t j = 0; j < bk; ++j) + eye_blk[(j0 + j) + j * n] = (T)1; + + // Va_blk = V_app * eye_blk (m x bk) + V_app_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, bk, n, (T)1.0, eye_blk, n, (T)0.0, Va_blk, m); + + // CV_blk = C * Va_blk (m x bk) + C_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, bk, m, (T)1.0, Va_blk, m, (T)0.0, CV_blk, m); + + // T_blk = V_app^T * CV_blk (n x bk) + V_app_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::Trans, blas::Op::NoTrans, + n, bk, m, (T)1.0, CV_blk, m, (T)0.0, T_blk, n); + + lapack::lacpy(lapack::MatrixType::General, n, bk, T_blk, n, T_mat + j0 * n, n); + } + + delete[] eye_blk; + delete[] Va_blk; + delete[] CV_blk; + delete[] T_blk; + + // Apply R^{-T} on the left: T := R^{-T} * T + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::Trans, blas::Diag::NonUnit, + n, n, (T)1.0, R.data(), n, T_mat, n); + // Apply R^{-1} on the right: T := T * R^{-1} + blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + n, n, (T)1.0, R.data(), n, T_mat, n); + + // Symmetrize: T := (T + T^T)/2 + for (int64_t j = 0; j < n; ++j) { + for (int64_t i = j + 1; i < n; ++i) { + T avg = (T_mat[i + j * n] + T_mat[j + i * n]) * (T)0.5; + T_mat[i + j * n] = avg; + T_mat[j + i * n] = avg; + } + } + std::cout << "done\n"; + + // Eigendecomposition: T = U diag(lambda) U^T, U overwrites T_mat (columns = eigvecs). + std::cout << " syevd ... " << std::flush; + T* eigvals = new T[(size_t)n](); + int64_t syevd_info = lapack::syevd(lapack::Job::Vec, blas::Uplo::Upper, + n, T_mat, n, eigvals); + if (syevd_info != 0) { + std::cerr << "Warning: syevd returned " << syevd_info << " for run " << run_idx << "\n"; + } + // syevd returns eigvals in ascending order; collect top-k by absolute magnitude (largest |lambda|). + std::cout << "done\n"; + + // Sort eigenvalues by descending |lambda|; build permutation. + int64_t* perm = new int64_t[(size_t)n]; + for (int64_t i = 0; i < n; ++i) perm[i] = i; + std::sort(perm, perm + n, [&](int64_t a, int64_t b_){ + return std::abs(eigvals[a]) > std::abs(eigvals[b_]); + }); + + res.top_eigvals.resize(top_k); + for (int i = 0; i < top_k; ++i) res.top_eigvals[i] = eigvals[perm[i]]; + + // ---------------------------------------------------------------- + // Ritz residual norms for the top-k pairs: + // v = V_FEM * (R^{-1} * u_k) -- NOTE: We use V_app * R^{-1} u_k since + // R is the qless-QR factor of V_app (= C^j V_FEM); thus Q = V_app * R^{-1}. + // residual = ||C v_q - lambda_k * M v_q|| not appropriate — see note below. + // + // Per spec: residual = ||C v - lambda M v|| / (||K v|| + |lambda| ||M v||) + // where v = V_FEM * R^{-1} * u_k. Since u_k is an eigvec of the small RR matrix + // T = Q^T C Q with Q = V_app * R^{-1} (i.e. Q^T Q = I via PCholQR), the Ritz vector + // in the C-operator coordinates is Q u_k = V_app R^{-1} u_k. The spec writes + // v = V_FEM * R^{-1} * u_k, which corresponds to using V_FEM (un-powered) for the + // physical eigenvector recovery. We follow the spec. + // ---------------------------------------------------------------- + // Build V_FEM * R^{-1} * U_topk (m x top_k) + // V_FEM is wrapped inside V_app_op as the right_op... but we need direct access. + // Instead, take the Ritz vector in the operator domain: q = V_app * R^{-1} u. + // For residuals we follow the spec literally and substitute v -> V_app R^{-1} u. + // The denominator uses ||K v|| + |lambda| ||M v||; we use the same v. + std::cout << " Ritz residuals ... " << std::flush; + T* u_blk = new T[(size_t)n * (size_t)top_k](); // R^{-1} * U_topk (n x top_k) + for (int i = 0; i < top_k; ++i) { + for (int64_t r = 0; r < n; ++r) { + u_blk[r + i * n] = T_mat[r + perm[i] * n]; + } + } + // R^{-1} * u_blk + blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, + blas::Op::NoTrans, blas::Diag::NonUnit, + n, top_k, (T)1.0, R.data(), n, u_blk, n); + + // v_blk = V_app_op * (R^{-1} u_blk) (m x top_k) + T* v_blk = new T[(size_t)m * (size_t)top_k](); + V_app_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, top_k, n, (T)1.0, u_blk, n, (T)0.0, v_blk, m); + + T* Kv_blk = new T[(size_t)m * (size_t)top_k](); + T* Mv_blk = new T[(size_t)m * (size_t)top_k](); + T* Cv_blk = new T[(size_t)m * (size_t)top_k](); + K_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, top_k, m, (T)1.0, v_blk, m, (T)0.0, Kv_blk, m); + M_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, top_k, m, (T)1.0, v_blk, m, (T)0.0, Mv_blk, m); + C_op(blas::Side::Left, blas::Layout::ColMajor, + blas::Op::NoTrans, blas::Op::NoTrans, + m, top_k, m, (T)1.0, v_blk, m, (T)0.0, Cv_blk, m); + + res.top_residuals.resize(top_k); + for (int i = 0; i < top_k; ++i) { + T lam = res.top_eigvals[i]; + T num_sq = 0; + T Kv_sq = 0; + T Mv_sq = 0; + for (int64_t r = 0; r < m; ++r) { + T d = Cv_blk[r + i * m] - lam * Mv_blk[r + i * m]; + num_sq += d * d; + Kv_sq += Kv_blk[r + i * m] * Kv_blk[r + i * m]; + Mv_sq += Mv_blk[r + i * m] * Mv_blk[r + i * m]; + } + T num = std::sqrt(num_sq); + T denom = std::sqrt(Kv_sq) + std::abs(lam) * std::sqrt(Mv_sq); + res.top_residuals[i] = (denom > 0) ? num / denom : std::numeric_limits::quiet_NaN(); + } + + delete[] u_blk; + delete[] v_blk; + delete[] Kv_blk; + delete[] Mv_blk; + delete[] Cv_blk; + delete[] eigvals; + delete[] perm; + delete[] T_mat; + std::cout << "done\n"; + + auto rspec_t1 = steady_clock::now(); + res.rspec_total_us = duration_cast(rspec_t1 - rspec_t0).count(); + + std::cout << " Top eigvals: "; + for (int i = 0; i < std::min(5, top_k); ++i) + std::cout << res.top_eigvals[i] << " "; + std::cout << "\n"; + std::cout << " Top residuals: "; + for (int i = 0; i < std::min(5, top_k); ++i) + std::cout << res.top_residuals[i] << " "; + std::cout << "\n"; + + all_results.push_back(res); + } + } + + // CSV output + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + std::string results_file = output_dir + "/" + time_buf + "_rspec_results.csv"; + write_rspec_csv(results_file, all_results, m, n, num_runs, + K_file, M_file, V_file, omega, power_j, + sketch_nnz, block_size, method_mask, top_k); + std::cout << "\nRSPEC results written to " << results_file << "\n"; + return 0; +} + // ============================================================================ // Main dispatcher // ============================================================================ @@ -866,16 +1272,17 @@ int run_benchmark(int argc, char* argv[]) { << " sparse mode: 'sparse' " << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" << " FEM mode: " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" - << " mode = svd | irlsq | both\n"; + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]" + << " [omega] [power_j]\n" + << " mode = svd | irlsq | both | rspec (rspec is FEM-only)\n"; return 1; } std::string output_dir = argv[2]; int64_t num_runs = std::stol(argv[3]); std::string mode = argv[4]; - if (mode != "svd" && mode != "irlsq" && mode != "both") { - std::cerr << "Error: must be one of {svd, irlsq, both}; got '" << mode << "'\n"; + if (mode != "svd" && mode != "irlsq" && mode != "both" && mode != "rspec") { + std::cerr << "Error: must be one of {svd, irlsq, both, rspec}; got '" << mode << "'\n"; return 1; } bool do_irlsq = (mode == "irlsq" || mode == "both"); @@ -922,6 +1329,19 @@ int run_benchmark(int argc, char* argv[]) { bool upcast_orth = (opt_long(5, 0) != 0); int64_t method_mask = opt_long(6, 79); T noise_level = (T)opt_double(7, 0.05); + double omega = opt_double(8, 0.0); + int64_t power_j = opt_long(9, 1); + + if (mode == "rspec") { + if (sparse_mode) { + std::cerr << "Error: mode 'rspec' is FEM-only; sparse input is not supported.\n"; + return 1; + } + if (power_j < 1 || power_j > 3) { + std::cerr << "Error: power_j must be in {1, 2, 3}; got " << power_j << "\n"; + return 1; + } + } std::cout << "=== CQRRT linop benchmark ===\n"; std::cout << " mode: " << mode << "\n"; @@ -947,6 +1367,8 @@ int run_benchmark(int argc, char* argv[]) { << " sCholQR3_basic=" << ((method_mask>>3)&1) << " linop_bqrrp=" << ((method_mask>>6)&1) << ")\n" << " noise_level: " << noise_level << "\n" + << " omega: " << omega << "\n" + << " power_j: " << power_j << "\n" << " num_runs: " << num_runs << "\n" #ifdef _OPENMP << " OpenMP threads: " << omp_get_max_threads() << "\n\n"; @@ -1046,6 +1468,115 @@ int run_benchmark(int argc, char* argv[]) { int64_t m = m_V; int64_t n = n_V; + + // -------- RSPEC mode (Algorithm 4): build C = L^T X^{-1} L and V_app = C^j V_FEM. -------- + if (mode == "rspec") { + std::cout << "\n=== RSPEC mode (Algorithm 4) ===\n" + << " omega: " << omega << "\n" + << " power_j: " << power_j << "\n"; + + // Load M as a CSR so we can form X = K - omega*M via shared-pattern axpby. + std::cout << "Loading M (mass) from " << M_file << " for X assembly... " << std::flush; + int64_t m_M, n_M, nnz_M; + auto M_csr = load_csr(M_file, m_M, n_M, nnz_M); + std::cout << "done (" << m_M << " x " << n_M << ", nnz=" << nnz_M << ")\n"; + if (m_M != m_K || n_M != m_K) { + std::cerr << "Error: M size (" << m_M << " x " << n_M + << ") must match K size " << m_K << "\n"; + return 1; + } + RandLAPACK::linops::SparseLinOp> M_op(m_K, m_K, M_csr); + + // 1. X = K - omega * M (CSR, shares sparsity with K and M). + std::cout << "Forming X = K - omega*M ... " << std::flush; + auto X_csr = RandLAPACK_extras::sparse_axpby_shared_pattern( + (T)1.0, K_csr, -(T)omega, M_csr); + std::cout << "done (nnz=" << X_csr.nnz << ")\n"; + + // 2. Convert X to Eigen::SparseMatrix (ColMajor) via triplets. + std::cout << "Converting X to Eigen sparse... " << std::flush; + Eigen::SparseMatrix X_eigen((Eigen::Index)m_K, (Eigen::Index)m_K); + { + std::vector> triplets; + triplets.reserve((size_t)X_csr.nnz); + for (int64_t i = 0; i < m_K; ++i) { + for (int64_t p = X_csr.rowptr[i]; p < X_csr.rowptr[i+1]; ++p) { + triplets.emplace_back((int)i, (int)X_csr.colidxs[p], X_csr.vals[p]); + } + } + X_eigen.setFromTriplets(triplets.begin(), triplets.end()); + } + std::cout << "done\n"; + + // 3. Factor X via SparseLU (handles indefinite case). + std::cout << "Factorizing X = LU (SparseLU) ... " << std::flush; + RandLAPACK_extras::linops::SparseLUSolverLinOp X_inv_op(std::move(X_eigen)); + auto x_fact_start = steady_clock::now(); + try { + X_inv_op.factorize(); + } catch (RandBLAS::Error const& e) { + auto x_fact_stop = steady_clock::now(); + long x_fact_us = duration_cast(x_fact_stop - x_fact_start).count(); + std::cerr << "\nSparseLU factorization failed (omega close to an eigenvalue): " + << e.what() << "\n"; + + // Write a single sentinel row to the CSV and return cleanly. + char time_buf[64]; + time_t now = time(nullptr); + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); + std::string results_file = output_dir + "/" + time_buf + "_rspec_results.csv"; + + std::vector> stub; + rspec_result r{}; + r.m = m_K; r.n = n_V; + r.run_idx = 0; + r.alg_name = "factorize_failed"; + r.qr_status = -99; + r.qr_time_us = -1; + r.factor_time_us = x_fact_us; + r.rspec_total_us = x_fact_us; + int top_k = (int)std::min(10, n_V); + r.top_eigvals.assign(top_k, std::numeric_limits::quiet_NaN()); + r.top_residuals.assign(top_k, std::numeric_limits::quiet_NaN()); + stub.push_back(r); + write_rspec_csv(results_file, stub, m_K, n_V, num_runs, + K_file, M_file, V_file, omega, power_j, + sketch_nnz, block_size, method_mask, top_k); + std::cout << "Stub CSV written to " << results_file << " (qr_status=-99).\n"; + return 0; + } + auto x_fact_stop = steady_clock::now(); + long x_factor_time_us = duration_cast(x_fact_stop - x_fact_start).count(); + std::cout << "done (" << x_factor_time_us << " us)\n"; + + // 4. L_op: wrap the L = chol(M) factor as a sparse linop (non-owning view). + auto L_csc = L_inv_op.make_L_csc(); + RandLAPACK::linops::SparseLinOp> L_op(m_K, m_K, L_csc); + + // 5. Compose C = L^T * X^{-1} * L. + RandLAPACK::linops::TransposedOp L_T_op(L_op); + RandLAPACK::linops::CompositeOperator inner_op(m_K, m_K, X_inv_op, L_op); + inner_op.block_size = block_size; + RandLAPACK::linops::CompositeOperator C_op(m_K, m_K, L_T_op, inner_op); + C_op.block_size = block_size; + + // 6. V_app = C^j * V_FEM (implicit). + RandLAPACK::linops::PowerOp Cj_op(C_op, (int)power_j); + RandLAPACK::linops::CompositeOperator V_app_op(m_K, n_V, Cj_op, V_op); + V_app_op.block_size = block_size; + + std::cout << "Operator chain: V_app = C^" << power_j + << " * V_FEM (" << m_K << " x " << n_V << ")\n\n"; + + long total_factor_us = chol_time_us + x_factor_time_us; + return run_rspec_benchmark( + V_app_op, C_op, K_op, M_op, + m_K, n_V, output_dir, num_runs, + d_factor, sketch_nnz, block_size, + method_mask, total_factor_us, + K_file, M_file, V_file, omega, power_j); + } + RandLAPACK::linops::CompositeOperator KV_op(m, n, K_op, V_op); KV_op.block_size = block_size; RandLAPACK::linops::CompositeOperator J_op(m, n, L_inv_op, KV_op); @@ -1084,8 +1615,9 @@ int main(int argc, char* argv[]) { << " sparse mode: 'sparse' " << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" << " FEM mode: " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" - << " mode = svd | irlsq | both\n"; + << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]" + << " [omega] [power_j]\n" + << " mode = svd | irlsq | both | rspec (rspec is FEM-only)\n"; return 1; } diff --git a/extras/linops/ext_cholsolver_linop.hh b/extras/linops/ext_cholsolver_linop.hh index b207caefb..126581834 100644 --- a/extras/linops/ext_cholsolver_linop.hh +++ b/extras/linops/ext_cholsolver_linop.hh @@ -114,6 +114,8 @@ private: using Uplo = blas::Uplo; using Diag = blas::Diag; +public: + /// Create a RandBLAS CSCMatrix view that wraps L_sparse's raw CSC arrays. /// Eigen uses int for sparse indices, so we use CSCMatrix (not int64_t). /// The expert constructor with own_memory=false avoids copying the data. @@ -125,7 +127,6 @@ private: ); } -public: /// Compute the sparse Cholesky factorization A = L * L^T. /// diff --git a/extras/linops/ext_sparselu_linop.hh b/extras/linops/ext_sparselu_linop.hh index 8429948ad..a1957637f 100644 --- a/extras/linops/ext_sparselu_linop.hh +++ b/extras/linops/ext_sparselu_linop.hh @@ -79,6 +79,18 @@ public: factorization_done = true; } + /// Concept-required 12-arg overload (no Side); delegates to Side::Left. + void operator()( + Layout layout, + Op trans_A, Op trans_B, + int64_t m, int64_t n, int64_t k, + T alpha, const T* B, int64_t ldb, + T beta, T* C, int64_t ldc) + { + (*this)(Side::Left, layout, trans_A, trans_B, + m, n, k, alpha, B, ldb, beta, C, ldc); + } + /// Dense operator, Side::Left, ColMajor, Op::NoTrans on B. /// C := alpha * op(A)^{-1} * B + beta * C /// where op(A) = A if trans_A == NoTrans, A^T if trans_A == Trans. @@ -86,7 +98,7 @@ public: Side side, Layout layout, Op trans_A, Op trans_B, int64_t m, int64_t n, int64_t k, - T alpha, T* const B, int64_t ldb, + T alpha, const T* B, int64_t ldb, T beta, T* C, int64_t ldc) { randblas_require(side == Side::Left); From 2ab53b5655babb87d8f543e429175302352f187a Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 14:43:12 -0700 Subject: [PATCH 25/47] Drop SVD/upcast paths from applications benchmark; remove obsolete test - CQRRT_linop_applications: remove GSVD post-processing (gesdd on R for generalized singular values/vectors), upcast-orth diagnostic, and the "svd"/"both" modes. Available modes are now {irlsq, rspec}. Drops ~410 lines: result-struct SVD fields, A_materialized + AtA_precomputed setup, do_svd/do_irlsq branching, GSVD CSV writers, skip_svd/upcast_orth CLI args, and the K_file/V_file params from the inner runner (unused after the GSVD writer deletion). - extras/test: delete test_ext_sparse_axpy.cc and drop it from CMakeLists.txt; the helper it covered is not part of the API surface we still care about. --- .../CQRRT_linop_applications.cc | 488 +++--------------- extras/test/CMakeLists.txt | 1 - extras/test/misc/test_ext_sparse_axpy.cc | 102 ---- 3 files changed, 77 insertions(+), 514 deletions(-) delete mode 100644 extras/test/misc/test_ext_sparse_axpy.cc diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index 9e21e8225..ab27bd38f 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -1,4 +1,4 @@ -// Unified Q-less QR benchmark — GSVD post-processing and/or IR-LSQ. +// Unified Q-less QR benchmark — IR-LSQ application, plus rspec (Algorithm 4). // // Pipeline: // 1. Load matrices (FEM mode: K, M, V .mtx files; sparse mode: a single A.mtx). @@ -8,17 +8,17 @@ // 4. Run Q-less QR via one of 5 variants (CQRRT_linop, CholQR, sCholQR3, // sCholQR3_basic, CQRRT_linop_bqrrp), selected by method_mask. // 5. Post-processing dictated by : -// svd — orth_err + r_backward_error + gesdd(R) for sing. values/vectors // irlsq — sketch-and-solve x_0 (Epperly–Meier–Nakatsukasa 2025 Alg. 1 line 3) + IterRefineLSQ -// both — both, sharing the same QR step and timestamp +// rspec — reduced spectral approximation (Algorithm 4): Rayleigh–Ritz on +// range(C^j V_FEM), C = L^T (K - ω M)^{-1} L. FEM-only. // // Usage: // ./CQRRT_linop_applications -// sparse [nnz] [b] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level] +// sparse [nnz] [b] [compute_cond] [method_mask] [noise_level] // ./CQRRT_linop_applications -// [nnz] [b] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level] +// [nnz] [b] [compute_cond] [method_mask] [noise_level] [omega] [power_j] // -// mode = "svd" | "irlsq" | "both" +// mode = "irlsq" | "rspec" // method_mask = bitmask of Q-less QR variants (default 0b1001111 = 79) // bit 0 ( 1): CQRRT_linop (TRSM_IDENTITY) // bit 1 ( 2): CholQR @@ -80,16 +80,8 @@ struct bench_result { int qr_status; // 0 on success long qr_time_us; // -1 if QR failed - // SVD-mode fields (sentinel -1 / unset when not computed) + // Q-factor orthogonality: ||Q^T Q - I||_F / sqrt(n), computed for all methods. T orth_error; - bool is_orthonormal; - double r_backward_error; - double orth_error_upcast; - long app_b_time_us; - long app_c_time_us; - T right_svec_orth_error; - long total_b_time_us; - long total_c_time_us; // IR-LSQ-mode fields long ir_total_us; @@ -106,19 +98,11 @@ struct bench_result { long analytical_kb; }; -// Compute Q = A * R^{-1} uniformly for all algorithms -template -static void compute_Q_from_R( - GLO& A_op, T* R, int64_t ldr, - T* Q_out, int64_t m, int64_t n) { - auto Eye = make_eye(n); - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye.data(), n, (T)0.0, Q_out, m); - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, - blas::Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); -} - -// Compute A^T A via blocked linop calls. Peak memory: O(m*b + n^2). +// Compute A^T A via blocked linop calls. Peak memory: O(m*b + n*b). +// AtA n x n output (caller-allocated) +// E_block n x b identity-column scratch +// A_block m x b linop NoTrans output +// AtA_block n x b linop Trans output, copied into AtA[:, j0:j0+bk] each iter template static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { std::fill(AtA, AtA + n * n, (T)0.0); @@ -143,111 +127,6 @@ static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t } } -template -static double compute_r_backward_error(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { - int64_t b = (block_size > 0) ? block_size : 256; - - std::vector AtA(n * n, 0.0); - compute_AtA_blocked(A_op, m, n, AtA.data(), b); - - std::vector RtR(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, n, (T)1.0, R, n, (T)0.0, RtR.data(), n); - fill_lower_from_upper(RtR.data(), n); - - T norm_A_sq = 0; - for (int64_t i = 0; i < n; ++i) - norm_A_sq += AtA[i + i * n]; - - T diff_norm_sq = 0; - #pragma omp parallel for reduction(+:diff_norm_sq) schedule(static) - for (int64_t i = 0; i < n * n; ++i) { - T d = AtA[i] - RtR[i]; - diff_norm_sq += d * d; - } - - return (double)(std::sqrt(diff_norm_sq) / (norm_A_sq)); -} - -// Upcast orthogonality: float->double via BLAS++; double->long double via Eigen. -template -static double compute_orth_upcast(GLO& A_op, const T* R, int64_t m, int64_t n, - const T* A_dense = nullptr) { - const T* A_ptr; - std::vector A_buf; - if (A_dense) { - A_ptr = A_dense; - } else { - A_buf.resize(m * n); - auto Eye = make_eye(n); - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_buf.data(), m); - A_ptr = A_buf.data(); - } - - std::vector A_U(m * n), R_U(n * n); - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < m * n; ++i) A_U[i] = (U)A_ptr[i]; - #pragma omp parallel for schedule(static) - for (int64_t i = 0; i < n * n; ++i) R_U[i] = (U)R[i]; - - if constexpr (std::is_same_v) { - blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, - blas::Op::NoTrans, blas::Diag::NonUnit, - m, n, 1.0, R_U.data(), n, A_U.data(), m); - std::vector GmI(n * n, 0.0); - RandLAPACK::util::eye(n, n, GmI.data()); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, 1.0, A_U.data(), m, -1.0, GmI.data(), n); - return lapack::lansy(lapack::Norm::Fro, blas::Uplo::Upper, n, GmI.data(), n) / std::sqrt((double)n); - } else { - Eigen::Map> - A_map(A_U.data(), m, n); - Eigen::Map> - R_map(R_U.data(), n, n); - Eigen::Matrix Qt = - R_map.transpose().template triangularView().solve(A_map.transpose()); - Eigen::Matrix QtQ = Qt * Qt.transpose(); - for (int64_t i = 0; i < n; ++i) QtQ(i, i) -= (U)1.0; - return (double)(QtQ.norm() / std::sqrt((U)n)); - } -} - -// ============================================================================ -// SVD post-processing -// ============================================================================ - -template -static void app_generalized_svals( - const T* R, int64_t ldr, int64_t n, - T* sigma, long& app_time_us) -{ - auto start = steady_clock::now(); - std::vector R_copy(n * n); - lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - std::vector dummy_U(1), dummy_Vt(1); - lapack::gesdd(lapack::Job::NoVec, n, n, R_copy.data(), n, - sigma, dummy_U.data(), 1, dummy_Vt.data(), 1); - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); -} - -template -static void app_generalized_svecs( - const T* R, int64_t ldr, int64_t n, - T* sigma, T* V_R, T* U_R, - T& right_svec_orth_error, long& app_time_us) -{ - auto start = steady_clock::now(); - std::vector R_copy(n * n); - lapack::lacpy(lapack::MatrixType::General, n, n, R, ldr, R_copy.data(), n); - lapack::gesdd(lapack::Job::AllVec, n, n, R_copy.data(), n, - sigma, U_R, n, V_R, n); - auto stop = steady_clock::now(); - app_time_us = duration_cast(stop - start).count(); - right_svec_orth_error = RandLAPACK::testing::orthogonality_error(V_R, n, n); -} - // Estimate ||A||_2 via power iteration on A^T A. O(iters * (m+n) memory) — no materialization. template static T estimate_op_2norm(GLO& A_op, int64_t m, int64_t n, int iters = 10) { @@ -290,95 +169,6 @@ static T compute_orth_error_memlite(GLO& A_op, const T* R, int64_t m, int64_t n, return std::sqrt(s) / std::sqrt((T)n); } -// ============================================================================ -// CSV writers — GSVD (preserves the column order of the original applications.cc) -// ============================================================================ - -template -static void write_gsvd_header_comments( - std::ofstream& out, int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) -{ - time_t now = time(nullptr); - out << "# Date: " << ctime(&now) - << "# Matrix dimensions: m=" << m << " n=" << n << "\n" - << "# Runs per algorithm: " << num_runs << "\n" -#ifdef _OPENMP - << "# OpenMP threads: " << omp_get_max_threads() << "\n" -#else - << "# OpenMP threads: 1\n" -#endif - << "# K_file: " << K_file << "\n" - << "# V_file: " << V_file << "\n" - << "# d_factor: " << d_factor << "\n" - << "# sketch_nnz: " << sketch_nnz << "\n" - << "# block_size: " << block_size << "\n" - << "# skip_apps: " << (skip_apps ? 1 : 0) << "\n" - << "# compute_cond: " << (compute_cond ? 1 : 0) << "\n"; -} - -template -static void write_gsvd_results( - const std::string& filename, - const std::vector>& results, - int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) -{ - std::ofstream out(filename); - out << "# GSVD Benchmark results\n"; - write_gsvd_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "m,n,run,algorithm,chol_time_us,qr_time_us,orth_error,r_backward_error,orth_error_upcast," - << "app_b_time_us," - << "app_c_time_us,right_svec_orth_error," - << "total_b_time_us,total_c_time_us," - << "peak_rss_kb,analytical_kb\n"; - for (const auto& r : results) { - out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name << "," - << r.chol_time_us << "," - << r.qr_time_us << "," - << std::scientific << std::setprecision(6) << r.orth_error << "," - << std::scientific << std::setprecision(6) << r.r_backward_error << "," - << std::scientific << std::setprecision(6) << r.orth_error_upcast << "," - << r.app_b_time_us << "," - << r.app_c_time_us << "," - << std::scientific << std::setprecision(6) << r.right_svec_orth_error << "," - << r.total_b_time_us << "," << r.total_c_time_us << "," - << r.peak_rss_kb << "," << r.analytical_kb - << "\n"; - } -} - -template -static void write_gsvd_breakdown( - const std::string& filename, - const std::vector>& results, - int64_t m, int64_t n, int num_runs, - const std::string& K_file, const std::string& V_file, - T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_apps, bool compute_cond) -{ - std::ofstream out(filename); - out << "# GSVD Benchmark runtime breakdown\n"; - write_gsvd_header_comments(out, m, n, num_runs, K_file, V_file, d_factor, sketch_nnz, block_size, skip_apps, compute_cond); - out << "# Times are in microseconds\n"; - out << "# CQRRT_linop breakdown (11): alloc, sketch, qr, tri_inv, fwd, adj, trmm, chol, finalize, rest, total\n"; - out << "m,n,run,algorithm"; - for (int i = 0; i < 11; ++i) out << ",t" << i; - out << "\n"; - for (const auto& r : results) { - out << r.m << "," << r.n << "," << r.run_idx << "," << r.alg_name; - for (size_t i = 0; i < r.qr_breakdown.size(); ++i) - out << "," << r.qr_breakdown[i]; - for (size_t i = r.qr_breakdown.size(); i < 11; ++i) - out << ",0"; - out << "\n"; - } -} - // ============================================================================ // CSV writers — IR-LSQ (preserves the column order plot_irlsq_results.m expects) // ============================================================================ @@ -524,30 +314,9 @@ static void write_rspec_csv( } // ============================================================================ -// Console summaries +// Console summary // ============================================================================ -template -static void print_svd_summary(const std::string& alg_name, const std::vector>& results) { - printf("\n %s (SVD):\n", alg_name.c_str()); - for (const auto& r : results) { - if (r.qr_status != 0) { - printf(" Run %ld: QR breakdown (status=%d)\n", (long)r.run_idx, r.qr_status); - continue; - } - printf(" Run %ld: orth_err=%.2e, r_bwd_err=%.2e, QR=%ld us\n", - (long)r.run_idx, (double)r.orth_error, r.r_backward_error, r.qr_time_us); - if (r.orth_error_upcast > 0) - printf(" orth_upcast=%.2e\n", r.orth_error_upcast); - if (r.app_b_time_us > 0 || r.app_c_time_us > 0) { - printf(" SVD: App(b)=%ld us, App(c)=%ld us\n", - r.app_b_time_us, r.app_c_time_us); - } - printf(" Memory: peak_RSS=%ld KB, predicted=%ld KB\n", - r.peak_rss_kb, r.analytical_kb); - } -} - template static void print_irlsq_summary(const bench_result& r) { std::printf("\n [%s] Run %ld (noise=%.3f):\n", @@ -577,20 +346,15 @@ static int run_benchmark_inner( OpType& A_op, int64_t m, int64_t n, int64_t input_nnz, const std::string& output_dir, int64_t num_runs, - const std::string& mode, T d_factor, int64_t sketch_nnz, int64_t block_size, - bool skip_svd, bool compute_cond, bool upcast_orth, + bool compute_cond, int64_t method_mask, T noise_level, long chol_time_us, - const std::string& K_file, const std::string& V_file, const std::string& op_label, const std::string& input_label, - const std::vector* b_ptr, // M-vector RHS (irlsq/both); nullptr if svd-only - const std::vector* x_true_ptr) // N-vector ground truth (sparse irlsq); nullptr otherwise + const std::vector* b_ptr, // M-vector RHS + const std::vector* x_true_ptr) // N-vector ground truth (sparse only); nullptr otherwise { - bool do_svd = (mode == "svd" || mode == "both"); - bool do_irlsq = (mode == "irlsq" || mode == "both"); - // Build the ordered list of selected algorithm names from the bitmask. std::vector selected_algs; if (method_mask & 1) selected_algs.push_back("CQRRT_linop"); @@ -630,37 +394,16 @@ static int run_benchmark_inner( } std::cout << "done\n\n"; - // Precompute ||A||_2 and ||b|| for the Higham backward-error metric used by IR-LSQ: + // Precompute ||A||_2 and ||b|| for the Higham backward-error metric: // ls_residual_norm = ||A x - b|| / (||A||_2 * ||x|| + ||b||) T A_2norm = (T)0, b_norm = (T)0; - if (do_irlsq && b_ptr) { + if (b_ptr) { std::cout << "Estimating ||A||_2 via power iteration (10 iters)... " << std::flush; A_2norm = estimate_op_2norm(A_op, m, n, 10); b_norm = blas::nrm2(m, b_ptr->data(), 1); std::cout << "||A||_2 ~ " << A_2norm << ", ||b|| = " << b_norm << "\n\n"; } - // Pre-materialize A for upcast / r_backward_error (SVD path only) - T* A_materialized = nullptr; - std::vector AtA_precomputed; - T norm_A_sq_precomputed = 0; - if (do_svd && upcast_orth) { - std::cout << "Materializing " << op_label << " for upcast (" << m << " x " << n << ", " - << (m * n * sizeof(T) / (1024.0 * 1024.0 * 1024.0)) << " GB)... " << std::flush; - A_materialized = new T[m * n]; - auto Eye = make_eye(n); - A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_materialized, m); - std::cout << "done\n\n"; - - AtA_precomputed.assign(n * n, (T)0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, m, (T)1.0, A_materialized, m, (T)0.0, AtA_precomputed.data(), n); - fill_lower_from_upper(AtA_precomputed.data(), n); - for (int64_t i = 0; i < n; ++i) - norm_A_sq_precomputed += AtA_precomputed[i + i * n]; - } - T x_true_norm = (T)0; if (x_true_ptr) x_true_norm = blas::nrm2(n, x_true_ptr->data(), 1); @@ -683,14 +426,6 @@ static int run_benchmark_inner( res.qr_status = 0; res.qr_time_us = 0; res.orth_error = (T)-1.0; - res.is_orthonormal = false; - res.r_backward_error = -1.0; - res.orth_error_upcast = 0.0; - res.app_b_time_us = 0; - res.app_c_time_us = 0; - res.right_svec_orth_error = (T)-1.0; - res.total_b_time_us = 0; - res.total_c_time_us = 0; res.ir_total_us = 0; res.ir_outer_iters = 0; res.ir_inner_iters_total = 0; @@ -763,56 +498,16 @@ static int run_benchmark_inner( res.analytical_kb = 0; alg_results.push_back(res); all_results.push_back(res); - if (do_irlsq) print_irlsq_summary(res); + print_irlsq_summary(res); continue; } std::cout << "done (" << res.qr_time_us << " us)"; - // ---- Orth_error: ||Q^T Q - I||_F / sqrt(n) via blocked compute (memlite). ---- - // Runs for both SVD and IR-LSQ modes — Max wants this for every method. - res.orth_error = compute_orth_error_memlite(A_op, R.data(), m, n, block_size); - res.is_orthonormal = (res.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - - // ---- SVD post-processing ---- - if (do_svd) { - if (A_materialized) { - std::vector RtR(n * n, 0.0); - blas::syrk(blas::Layout::ColMajor, blas::Uplo::Upper, blas::Op::Trans, - n, n, (T)1.0, R.data(), n, (T)0.0, RtR.data(), n); - fill_lower_from_upper(RtR.data(), n); - T diff_sq = 0; - #pragma omp parallel for reduction(+:diff_sq) schedule(static) - for (int64_t i = 0; i < n * n; ++i) { - T d = AtA_precomputed[i] - RtR[i]; diff_sq += d * d; - } - res.r_backward_error = (double)(std::sqrt(diff_sq) / norm_A_sq_precomputed); - } else { - res.r_backward_error = compute_r_backward_error(A_op, R.data(), m, n, block_size); - } - - if (upcast_orth) { - if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); - } else if constexpr (std::is_same_v) { - res.orth_error_upcast = compute_orth_upcast(A_op, R.data(), m, n, A_materialized); - } - } - - if (!skip_svd) { - std::vector sigma_b(n, 0.0); - app_generalized_svals(R.data(), n, n, sigma_b.data(), res.app_b_time_us); - - std::vector sigma_c(n, 0.0), V_R(n * n, 0.0), U_R(n * n, 0.0); - app_generalized_svecs(R.data(), n, n, sigma_c.data(), V_R.data(), U_R.data(), - res.right_svec_orth_error, res.app_c_time_us); - } - - res.total_b_time_us = res.qr_time_us + res.app_b_time_us; - res.total_c_time_us = res.qr_time_us + res.app_c_time_us; - } + // ---- Orth_error: ||Q^T Q - I||_F / sqrt(n), blocked compute. Runs for every method. ---- + res.orth_error = compute_orth_error_memlite(A_op, R.data(), m, n, block_size); // ---- IR-LSQ post-processing ---- - if (do_irlsq) { + { std::cout << ". IR-LSQ ... " << std::flush; const std::vector& b = *b_ptr; auto ls_t0 = steady_clock::now(); @@ -888,46 +583,28 @@ static int run_benchmark_inner( res.ls_solution_error = (T)-1.0; } std::cout << "done (" << res.ir_total_us << " us)\n"; - } else { - std::cout << "\n"; } - if (do_irlsq) print_irlsq_summary(res); + print_irlsq_summary(res); alg_results.push_back(res); all_results.push_back(res); } - - if (do_svd) print_svd_summary(alg_name, alg_results); } - delete[] A_materialized; - // ================================================================ - // CSV output (shared timestamp across modes) + // CSV output // ================================================================ char time_buf[64]; time_t now = time(nullptr); strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", localtime(&now)); - if (do_svd) { - std::string results_file = output_dir + "/" + time_buf + "_gsvd_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_gsvd_breakdown.csv"; - write_gsvd_results(results_file, all_results, m, n, num_runs, - K_file, V_file, d_factor, sketch_nnz, block_size, skip_svd, compute_cond); - std::cout << "\nGSVD results written to " << results_file << "\n"; - write_gsvd_breakdown(breakdown_file, all_results, m, n, num_runs, - K_file, V_file, d_factor, sketch_nnz, block_size, skip_svd, compute_cond); - std::cout << "GSVD breakdown written to " << breakdown_file << "\n"; - } - if (do_irlsq) { - std::string results_file = output_dir + "/" + time_buf + "_irlsq_results.csv"; - std::string breakdown_file = output_dir + "/" + time_buf + "_irlsq_breakdown.csv"; - write_irlsq_results(results_file, all_results, m, n, input_nnz, input_label, - noise_level, d_factor, sketch_nnz, block_size, method_mask); - std::cout << "\nIR-LSQ results written to " << results_file << "\n"; - write_irlsq_breakdown(breakdown_file, all_results); - std::cout << "IR-LSQ breakdown written to " << breakdown_file << "\n"; - } + std::string results_file = output_dir + "/" + time_buf + "_irlsq_results.csv"; + std::string breakdown_file = output_dir + "/" + time_buf + "_irlsq_breakdown.csv"; + write_irlsq_results(results_file, all_results, m, n, input_nnz, input_label, + noise_level, d_factor, sketch_nnz, block_size, method_mask); + std::cout << "\nIR-LSQ results written to " << results_file << "\n"; + write_irlsq_breakdown(breakdown_file, all_results); + std::cout << "IR-LSQ breakdown written to " << breakdown_file << "\n"; return 0; } @@ -1270,22 +947,21 @@ int run_benchmark(int argc, char* argv[]) { std::cerr << "Usage: " << argv[0] << " \n" << " sparse mode: 'sparse' " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " [sketch_nnz] [block_size] [compute_cond] [method_mask] [noise_level]\n" << " FEM mode: " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]" + << " [sketch_nnz] [block_size] [compute_cond] [method_mask] [noise_level]" << " [omega] [power_j]\n" - << " mode = svd | irlsq | both | rspec (rspec is FEM-only)\n"; + << " mode = irlsq | rspec (rspec is FEM-only)\n"; return 1; } std::string output_dir = argv[2]; int64_t num_runs = std::stol(argv[3]); std::string mode = argv[4]; - if (mode != "svd" && mode != "irlsq" && mode != "both" && mode != "rspec") { - std::cerr << "Error: must be one of {svd, irlsq, both, rspec}; got '" << mode << "'\n"; + if (mode != "irlsq" && mode != "rspec") { + std::cerr << "Error: must be one of {irlsq, rspec}; got '" << mode << "'\n"; return 1; } - bool do_irlsq = (mode == "irlsq" || mode == "both"); std::string arg5 = argv[5]; bool sparse_mode = (arg5 == "sparse"); @@ -1324,13 +1000,11 @@ int run_benchmark(int argc, char* argv[]) { }; int64_t sketch_nnz = opt_long(1, 4); int64_t block_size = opt_long(2, 0); - bool skip_svd = (opt_long(3, 0) != 0); - bool compute_cond = (opt_long(4, 0) != 0); - bool upcast_orth = (opt_long(5, 0) != 0); - int64_t method_mask = opt_long(6, 79); - T noise_level = (T)opt_double(7, 0.05); - double omega = opt_double(8, 0.0); - int64_t power_j = opt_long(9, 1); + bool compute_cond = (opt_long(3, 0) != 0); + int64_t method_mask = opt_long(4, 79); + T noise_level = (T)opt_double(5, 0.05); + double omega = opt_double(6, 0.0); + int64_t power_j = opt_long(7, 1); if (mode == "rspec") { if (sparse_mode) { @@ -1357,9 +1031,7 @@ int run_benchmark(int argc, char* argv[]) { std::cout << " d_factor: " << d_factor << "\n" << " sketch_nnz: " << sketch_nnz << "\n" << " block_size: " << block_size << "\n" - << " skip_svd: " << (skip_svd ? "yes" : "no") << "\n" << " compute_cond: " << (compute_cond ? "yes" : "no") << "\n" - << " upcast_orth: " << (upcast_orth ? "yes" : "no") << "\n" << " method_mask: " << method_mask << " (linop=" << (method_mask&1) << " CholQR=" << ((method_mask>>1)&1) @@ -1392,38 +1064,34 @@ int run_benchmark(int argc, char* argv[]) { } // Sparse irlsq b construction: x_true ~ U(-1,1)^n, b = A x_true + scaled Gaussian noise. - std::vector x_true, b; - if (do_irlsq) { - x_true.assign(n, (T)0); - { - std::mt19937 rng(42); - std::uniform_real_distribution dist((T)-1.0, (T)1.0); - for (auto& v : x_true) v = dist(rng); - } - std::vector b_clean(m, (T)0), noise_vec(m, (T)0); - A_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b_clean.data(), m); - T b_clean_norm = blas::nrm2(m, b_clean.data(), 1); - std::mt19937 noise_rng(13); - std::normal_distribution N01(0, 1); - for (auto& v : noise_vec) v = N01(noise_rng); - T raw_noise_norm = blas::nrm2(m, noise_vec.data(), 1); - T scale = noise_level * b_clean_norm / raw_noise_norm; - b.assign(m, (T)0); - for (int64_t i = 0; i < m; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; - std::cout << "Synthetic LS problem: ||x_true|| = " << blas::nrm2(n, x_true.data(), 1) - << ", ||b|| = " << blas::nrm2(m, b.data(), 1) << "\n\n"; + std::vector x_true(n, (T)0); + { + std::mt19937 rng(42); + std::uniform_real_distribution dist((T)-1.0, (T)1.0); + for (auto& v : x_true) v = dist(rng); } + std::vector b_clean(m, (T)0), noise_vec(m, (T)0); + A_linop(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m, 1, n, (T)1.0, x_true.data(), n, (T)0.0, b_clean.data(), m); + T b_clean_norm = blas::nrm2(m, b_clean.data(), 1); + std::mt19937 noise_rng(13); + std::normal_distribution N01(0, 1); + for (auto& v : noise_vec) v = N01(noise_rng); + T raw_noise_norm = blas::nrm2(m, noise_vec.data(), 1); + T scale = noise_level * b_clean_norm / raw_noise_norm; + std::vector b(m, (T)0); + for (int64_t i = 0; i < m; ++i) b[i] = b_clean[i] + scale * noise_vec[i]; + std::cout << "Synthetic LS problem: ||x_true|| = " << blas::nrm2(n, x_true.data(), 1) + << ", ||b|| = " << blas::nrm2(m, b.data(), 1) << "\n\n"; return run_benchmark_inner( - A_linop, m, n, nnz_A, output_dir, num_runs, mode, + A_linop, m, n, nnz_A, output_dir, num_runs, d_factor, sketch_nnz, block_size, - skip_svd, compute_cond, upcast_orth, + compute_cond, method_mask, noise_level, - 0L /*chol_time_us*/, "sparse", A_file, + 0L /*chol_time_us*/, "A (" + A_file + ")", A_file, - do_irlsq ? &b : nullptr, - do_irlsq ? &x_true : nullptr); + &b, &x_true); } // ================================================================ @@ -1584,28 +1252,26 @@ int run_benchmark(int argc, char* argv[]) { std::cout << "Composite operator J = L^{-1} K V : " << m << " x " << n << "\n\n"; // FEM irlsq b construction: b = L^{-1} * r, r ~ N(0, 1)^{m_K}. No ground truth x_true. - std::vector b; - if (do_irlsq) { - std::vector r(m_K, (T)0); + std::vector r(m_K, (T)0); + { std::mt19937 rng_b(13); std::normal_distribution N01(0, 1); for (auto& v : r) v = N01(rng_b); - b.assign(m_K, (T)0); - L_inv_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m_K, 1, m_K, (T)1.0, r.data(), m_K, (T)0.0, b.data(), m_K); - std::cout << "FEM IR-LSQ b: ||b|| = " << blas::nrm2(m_K, b.data(), 1) - << " (b = L^{-1} r, r ~ N(0,1)^M)\n\n"; } + std::vector b(m_K, (T)0); + L_inv_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, + m_K, 1, m_K, (T)1.0, r.data(), m_K, (T)0.0, b.data(), m_K); + std::cout << "FEM IR-LSQ b: ||b|| = " << blas::nrm2(m_K, b.data(), 1) + << " (b = L^{-1} r, r ~ N(0,1)^M)\n\n"; return run_benchmark_inner( - J_op, m, n, nnz_K, output_dir, num_runs, mode, + J_op, m, n, nnz_K, output_dir, num_runs, d_factor, sketch_nnz, block_size, - skip_svd, compute_cond, upcast_orth, + compute_cond, method_mask, noise_level, - chol_time_us, K_file, V_file, + chol_time_us, "L^{-1} K V (M=" + M_file + ")", K_file, - do_irlsq ? &b : nullptr, - nullptr /*x_true_ptr: FEM has no ground truth*/); + &b, nullptr /*x_true_ptr: FEM has no ground truth*/); } int main(int argc, char* argv[]) { @@ -1613,11 +1279,11 @@ int main(int argc, char* argv[]) { std::cerr << "Usage: " << argv[0] << " \n" << " sparse mode: 'sparse' " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]\n" + << " [sketch_nnz] [block_size] [compute_cond] [method_mask] [noise_level]\n" << " FEM mode: " - << " [sketch_nnz] [block_size] [skip_svd] [compute_cond] [upcast_orth] [method_mask] [noise_level]" + << " [sketch_nnz] [block_size] [compute_cond] [method_mask] [noise_level]" << " [omega] [power_j]\n" - << " mode = svd | irlsq | both | rspec (rspec is FEM-only)\n"; + << " mode = irlsq | rspec (rspec is FEM-only)\n"; return 1; } diff --git a/extras/test/CMakeLists.txt b/extras/test/CMakeLists.txt index 6cf89fd5b..fbddc0fbc 100644 --- a/extras/test/CMakeLists.txt +++ b/extras/test/CMakeLists.txt @@ -16,7 +16,6 @@ if (GTest_FOUND) linops/test_ext_solver_linop_unified.cc linops/test_ext_composite_linop.cc linops/test_ext_sparselu_linop.cc - misc/test_ext_sparse_axpy.cc ) # Create test executable diff --git a/extras/test/misc/test_ext_sparse_axpy.cc b/extras/test/misc/test_ext_sparse_axpy.cc deleted file mode 100644 index 047d304e6..000000000 --- a/extras/test/misc/test_ext_sparse_axpy.cc +++ /dev/null @@ -1,102 +0,0 @@ -// Tests for sparse_axpby_shared_pattern in extras/misc/ext_sparse_axpy.hh. - -#include "../../misc/ext_sparse_axpy.hh" - -#include -#include -#include -#include - -using std::vector; - - -// Build a small CSR matrix (n_rows × n_cols, nnz given) by deep-copying user-provided -// rowptr/colidxs/vals arrays. The matrix owns its memory and frees on destruction. -template -RandBLAS::sparse_data::CSRMatrix make_owned_csr( - int64_t n_rows, int64_t n_cols, int64_t nnz, - const sint_t* rowptr_src, const sint_t* colidxs_src, const T* vals_src) -{ - RandBLAS::sparse_data::CSRMatrix M(n_rows, n_cols); - M.reserve(nnz); - for (int64_t i = 0; i <= n_rows; ++i) M.rowptr[i] = rowptr_src[i]; - for (int64_t i = 0; i < nnz; ++i) M.colidxs[i] = colidxs_src[i]; - for (int64_t i = 0; i < nnz; ++i) M.vals[i] = vals_src[i]; - return M; -} - - -TEST(TestSparseAxpby, shared_pattern_simple) { - // 3x3 tridiagonal pattern. - // row 0: cols 0, 1 - // row 1: cols 0, 1, 2 - // row 2: cols 1, 2 - // nnz = 7 - int rowptr[4] = {0, 2, 5, 7}; - int colidxs[7] = {0, 1, 0, 1, 2, 1, 2}; - double A_vals[7] = {1, 2, 3, 4, 5, 6, 7}; - double B_vals[7] = {10, 20, 30, 40, 50, 60, 70}; - - auto A = make_owned_csr(3, 3, 7, rowptr, colidxs, A_vals); - auto B = make_owned_csr(3, 3, 7, rowptr, colidxs, B_vals); - - // C := 2.0 * A + (-3.0) * B - auto C = RandLAPACK_extras::sparse_axpby_shared_pattern( - 2.0, A, -3.0, B); - - ASSERT_EQ(C.n_rows, 3); - ASSERT_EQ(C.n_cols, 3); - ASSERT_EQ(C.nnz, 7); - - for (int i = 0; i <= 3; ++i) EXPECT_EQ(C.rowptr[i], rowptr[i]); - for (int i = 0; i < 7; ++i) EXPECT_EQ(C.colidxs[i], colidxs[i]); - - double expected[7]; - for (int i = 0; i < 7; ++i) - expected[i] = 2.0 * A_vals[i] + (-3.0) * B_vals[i]; - - for (int i = 0; i < 7; ++i) - EXPECT_NEAR(C.vals[i], expected[i], 1e-14); -} - - -TEST(TestSparseAxpby, shifted_inverse_pattern) { - // Mimics the rspec use case: X = K - omega * M with K, M sharing sparsity. - // Symmetric 4x4 pattern with 10 nonzeros. - int rowptr[5] = {0, 3, 6, 8, 10}; - int colidxs[10] = {0, 1, 2, 0, 1, 3, 0, 2, 1, 3}; - double K_vals[10] = {4, 1, 1, 1, 3, 1, 1, 2, 1, 5}; - double M_vals[10] = {2, 0.5, 0.5, 0.5, 2, 0.5, 0.5, 1, 0.5, 2}; - - auto K = make_owned_csr(4, 4, 10, rowptr, colidxs, K_vals); - auto M = make_owned_csr(4, 4, 10, rowptr, colidxs, M_vals); - - double omega = 0.7; - auto X = RandLAPACK_extras::sparse_axpby_shared_pattern( - 1.0, K, -omega, M); - - for (int i = 0; i < 10; ++i) { - double expected = K_vals[i] - omega * M_vals[i]; - EXPECT_NEAR(X.vals[i], expected, 1e-14); - } - ASSERT_EQ(X.nnz, 10); - for (int i = 0; i <= 4; ++i) EXPECT_EQ(X.rowptr[i], rowptr[i]); - for (int i = 0; i < 10; ++i) EXPECT_EQ(X.colidxs[i], colidxs[i]); -} - - -TEST(TestSparseAxpby, float_type) { - int rowptr[3] = {0, 1, 2}; - int colidxs[2] = {0, 1}; - float A_vals[2] = {1.5f, 2.5f}; - float B_vals[2] = {0.5f, 1.0f}; - - auto A = make_owned_csr(2, 2, 2, rowptr, colidxs, A_vals); - auto B = make_owned_csr(2, 2, 2, rowptr, colidxs, B_vals); - - auto C = RandLAPACK_extras::sparse_axpby_shared_pattern( - 2.0f, A, 4.0f, B); - - EXPECT_NEAR(C.vals[0], 2.0f * 1.5f + 4.0f * 0.5f, 1e-6f); - EXPECT_NEAR(C.vals[1], 2.0f * 2.5f + 4.0f * 1.0f, 1e-6f); -} From 48488a0ffc38271dc3b777c17a03dca1b86fa7c3 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 14:46:27 -0700 Subject: [PATCH 26/47] Use canonical condition-number and orth-error helpers in diagnostic.cc Drop the local orth_error wrapper and condition_number reimplementation in CQRRT_diagnostic.cc; their callers now use RandLAPACK::testing::orthogonality_error and RandLAPACK::util::cond_num_check directly. gram_condition_number is kept as a thin convenience that builds G = A^T A then calls cond_num_check. --- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index ec6b6c074..1a0536e31 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -112,11 +112,6 @@ static constexpr const char* PATH_DESCS[N_PATHS] = { // Helpers // ============================================================================ -template -static T orth_error(const T* Q, int64_t m, int64_t n) { - return RandLAPACK::testing::orthogonality_error(Q, m, n); -} - template static T rel_diff(const T* A, const T* B, int64_t len) { T nd = 0, na = 0; @@ -127,22 +122,13 @@ static T rel_diff(const T* A, const T* B, int64_t len) { return (na > 0) ? std::sqrt(nd / na) : std::sqrt(nd); } -template -static T condition_number(const T* A, int64_t m, int64_t n) { - std::vector tmp(A, A + m * n); - std::vector s(n); - lapack::gesdd(lapack::Job::NoVec, m, n, tmp.data(), m, - s.data(), nullptr, 1, nullptr, 1); - return (s[n-1] > 0) ? s[0] / s[n-1] : std::numeric_limits::infinity(); -} - -// Condition number of the Gram matrix G = A_pre^T A_pre +// Condition number of the Gram matrix G = A_pre^T A_pre. template static T gram_condition_number(const T* A_pre, int64_t m, int64_t n) { std::vector G(n * n, 0.0); blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, n, n, m, (T)1.0, A_pre, m, A_pre, m, (T)0.0, G.data(), n); - return condition_number(G.data(), n, n); + return RandLAPACK::util::cond_num_check(n, n, G.data(), /*verbose=*/false); } // Full CQRRT pipeline (matching CQRRT_linops Gram computation): @@ -174,7 +160,7 @@ static T cholqr_orth_error(const std::vector& A_pre, const T* A_orig, std::vector Q(A_orig, A_orig + m * n); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, G.data(), n, Q.data(), m); - return orth_error(Q.data(), m, n); + return RandLAPACK::testing::orthogonality_error(Q.data(), m, n); } // ============================================================================ @@ -241,7 +227,7 @@ static TrialResult run_trial( for (int64_t i = 0; i <= j; ++i) R_sk[i + j*n] = Ahat[i + j*d]; } - res.cond_Rsk = (double)condition_number(R_sk.data(), n, n); + res.cond_Rsk = (double)RandLAPACK::util::cond_num_check(n, n, R_sk.data(), /*verbose=*/false); // ---------------------------------------------------------------- // Explicit inverses of R_sk via two methods @@ -503,7 +489,7 @@ static TrialResult run_trial( // Per-path metrics // ---------------------------------------------------------------- for (int p = 0; p < N_PATHS; ++p) { - res.cond_Apre[p] = (double)condition_number(Apre[p].data(), m, n); + res.cond_Apre[p] = (double)RandLAPACK::util::cond_num_check(m, n, Apre[p].data(), /*verbose=*/false); res.cond_G[p] = (double)gram_condition_number(Apre[p].data(), m, n); res.orth_Q[p] = (double)cholqr_orth_error(Apre[p], A_dense, m, n, R_sk.data()); } @@ -657,7 +643,7 @@ int run_benchmark(int argc, char* argv[]) { A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); } - T cond_A = condition_number(A_dense.data(), m, n); + T cond_A = RandLAPACK::util::cond_num_check(m, n, A_dense.data(), /*verbose=*/false); std::cout << " cond(A): " << std::scientific << std::setprecision(3) << (double)cond_A << "\n\n"; std::string label = "gen_" + std::to_string(m) + "x" + std::to_string(n) @@ -686,7 +672,7 @@ int run_benchmark(int argc, char* argv[]) { A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); } - T cond_A = condition_number(A_dense.data(), m, n); + T cond_A = RandLAPACK::util::cond_num_check(m, n, A_dense.data(), /*verbose=*/false); int64_t d = (int64_t)std::ceil(d_factor * n); std::cout << "\n=== CQRRT Preconditioner Comparison ===\n"; From 94cf3ac0819d39e105e2b653e2d31dc16342a285 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:00:31 -0700 Subject: [PATCH 27/47] Style fixes in CQRRT benchmarks: lacpy, syrk+symmetrize, drop wrappers - Drop the make_eye(n) and fill_lower_from_upper helpers in cqrrt_bench_common.hh. The 6 make_eye callers now allocate locally and call RandLAPACK::util::eye directly; fill_lower_from_upper had no remaining callers. - gram_condition_number: replace gemm A_pre^T A_pre with syrk (upper) + RandBLAS::symmetrize, since the result is symmetric by construction. - Replace the two hand-rolled upper-triangle extraction loops in the GEQP3 and BQRRP paths of CQRRT_diagnostic with lapack::lacpy(Upper). --- .../bench_CQRRT_linops/CQRRT_diagnostic.cc | 29 ++++++++++--------- .../bench_CQRRT_linops/CQRRT_linop_basic.cc | 6 ++-- .../bench_CQRRT_linops/cqrrt_bench_common.hh | 15 ---------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc index 1a0536e31..b47f787fe 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_diagnostic.cc @@ -122,12 +122,15 @@ static T rel_diff(const T* A, const T* B, int64_t len) { return (na > 0) ? std::sqrt(nd / na) : std::sqrt(nd); } -// Condition number of the Gram matrix G = A_pre^T A_pre. +// Condition number of the Gram matrix G = A_pre^T A_pre. Uses syrk +// (upper triangle) + symmetrize so gesdd inside cond_num_check sees a +// full symmetric matrix. template static T gram_condition_number(const T* A_pre, int64_t m, int64_t n) { std::vector G(n * n, 0.0); - blas::gemm(Layout::ColMajor, Op::Trans, Op::NoTrans, - n, n, m, (T)1.0, A_pre, m, A_pre, m, (T)0.0, G.data(), n); + blas::syrk(Layout::ColMajor, Uplo::Upper, Op::Trans, + n, m, (T)1.0, A_pre, m, (T)0.0, G.data(), n); + RandBLAS::symmetrize(Layout::ColMajor, Uplo::Upper, n, G.data(), n); return RandLAPACK::util::cond_num_check(n, n, G.data(), /*verbose=*/false); } @@ -234,7 +237,8 @@ static TrialResult run_trial( // ---------------------------------------------------------------- // Method A: TRSM on identity (path [2]) - auto R_inv_trsm = make_eye(n); + std::vector R_inv_trsm(n * n, T(0)); + RandLAPACK::util::eye(n, n, R_inv_trsm.data()); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, R_sk.data(), n, R_inv_trsm.data(), n); @@ -261,9 +265,7 @@ static TrialResult run_trial( // Extract upper triangular R_buf before overwriting with Q std::vector R_buf(n * n, 0.0); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_buf[i + j*n] = R_copy[i + j*n]; + lapack::lacpy(MatrixType::Upper, n, n, R_copy.data(), n, R_buf.data(), n); // Expand Q_buf from Householder reflectors (overwrites R_copy) lapack::ungqr(n, n, n, R_copy.data(), n, tau_qr.data()); @@ -322,9 +324,7 @@ static TrialResult run_trial( bqrrp.call(n, n, R_copy.data(), n, (T)1.0, tau_qr.data(), jpiv.data(), state); std::vector R_buf(n * n, 0.0); - for (int64_t j = 0; j < n; ++j) - for (int64_t i = 0; i <= j; ++i) - R_buf[i + j*n] = R_copy[i + j*n]; + lapack::lacpy(MatrixType::Upper, n, n, R_copy.data(), n, R_buf.data(), n); lapack::ungqr(n, n, n, R_copy.data(), n, tau_qr.data()); @@ -450,7 +450,8 @@ static TrialResult run_trial( // ---- Path [2]: CQRRT_linop — TRSM_IDENTITY, linop fwd/adj, TRMM ---- // R_inv via TRSM_IDENTITY: solve X * R_sk_2 = I (upper triangular result) - auto R_inv_2 = make_eye(n); + std::vector R_inv_2(n * n, T(0)); + RandLAPACK::util::eye(n, n, R_inv_2.data()); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, n, n, (T)1.0, R_sk_2.data(), n, R_inv_2.data(), n); for (int64_t j = 0; j < n; ++j) @@ -639,7 +640,8 @@ int run_benchmark(int argc, char* argv[]) { std::vector A_dense(m * n, 0.0); { - auto Eye = make_eye(n); + std::vector Eye(n * n, T(0)); + RandLAPACK::util::eye(n, n, Eye.data()); A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); } @@ -668,7 +670,8 @@ int run_benchmark(int argc, char* argv[]) { std::vector A_dense(m * n, 0.0); { - auto Eye = make_eye(n); + std::vector Eye(n * n, T(0)); + RandLAPACK::util::eye(n, n, Eye.data()); A_linop(Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, A_dense.data(), m); } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc index 233301a28..c3cd70721 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc @@ -68,7 +68,8 @@ template static void compute_Q_from_R( GLO& A_op, T* R, int64_t ldr, T* Q_out, int64_t m, int64_t n) { - auto Eye = make_eye(n); + std::vector Eye(n * n, T(0)); + RandLAPACK::util::eye(n, n, Eye.data()); A_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, m, n, n, (T)1.0, Eye.data(), n, (T)0.0, Q_out, m); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, @@ -222,7 +223,8 @@ static std::vector> run_algorithms( dense_mem.start(); // Step 1: Materialize the operator by multiplying with identity - auto I_mat = make_eye(n); + std::vector I_mat(n * n, T(0)); + RandLAPACK::util::eye(n, n, I_mat.data()); T* A_materialized = new T[m * n](); auto materialize_start = steady_clock::now(); diff --git a/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh b/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh index bea34a1e8..9bf36ebd2 100644 --- a/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh +++ b/benchmark/bench_CQRRT_linops/cqrrt_bench_common.hh @@ -20,18 +20,3 @@ static RandBLAS::sparse_data::csr::CSRMatrix load_csr( return csr; } -// Return an n x n identity matrix (column-major). -template -static std::vector make_eye(int64_t n) { - std::vector I(n * n, T(0)); - RandLAPACK::util::eye(n, n, I.data()); - return I; -} - -// Fill lower triangle of a column-major symmetric n x n matrix from its upper triangle. -template -static void fill_lower_from_upper(T* M, int64_t n) { - for (int64_t j = 0; j < n; ++j) - for (int64_t i = j + 1; i < n; ++i) - M[i + j * n] = M[j + i * n]; -} From 6e05d80cd97208d69b0b2cc7c3cfe107a80dcb74 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:08:21 -0700 Subject: [PATCH 28/47] SparseLU: reuse solve scratch across calls; simplify tests ext_sparselu_linop.hh Replace the per-call Eigen::Matrix X(m, n) allocation with a member T* x_buf_ + int64_t x_buf_size_ buffer, grown on demand. The solve writes through an Eigen::Map over x_buf_ and the alpha/beta scale- and-accumulate into C reads from the same buffer. Destructor frees; copy is now deleted (Eigen members were already non-trivially copyable). test_ext_sparselu_linop.cc - Use setFromTriplets instead of reserve/insert for sparse construction. - apply_A: replace the Eigen::Map dance with a direct InnerIterator sweep over the sparse matrix. - Replace the hand-rolled err/ref accumulator in every test with a shared rel_err() helper that uses blas::nrm2. --- extras/linops/ext_sparselu_linop.hh | 25 +++- extras/test/linops/test_ext_sparselu_linop.cc | 124 ++++++++---------- 2 files changed, 79 insertions(+), 70 deletions(-) diff --git a/extras/linops/ext_sparselu_linop.hh b/extras/linops/ext_sparselu_linop.hh index a1957637f..e678b28ab 100644 --- a/extras/linops/ext_sparselu_linop.hh +++ b/extras/linops/ext_sparselu_linop.hh @@ -53,11 +53,20 @@ struct SparseLUSolverLinOp { randblas_require(n_rows == n_cols); // must be square to invert } + ~SparseLUSolverLinOp() { delete[] x_buf_; } + + SparseLUSolverLinOp(const SparseLUSolverLinOp&) = delete; + SparseLUSolverLinOp& operator=(const SparseLUSolverLinOp&) = delete; + private: using Layout = blas::Layout; using Op = blas::Op; using Side = blas::Side; + // Reused scratch for X = op(A)^{-1} * B; grown on demand in operator(). + T* x_buf_ = nullptr; + int64_t x_buf_size_ = 0; + public: /// Run the sparse LU factorization (analyze + numeric). Idempotent. @@ -117,13 +126,21 @@ public: Eigen::Unaligned, Eigen::OuterStride<>> B_map(B, m, n, Eigen::OuterStride<>(ldb)); - Eigen::Matrix X(m, n); + // Grow x_buf_ to hold m × n (ColMajor) on demand; reuse across calls. + int64_t needed = m * n; + if (needed > x_buf_size_) { + delete[] x_buf_; + x_buf_ = new T[needed]; + x_buf_size_ = needed; + } + Eigen::Map> + X_map(x_buf_, m, n); if (trans_A == Op::NoTrans) { - X.noalias() = lu_solver.solve(B_map); + X_map.noalias() = lu_solver.solve(B_map); } else { // A^{-T} * B - X.noalias() = lu_solver.transpose().solve(B_map); + X_map.noalias() = lu_solver.transpose().solve(B_map); } if (lu_solver.info() != Eigen::Success) { @@ -133,7 +150,7 @@ public: // C := alpha * X + beta * C (in-place column-by-column) for (int64_t j = 0; j < n; ++j) { T* C_col = C + (size_t)j * (size_t)ldc; - const T* X_col = X.data() + (size_t)j * (size_t)m; + const T* X_col = x_buf_ + (size_t)j * (size_t)m; for (int64_t i = 0; i < m; ++i) { C_col[i] = alpha * X_col[i] + beta * C_col[i]; } diff --git a/extras/test/linops/test_ext_sparselu_linop.cc b/extras/test/linops/test_ext_sparselu_linop.cc index 1d8db1076..a74cd5db8 100644 --- a/extras/test/linops/test_ext_sparselu_linop.cc +++ b/extras/test/linops/test_ext_sparselu_linop.cc @@ -3,7 +3,6 @@ #include "../../linops/ext_sparselu_linop.hh" #include -#include #include #include #include @@ -13,39 +12,52 @@ using std::vector; using blas::Layout; using blas::Op; using blas::Side; +using SpMat = Eigen::SparseMatrix; -// Build a small symmetric tridiagonal sparse matrix A of dimension n, -// with chosen diagonal value d and off-diagonal value e. Eigen ColMajor. -Eigen::SparseMatrix build_tridiag(int n, double d, double e) { - Eigen::SparseMatrix A(n, n); - A.reserve(Eigen::VectorXi::Constant(n, 3)); +// Symmetric tridiagonal matrix with diagonal d and off-diagonal e. +static SpMat build_tridiag(int n, double d, double e) { + std::vector> t; + t.reserve(3 * n); for (int i = 0; i < n; ++i) { - A.insert(i, i) = d; + t.emplace_back(i, i, d); if (i + 1 < n) { - A.insert(i, i + 1) = e; - A.insert(i + 1, i) = e; + t.emplace_back(i, i + 1, e); + t.emplace_back(i + 1, i, e); } } + SpMat A(n, n); + A.setFromTriplets(t.begin(), t.end()); A.makeCompressed(); return A; } -// Reference: compute A * v (dense GEMV) for verification. -void apply_A(const Eigen::SparseMatrix& A, - const vector& v, vector& out) -{ +// out := A * v. +static void apply_A(const SpMat& A, const vector& v, vector& out) { int n = (int)A.rows(); out.assign(n, 0.0); - Eigen::Map v_map(v.data(), n); - Eigen::Map out_map(out.data(), n); - out_map = A * v_map; + for (int j = 0; j < A.outerSize(); ++j) { + for (SpMat::InnerIterator it(A, j); it; ++it) { + out[it.row()] += it.value() * v[it.col()]; + } + } +} + + +// Relative 2-norm error ||u - ref|| / ||ref||. +static double rel_err(const vector& u, const vector& ref) { + int n = (int)u.size(); + vector diff(n); + for (int i = 0; i < n; ++i) diff[i] = u[i] - ref[i]; + double dn = blas::nrm2(n, diff.data(), 1); + double rn = blas::nrm2(n, ref.data(), 1); + return dn / rn; } TEST(TestSparseLUSolverLinOp, spd_tridiag_inverse) { - // SPD case: d=2, e=-1 makes the 1D Laplacian (SPD). + // SPD case: d=2, e=-1 is the 1D Laplacian. int n = 8; auto A = build_tridiag(n, 2.0, -1.0); @@ -53,40 +65,32 @@ TEST(TestSparseLUSolverLinOp, spd_tridiag_inverse) { A_inv.factorize(); vector b(n); - for (int i = 0; i < n; ++i) b[i] = (double)(i + 1); // pick a known b + for (int i = 0; i < n; ++i) b[i] = (double)(i + 1); vector x(n, 0.0); A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, n, 1, n, 1.0, b.data(), n, 0.0, x.data(), n); - // Verify A x ≈ b vector Ax(n); apply_A(A, x, Ax); - double err = 0, ref = 0; - for (int i = 0; i < n; ++i) { - double d = Ax[i] - b[i]; - err += d * d; - ref += b[i] * b[i]; - } - ASSERT_LE(std::sqrt(err / ref), 1e-12); + ASSERT_LE(rel_err(Ax, b), 1e-12); } TEST(TestSparseLUSolverLinOp, indefinite_inverse) { - // Indefinite case: d=-1 makes A = -I + tridiag(e, -1, e) which can be indefinite. - // More directly: build an explicitly indefinite matrix. + // Indefinite: diagonal sign-changing, small off-diagonal. int n = 6; - // Diagonal [1, -2, 3, -4, 5, -6] with off-diagonal 0.1 - Eigen::SparseMatrix A(n, n); - A.reserve(Eigen::VectorXi::Constant(n, 3)); - double diag_vals[6] = {1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + double d[6] = {1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + std::vector> t; for (int i = 0; i < n; ++i) { - A.insert(i, i) = diag_vals[i]; + t.emplace_back(i, i, d[i]); if (i + 1 < n) { - A.insert(i, i + 1) = 0.1; - A.insert(i + 1, i) = 0.1; + t.emplace_back(i, i + 1, 0.1); + t.emplace_back(i + 1, i, 0.1); } } + SpMat A(n, n); + A.setFromTriplets(t.begin(), t.end()); A.makeCompressed(); RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); @@ -95,32 +99,27 @@ TEST(TestSparseLUSolverLinOp, indefinite_inverse) { for (int i = 0; i < n; ++i) b[i] = std::sin((double)i); vector x(n, 0.0); - // Lazy factorization: don't call factorize() explicitly. + // Lazy factorization — first apply triggers factorize(). A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, n, 1, n, 1.0, b.data(), n, 0.0, x.data(), n); vector Ax(n); apply_A(A, x, Ax); - double err = 0, ref = 0; - for (int i = 0; i < n; ++i) { - double d = Ax[i] - b[i]; - err += d * d; - ref += b[i] * b[i]; - } - ASSERT_LE(std::sqrt(err / ref), 1e-12); + ASSERT_LE(rel_err(Ax, b), 1e-12); } TEST(TestSparseLUSolverLinOp, transpose_inverse) { - // Use a NON-symmetric matrix so A^{-T} != A^{-1}, to actually exercise trans dispatch. + // Non-symmetric (upper != lower) so A^{-T} != A^{-1}. int n = 5; - Eigen::SparseMatrix A(n, n); - A.reserve(Eigen::VectorXi::Constant(n, 3)); + std::vector> t; for (int i = 0; i < n; ++i) { - A.insert(i, i) = (double)(i + 2); - if (i + 1 < n) A.insert(i, i + 1) = 1.0; // upper off-diag - if (i > 0) A.insert(i, i - 1) = 0.3; // lower off-diag (different value) + t.emplace_back(i, i, (double)(i + 2)); + if (i + 1 < n) t.emplace_back(i, i + 1, 1.0); + if (i > 0) t.emplace_back(i, i - 1, 0.3); } + SpMat A(n, n); + A.setFromTriplets(t.begin(), t.end()); A.makeCompressed(); RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); @@ -132,23 +131,17 @@ TEST(TestSparseLUSolverLinOp, transpose_inverse) { A_inv(Side::Left, Layout::ColMajor, Op::Trans, Op::NoTrans, n, 1, n, 1.0, b.data(), n, 0.0, x_trans.data(), n); - // Verify A^T * x_trans ≈ b (i.e., x_trans = A^{-T} * b) - Eigen::SparseMatrix A_T = A.transpose(); + // x_trans = A^{-T} b ⇒ A^T x_trans = b. + SpMat A_T = A.transpose(); vector AT_x(n); apply_A(A_T, x_trans, AT_x); - double err = 0, ref = 0; - for (int i = 0; i < n; ++i) { - double d = AT_x[i] - b[i]; - err += d * d; - ref += b[i] * b[i]; - } - ASSERT_LE(std::sqrt(err / ref), 1e-12); + ASSERT_LE(rel_err(AT_x, b), 1e-12); } TEST(TestSparseLUSolverLinOp, multi_rhs_with_alpha_beta) { int n = 6; - auto A = build_tridiag(n, 4.0, -1.0); // SPD + auto A = build_tridiag(n, 4.0, -1.0); RandLAPACK_extras::linops::SparseLUSolverLinOp A_inv(A); int n_rhs = 3; @@ -156,20 +149,19 @@ TEST(TestSparseLUSolverLinOp, multi_rhs_with_alpha_beta) { for (int i = 0; i < n * n_rhs; ++i) B[i] = std::cos((double)i * 0.5); const double alpha = 2.0, beta = 1.5; - vector C(n * n_rhs, 7.0); // initial C + vector C(n * n_rhs, 7.0); vector C_initial = C; A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, n, n_rhs, n, alpha, B.data(), n, beta, C.data(), n); - // Reference: C_ref = alpha * A^{-1} * B + beta * C_initial. - // Compute A^{-1} * B column-by-column with a fresh solver call. + // Reference: alpha * A^{-1} B + beta * C_initial. vector Ainv_B(n * n_rhs, 0.0); A_inv(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, n, n_rhs, n, 1.0, B.data(), n, 0.0, Ainv_B.data(), n); - for (int i = 0; i < n * n_rhs; ++i) { - double expected = alpha * Ainv_B[i] + beta * C_initial[i]; - ASSERT_NEAR(C[i], expected, 1e-12); - } + vector expected(n * n_rhs); + for (int i = 0; i < n * n_rhs; ++i) + expected[i] = alpha * Ainv_B[i] + beta * C_initial[i]; + ASSERT_LE(rel_err(C, expected), 1e-12); } From 132ca465ccc5c13f7f320e550d476eaa78c88fc5 Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:17:11 -0700 Subject: [PATCH 29/47] cholqr_primitive: internalize scratch allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the caller-owned G (n×n) and A_temp (m×b_eff) workspace arguments from cholqr_primitive; the primitive now allocates and frees them itself. The three callers (CholQR_linops, sCholQR3_linops, sCholQR3_linops_basic) drop their explicit alloc/free for these two buffers; the timing accumulator alloc_dur for them goes to zero (other per-driver allocations remain). pcholqr_primitive's caller-owned scratch is unchanged. --- RandLAPACK/comps/rl_cholqr.hh | 22 +++++++++++++++------- RandLAPACK/drivers/rl_cholqr_linops.hh | 11 ----------- RandLAPACK/drivers/rl_scholqr3_linops.hh | 2 -- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/RandLAPACK/comps/rl_cholqr.hh b/RandLAPACK/comps/rl_cholqr.hh index b60e63c5d..277de4c91 100644 --- a/RandLAPACK/comps/rl_cholqr.hh +++ b/RandLAPACK/comps/rl_cholqr.hh @@ -141,10 +141,10 @@ void blocked_preconditioned_gram( // iter-1 spec wants s = 11 * eps * n * ||A||_F^2 so the caller // passes shift_factor = 11 * eps * n. Pass 0 for unshifted CholQR. // -// Workspaces (caller-owned): -// R — n × n output buffer (lower triangle returned zeroed) -// G — n × n Gram scratch -// A_temp — m × b_eff +// Output: +// R — n × n upper-triangular (lower triangle returned zeroed). +// +// Workspaces (G n×n, A_temp m×b_eff) are allocated and freed internally. // // Returns potrf info (0 on success; >0 on Cholesky breakdown). template @@ -153,8 +153,6 @@ int cholqr_primitive( T* R, int64_t ldr, T shift_factor, int64_t block_size, - T* G, - T* A_temp, long& fwd_us, long& adj_us, long& chol_us, bool timing) { @@ -165,6 +163,9 @@ int cholqr_primitive( int64_t n = A.n_cols; int64_t b_eff = (block_size > 0 && block_size < n) ? block_size : n; + T* G = new T[n * n](); + T* A_temp = new T[m * b_eff]; + long gemm_unused = 0; blocked_preconditioned_gram(A, (const T*)nullptr, G, m, n, b_eff, A_temp, (T*)nullptr, @@ -184,7 +185,11 @@ int cholqr_primitive( if (n > 1) lapack::laset(MatrixType::Lower, n - 1, n - 1, T(0), T(0), &G[1], n); int info = lapack::potrf(Uplo::Upper, n, G, n); - if (info) return info; + if (info) { + delete[] G; + delete[] A_temp; + return info; + } lapack::lacpy(MatrixType::Upper, n, n, G, n, R, ldr); if (n > 1) @@ -194,6 +199,9 @@ int cholqr_primitive( t1 = steady_clock::now(); chol_us = duration_cast(t1 - t0).count(); } + + delete[] G; + delete[] A_temp; return 0; } diff --git a/RandLAPACK/drivers/rl_cholqr_linops.hh b/RandLAPACK/drivers/rl_cholqr_linops.hh index b0dabfc74..045f1a4b6 100644 --- a/RandLAPACK/drivers/rl_cholqr_linops.hh +++ b/RandLAPACK/drivers/rl_cholqr_linops.hh @@ -80,22 +80,13 @@ class CholQR_linops { int64_t b_eff = (this->block_size > 0 && this->block_size < n) ? this->block_size : n; - // Allocate workspaces for the primitive: G (n×n) and A_temp (m×b_eff). - if (this->timing) t0 = steady_clock::now(); - T* G = new T[n * n](); - T* A_temp = new T[m * b_eff]; - if (this->timing) { t1 = steady_clock::now(); alloc_dur = duration_cast(t1 - t0).count(); } - int info = cholqr_primitive( A, R, ldr, /*shift_factor=*/T(0), this->block_size, - G, A_temp, fwd_dur, adj_dur, chol_dur, this->timing); if (info != 0) { - delete[] G; - delete[] A_temp; return info; } @@ -134,8 +125,6 @@ class CholQR_linops { this->times = {alloc_dur, fwd_dur, adj_dur, chol_dur, rest_dur, total_dur}; } - delete[] G; - delete[] A_temp; return 0; } }; diff --git a/RandLAPACK/drivers/rl_scholqr3_linops.hh b/RandLAPACK/drivers/rl_scholqr3_linops.hh index 296374fa6..c90681cf9 100644 --- a/RandLAPACK/drivers/rl_scholqr3_linops.hh +++ b/RandLAPACK/drivers/rl_scholqr3_linops.hh @@ -112,7 +112,6 @@ class sCholQR3_linops { A, R, ldr, shift_factor_iter1, this->block_size, - G, A_temp, fwd1_dur, adj1_dur, chol1_dur, this->timing); if (info != 0) { delete[] G; delete[] R_pre; delete[] P_prev; @@ -303,7 +302,6 @@ class sCholQR3_linops_basic { A, R, ldr, shift_factor_iter1, /*block_size=*/0, // basic variant is non-blocked - G, Q_buf, // re-use Q_buf as the m × b_eff scratch for cholqr_primitive fwd1_dur, adj1_dur, chol1_dur, this->timing); if (info != 0) { delete[] Q_buf; delete[] G; delete[] M; From fd08005475b0fb38f5ee0a480b73a56b83ddb7db Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:43:55 -0700 Subject: [PATCH 30/47] Replace std::vector workspaces with raw T* (IR-LSQ + applications bench) IR-LSQ (rl_iter_refine_lsq.hh) Replace the 10 std::vector workspaces in IterRefineLSQ::call (r, g, c, z, dx, cg_r, cg_p, cg_Mp, tmp_n, tmp_m) with raw T* + a single free_workspace() cleanup lambda invoked on both return paths. Applications benchmark (CQRRT_linop_applications.cc) - Hoist the per-(alg, run) QR-output buffer R, plus the four IR-LSQ sketch-and-solve buffers (SA, Sb, x_ls, Ax), out of both loops and into raw T* allocations at function entry; zero-fill R per iter to match the prior std::vector R(n*n, 0) behavior. Apply the same pattern to the warmup R_warm and to the rspec R buffer. - Replace std::vector with raw T* in compute_AtA_blocked (E_block, A_block, AtA_block), estimate_op_2norm (v, Av), and compute_orth_error_memlite (X). Output-state vectors (bench_result lists, top_eigvals/top_residuals, selected_algs, run_states, ir.times / inner_iters_per_step, and one-shot RHS construction at function boundaries) keep their std::vector type per the project rule that small fixed-size scratch / containers are fine. --- RandLAPACK/drivers/rl_iter_refine_lsq.hh | 54 ++++---- .../CQRRT_linop_applications.cc | 120 ++++++++++-------- 2 files changed, 99 insertions(+), 75 deletions(-) diff --git a/RandLAPACK/drivers/rl_iter_refine_lsq.hh b/RandLAPACK/drivers/rl_iter_refine_lsq.hh index c09b8ec01..6b3975c1b 100644 --- a/RandLAPACK/drivers/rl_iter_refine_lsq.hh +++ b/RandLAPACK/drivers/rl_iter_refine_lsq.hh @@ -127,16 +127,22 @@ struct IterRefineLSQ { inner_iters_per_step.clear(); outer_iters_done = 0; - std::vector r(m); // residual - std::vector g(n); // J^T r - std::vector c(n); // R^{-T} g - std::vector z(n); // inner-solve output - std::vector dx(n); // R^{-1} z - - // CG workspaces (allocated once, reused across outer steps) - std::vector cg_r(n), cg_p(n), cg_Mp(n); - // Inside-M-apply workspaces - std::vector tmp_n(n), tmp_m(m); + // Per-call workspace buffers. Allocated once up front, reused across outer steps. + T* r = new T[m](); // residual + T* g = new T[n](); // J^T r + T* c = new T[n](); // R^{-T} g + T* z = new T[n](); // inner-solve output + T* dx = new T[n](); // R^{-1} z + T* cg_r = new T[n](); // CG residual + T* cg_p = new T[n](); // CG search direction + T* cg_Mp = new T[n](); // M * p inside CG + T* tmp_n = new T[n](); // R^{-1} p scratch + T* tmp_m = new T[m](); // J * v scratch (m-length) + auto free_workspace = [&]() { + delete[] r; delete[] g; delete[] c; delete[] z; delete[] dx; + delete[] cg_r; delete[] cg_p; delete[] cg_Mp; + delete[] tmp_n; delete[] tmp_m; + }; T b_norm = blas::nrm2(m, b, 1); if (b_norm == (T)0) b_norm = (T)1; // avoid div-by-zero in residual reporting @@ -146,13 +152,13 @@ struct IterRefineLSQ { // tmp_m = J*x auto t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); + m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m, m); t_outer_fwd += std::chrono::duration_cast(clock::now() - t0).count(); // r = b - tmp_m for (int64_t i = 0; i < m; ++i) r[i] = b[i] - tmp_m[i]; - T r_norm = blas::nrm2(m, r.data(), 1); + T r_norm = blas::nrm2(m, r, 1); if (verbose) { std::printf("[IR-LSQ] step %d: ||r||/||b|| = %.4e\n", step, (double)(r_norm / b_norm)); } @@ -160,24 +166,22 @@ struct IterRefineLSQ { // g = J^T r t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, 1, m, (T)1.0, r.data(), m, (T)0.0, g.data(), n); + n, 1, m, (T)1.0, r, m, (T)0.0, g, n); t_outer_adj += std::chrono::duration_cast(clock::now() - t0).count(); // c = R^{-T} g (in-place TRSM on a copy of g) - std::copy(g.begin(), g.end(), c.begin()); + std::copy(g, g + n, c); t0 = clock::now(); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, blas::Diag::NonUnit, - n, 1, (T)1.0, R, ldr, c.data(), n); + n, 1, (T)1.0, R, ldr, c, n); t_outer_trsm += std::chrono::duration_cast(clock::now() - t0).count(); // Inner CG on M*z = c int inner_iters = 0; auto t_in0 = clock::now(); - int cg_status = inner_cg(J, R, ldr, c.data(), n, m, - z.data(), - cg_r.data(), cg_p.data(), cg_Mp.data(), - tmp_n.data(), tmp_m.data(), + int cg_status = inner_cg(J, R, ldr, c, n, m, + z, cg_r, cg_p, cg_Mp, tmp_n, tmp_m, inner_iters, t_inner_trsm, t_inner_fwd, t_inner_adj); t_inner_total += std::chrono::duration_cast(clock::now() - t_in0).count(); inner_iters_per_step.push_back(inner_iters); @@ -187,19 +191,20 @@ struct IterRefineLSQ { if (timing) populate_times(outer_start, t_inner_total, t_outer_trsm, t_outer_fwd, t_outer_adj, t_inner_trsm, t_inner_fwd, t_inner_adj); + free_workspace(); return cg_status; } // dx = R^{-1} z - std::copy(z.begin(), z.end(), dx.begin()); + std::copy(z, z + n, dx); t0 = clock::now(); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, - n, 1, (T)1.0, R, ldr, dx.data(), n); + n, 1, (T)1.0, R, ldr, dx, n); t_outer_trsm += std::chrono::duration_cast(clock::now() - t0).count(); // x ← x + dx - blas::axpy(n, (T)1.0, dx.data(), 1, x, 1); + blas::axpy(n, (T)1.0, dx, 1, x, 1); outer_iters_done = step + 1; } @@ -207,15 +212,16 @@ struct IterRefineLSQ { { auto t0 = clock::now(); J(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m.data(), m); + m, 1, n, (T)1.0, x, n, (T)0.0, tmp_m, m); t_outer_fwd += std::chrono::duration_cast(clock::now() - t0).count(); for (int64_t i = 0; i < m; ++i) tmp_m[i] = b[i] - tmp_m[i]; - final_residual_norm = blas::nrm2(m, tmp_m.data(), 1) / b_norm; + final_residual_norm = blas::nrm2(m, tmp_m, 1) / b_norm; } if (timing) populate_times(outer_start, t_inner_total, t_outer_trsm, t_outer_fwd, t_outer_adj, t_inner_trsm, t_inner_fwd, t_inner_adj); + free_workspace(); return 0; } diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc index ab27bd38f..8040bd3df 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_applications.cc @@ -106,47 +106,54 @@ struct bench_result { template static void compute_AtA_blocked(GLO& A_op, int64_t m, int64_t n, T* AtA, int64_t b) { std::fill(AtA, AtA + n * n, (T)0.0); - std::vector E_block(n * b, 0.0); - std::vector A_block(m * b, 0.0); - std::vector AtA_block(n * b, 0.0); + T* E_block = new T[n * b](); + T* A_block = new T[m * b]; + T* AtA_block = new T[n * b]; for (int64_t j0 = 0; j0 < n; j0 += b) { int64_t bk = std::min(b, n - j0); - std::fill(E_block.begin(), E_block.end(), (T)0.0); + std::fill(E_block, E_block + n * b, (T)0.0); for (int64_t j = 0; j < bk; ++j) E_block[(j0 + j) + j * n] = (T)1.0; A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, bk, n, (T)1.0, E_block.data(), n, (T)0.0, A_block.data(), m); + m, bk, n, (T)1.0, E_block, n, (T)0.0, A_block, m); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, bk, m, (T)1.0, A_block.data(), m, (T)0.0, AtA_block.data(), n); + n, bk, m, (T)1.0, A_block, m, (T)0.0, AtA_block, n); - lapack::lacpy(lapack::MatrixType::General, n, bk, AtA_block.data(), n, AtA + j0 * n, n); + lapack::lacpy(lapack::MatrixType::General, n, bk, AtA_block, n, AtA + j0 * n, n); } + + delete[] E_block; + delete[] A_block; + delete[] AtA_block; } // Estimate ||A||_2 via power iteration on A^T A. O(iters * (m+n) memory) — no materialization. template static T estimate_op_2norm(GLO& A_op, int64_t m, int64_t n, int iters = 10) { - std::vector v(n), Av(m); + T* v = new T[n]; + T* Av = new T[m]; { std::mt19937 rng(7); std::normal_distribution N01(0, 1); - for (auto& x : v) x = N01(rng); + for (int64_t i = 0; i < n; ++i) v[i] = N01(rng); } T sigma = (T)0; for (int it = 0; it < iters; ++it) { - T nv = blas::nrm2(n, v.data(), 1); - if (nv == 0) return (T)0; - blas::scal(n, (T)1.0 / nv, v.data(), 1); + T nv = blas::nrm2(n, v, 1); + if (nv == 0) { delete[] v; delete[] Av; return (T)0; } + blas::scal(n, (T)1.0 / nv, v, 1); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, v.data(), n, (T)0.0, Av.data(), m); - sigma = blas::nrm2(m, Av.data(), 1); + m, 1, n, (T)1.0, v, n, (T)0.0, Av, m); + sigma = blas::nrm2(m, Av, 1); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::Trans, blas::Op::NoTrans, - n, 1, m, (T)1.0, Av.data(), m, (T)0.0, v.data(), n); + n, 1, m, (T)1.0, Av, m, (T)0.0, v, n); } + delete[] v; + delete[] Av; return sigma; } @@ -156,16 +163,17 @@ static T estimate_op_2norm(GLO& A_op, int64_t m, int64_t n, int iters = 10) { template static T compute_orth_error_memlite(GLO& A_op, const T* R, int64_t m, int64_t n, int64_t block_size) { int64_t b = (block_size > 0) ? block_size : 256; - std::vector X(n * n, (T)0); - compute_AtA_blocked(A_op, m, n, X.data(), b); + T* X = new T[n * n](); + compute_AtA_blocked(A_op, m, n, X, b); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, - blas::Op::Trans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X.data(), n); + blas::Op::Trans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X, n); blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, - blas::Op::NoTrans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X.data(), n); + blas::Op::NoTrans, blas::Diag::NonUnit, n, n, (T)1.0, R, n, X, n); for (int64_t i = 0; i < n; ++i) X[i + i * n] -= (T)1.0; T s = 0; #pragma omp parallel for reduction(+:s) schedule(static) for (int64_t i = 0; i < n * n; ++i) s += X[i] * X[i]; + delete[] X; return std::sqrt(s) / std::sqrt((T)n); } @@ -386,11 +394,12 @@ static int run_benchmark_inner( std::cout << "Running warmup... " << std::flush; { auto warm_state = run_states[0]; - std::vector R_warm(n * n, (T)0); + T* R_warm = new T[n * n](); RandLAPACK::CQRRT_linops warm_algo(false, tol, false); warm_algo.nnz = sketch_nnz; warm_algo.block_size = block_size; - warm_algo.call(A_op, R_warm.data(), n, d_factor, warm_state); + warm_algo.call(A_op, R_warm, n, d_factor, warm_state); + delete[] R_warm; } std::cout << "done\n\n"; @@ -409,6 +418,14 @@ static int run_benchmark_inner( std::vector> all_results; + // Per-iteration workspaces, hoisted once: invariant sizes across all (alg, run) iters. + const int64_t d_init = (int64_t)(d_factor * (T)n); + T* R = new T[n * n](); // QR output; zero-filled per iter to match prior behavior + T* SA = new T[d_init * n]; // S2 * A (sketch-and-solve LHS); overwritten beta=0 + T* Sb = new T[d_init]; // S2 * b; overwritten beta=0 + T* x_ls = new T[n]; // initial guess + refined solution + T* Ax = new T[m]; // A * x_ls for residual; overwritten beta=0 + // ================================================================ // Per-(method, run) loop // ================================================================ @@ -434,7 +451,7 @@ static int run_benchmark_inner( res.peak_rss_kb = 0; res.analytical_kb = 0; - std::vector R(n * n, (T)0); + std::fill(R, R + n * n, (T)0); auto state = run_states[run_idx]; // ---- QR dispatch (5-way, lifted verbatim from CQRRT_linop_irlsq.cc) ---- @@ -443,7 +460,7 @@ static int run_benchmark_inner( if (alg_name == "sCholQR3") { RandLAPACK::sCholQR3_linops qr_algo(/*time_subroutines=*/true, tol); qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(A_op, R.data(), n); + res.qr_status = qr_algo.call(A_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[17]; @@ -452,7 +469,7 @@ static int run_benchmark_inner( } } else if (alg_name == "sCholQR3_basic") { RandLAPACK::sCholQR3_linops_basic qr_algo(/*time_subroutines=*/true, tol); - res.qr_status = qr_algo.call(A_op, R.data(), n); + res.qr_status = qr_algo.call(A_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[14]; @@ -462,7 +479,7 @@ static int run_benchmark_inner( } else if (alg_name == "CholQR") { RandLAPACK::CholQR_linops qr_algo(/*time_subroutines=*/true, tol); qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(A_op, R.data(), n); + res.qr_status = qr_algo.call(A_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[5]; @@ -478,7 +495,7 @@ static int run_benchmark_inner( qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; else /* CQRRT_linop_bqrrp */ qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; - res.qr_status = qr_algo.call(A_op, R.data(), n, d_factor, state); + res.qr_status = qr_algo.call(A_op, R, n, d_factor, state); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[10]; @@ -504,7 +521,7 @@ static int run_benchmark_inner( std::cout << "done (" << res.qr_time_us << " us)"; // ---- Orth_error: ||Q^T Q - I||_F / sqrt(n), blocked compute. Runs for every method. ---- - res.orth_error = compute_orth_error_memlite(A_op, R.data(), m, n, block_size); + res.orth_error = compute_orth_error_memlite(A_op, R, m, n, block_size); // ---- IR-LSQ post-processing ---- { @@ -514,39 +531,35 @@ static int run_benchmark_inner( // Sketch-and-solve initial guess x_0 (Alg. 1 line 3 of Epperly–Meier–Nakatsukasa 2025); // fresh sparse sketch S_2 independent of CQRRT's S_1. - const int64_t d_init = (int64_t)(d_factor * (T)n); RandBLAS::SparseDist DS_init(d_init, m, sketch_nnz); auto x0_state = state; x0_state.key.incr(0xA1B2C3D4u); RandBLAS::SparseSkOp S2(DS_init, x0_state); RandBLAS::fill_sparse(S2); - std::vector SA(d_init * n, (T)0); A_op(blas::Side::Right, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - d_init, n, m, (T)1.0, S2, (T)0.0, SA.data(), d_init); + d_init, n, m, (T)1.0, S2, (T)0.0, SA, d_init); - std::vector Sb(d_init, (T)0); RandBLAS::sketch_general(blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, d_init, (int64_t)1, m, (T)1.0, - S2, b.data(), m, (T)0.0, Sb.data(), d_init); + S2, b.data(), m, (T)0.0, Sb, d_init); - std::vector x_ls(n, (T)0); blas::gemv(blas::Layout::ColMajor, blas::Op::Trans, d_init, n, - (T)1.0, SA.data(), d_init, Sb.data(), 1, - (T)0.0, x_ls.data(), 1); + (T)1.0, SA, d_init, Sb, 1, + (T)0.0, x_ls, 1); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, blas::Diag::NonUnit, n, 1, - (T)1.0, R.data(), n, x_ls.data(), n); + (T)1.0, R, n, x_ls, n); blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, n, 1, - (T)1.0, R.data(), n, x_ls.data(), n); + (T)1.0, R, n, x_ls, n); RandLAPACK::IterRefineLSQ ir(/*tol=*/tol, /*max_inner=*/200, /*n_steps=*/2, /*timing=*/true, /*verbose=*/false); - int ir_status = ir.call(A_op, R.data(), n, b.data(), m, x_ls.data(), n); + int ir_status = ir.call(A_op, R, n, b.data(), m, x_ls, n); auto ls_t1 = steady_clock::now(); if (ir_status != 0) { std::cerr << "Warning: IterRefineLSQ status " << ir_status << " (CG breakdown)\n"; @@ -561,14 +574,13 @@ static int run_benchmark_inner( // Higham normwise backward-error metric: // ls_residual_norm = ||A x - b|| / (||A||_2 * ||x|| + ||b||) // Drivable to machine epsilon for a backward-stable LS solver. - std::vector Ax(m, (T)0); A_op(blas::Side::Left, blas::Layout::ColMajor, blas::Op::NoTrans, blas::Op::NoTrans, - m, 1, n, (T)1.0, x_ls.data(), n, (T)0.0, Ax.data(), m); + m, 1, n, (T)1.0, x_ls, n, (T)0.0, Ax, m); T resid_sq = 0; #pragma omp parallel for reduction(+:resid_sq) schedule(static) for (int64_t i = 0; i < m; ++i) { T d = Ax[i] - b[i]; resid_sq += d * d; } T resid_norm = std::sqrt(resid_sq); - T x_norm = blas::nrm2(n, x_ls.data(), 1); + T x_norm = blas::nrm2(n, x_ls, 1); T denom = A_2norm * x_norm + b_norm; res.ls_residual_norm = (denom > 0) ? resid_norm / denom : (T)-1.0; @@ -606,6 +618,7 @@ static int run_benchmark_inner( write_irlsq_breakdown(breakdown_file, all_results); std::cout << "IR-LSQ breakdown written to " << breakdown_file << "\n"; + delete[] R; delete[] SA; delete[] Sb; delete[] x_ls; delete[] Ax; return 0; } @@ -658,16 +671,20 @@ static int run_rspec_benchmark( std::cout << "Running rspec warmup... " << std::flush; { auto warm_state = run_states[0]; - std::vector R_warm(n * n, (T)0); + T* R_warm = new T[n * n](); RandLAPACK::CQRRT_linops warm_algo(false, tol, false); warm_algo.nnz = sketch_nnz; warm_algo.block_size = block_size; - warm_algo.call(V_app_op, R_warm.data(), n, d_factor, warm_state); + warm_algo.call(V_app_op, R_warm, n, d_factor, warm_state); + delete[] R_warm; } std::cout << "done\n\n"; std::vector> all_results; + // Per-iteration QR output; invariant size across all (alg, run) iters. + T* R = new T[n * n](); + for (const auto& alg_name : selected_algs) { std::cout << "\n=== Algorithm: " << alg_name << " (rspec) ===\n"; @@ -686,7 +703,7 @@ static int run_rspec_benchmark( auto rspec_t0 = steady_clock::now(); - std::vector R(n * n, (T)0); + std::fill(R, R + n * n, (T)0); auto state = run_states[run_idx]; std::cout << "[Run " << run_idx << ", " << alg_name << "] PCholQR ... " << std::flush; @@ -694,7 +711,7 @@ static int run_rspec_benchmark( if (alg_name == "sCholQR3") { RandLAPACK::sCholQR3_linops qr_algo(true, tol); qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.qr_status = qr_algo.call(V_app_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[17]; @@ -702,7 +719,7 @@ static int run_rspec_benchmark( } } else if (alg_name == "sCholQR3_basic") { RandLAPACK::sCholQR3_linops_basic qr_algo(true, tol); - res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.qr_status = qr_algo.call(V_app_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[14]; @@ -711,7 +728,7 @@ static int run_rspec_benchmark( } else if (alg_name == "CholQR") { RandLAPACK::CholQR_linops qr_algo(true, tol); qr_algo.block_size = block_size; - res.qr_status = qr_algo.call(V_app_op, R.data(), n); + res.qr_status = qr_algo.call(V_app_op, R, n); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[5]; @@ -725,7 +742,7 @@ static int run_rspec_benchmark( qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::TRSM_IDENTITY; else qr_algo.precond_method = RandLAPACK::CQRRTLinopPrecond::BQRRP; - res.qr_status = qr_algo.call(V_app_op, R.data(), n, d_factor, state); + res.qr_status = qr_algo.call(V_app_op, R, n, d_factor, state); res.peak_rss_kb = mem.stop(); if (res.qr_status == 0) { res.qr_time_us = qr_algo.times[10]; @@ -795,11 +812,11 @@ static int run_rspec_benchmark( // Apply R^{-T} on the left: T := R^{-T} * T blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::Trans, blas::Diag::NonUnit, - n, n, (T)1.0, R.data(), n, T_mat, n); + n, n, (T)1.0, R, n, T_mat, n); // Apply R^{-1} on the right: T := T * R^{-1} blas::trsm(blas::Layout::ColMajor, blas::Side::Right, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, - n, n, (T)1.0, R.data(), n, T_mat, n); + n, n, (T)1.0, R, n, T_mat, n); // Symmetrize: T := (T + T^T)/2 for (int64_t j = 0; j < n; ++j) { @@ -860,7 +877,7 @@ static int run_rspec_benchmark( // R^{-1} * u_blk blas::trsm(blas::Layout::ColMajor, blas::Side::Left, blas::Uplo::Upper, blas::Op::NoTrans, blas::Diag::NonUnit, - n, top_k, (T)1.0, R.data(), n, u_blk, n); + n, top_k, (T)1.0, R, n, u_blk, n); // v_blk = V_app_op * (R^{-1} u_blk) (m x top_k) T* v_blk = new T[(size_t)m * (size_t)top_k](); @@ -933,6 +950,7 @@ static int run_rspec_benchmark( K_file, M_file, V_file, omega, power_j, sketch_nnz, block_size, method_mask, top_k); std::cout << "\nRSPEC results written to " << results_file << "\n"; + delete[] R; return 0; } From 5ecbe1f6617a3cfa1d451333d3349f20b729dbdb Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:50:47 -0700 Subject: [PATCH 31/47] Replace std::vector workspaces with raw T* in CQRRT_linop_basic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compute_Q_from_R helper: Eye buffer → raw T*. - run_algorithms hot loops: Q_uniform (function-level reusable Q buffer), R_rss (CQRRT/CholQR RSS scopes), R_cqrrt/R_cholqr/R_scholqr3/R_dense (per-algorithm per-run QR output), I_mat → raw T* allocated outside the inner loops and zero-filled per iter where needed. Q_uniform freed at function exit. Container vectors (run_states, results, sizes) and breakdown output state retain std::vector per the project convention for small/fixed-size containers. --- .../bench_CQRRT_linops/CQRRT_linop_basic.cc | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc index c3cd70721..5dd88633e 100644 --- a/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc +++ b/benchmark/bench_CQRRT_linops/CQRRT_linop_basic.cc @@ -68,12 +68,13 @@ template static void compute_Q_from_R( GLO& A_op, T* R, int64_t ldr, T* Q_out, int64_t m, int64_t n) { - std::vector Eye(n * n, T(0)); - RandLAPACK::util::eye(n, n, Eye.data()); + T* Eye = new T[n * n](); + RandLAPACK::util::eye(n, n, Eye); A_op(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, Eye.data(), n, (T)0.0, Q_out, m); + m, n, n, (T)1.0, Eye, n, (T)0.0, Q_out, m); blas::trsm(Layout::ColMajor, Side::Right, Uplo::Upper, Op::NoTrans, Diag::NonUnit, m, n, (T)1.0, R, ldr, Q_out, m); + delete[] Eye; } // Core algorithm runner: operates on a pre-constructed SparseLinOp. @@ -105,7 +106,7 @@ static std::vector> run_algorithms( T tol = std::pow(std::numeric_limits::epsilon(), 0.85); // Single reusable Q buffer for uniform Q = A * R^{-1} computation across all algorithms - std::vector Q_uniform(m * n); + T* Q_uniform = new T[m * n]; // ============================================================ // Run CQRRT (preconditioned Cholesky QR) - multiple runs @@ -116,36 +117,39 @@ static std::vector> run_algorithms( // RSS measurement (test_mode=false) long cqrrt_peak_rss_kb = 0; { - std::vector R_rss(n * n, 0.0); + T* R_rss = new T[n * n](); auto state_rss = run_states[0]; RandLAPACK::CQRRT_linops CQRRT_rss(false, tol, false); CQRRT_rss.nnz = sketch_nnz; CQRRT_rss.block_size = block_size; RandLAPACK::PeakRSSTracker cqrrt_mem; cqrrt_mem.start(); - CQRRT_rss.call(A_linop, R_rss.data(), n, d_factor, state_rss); + CQRRT_rss.call(A_linop, R_rss, n, d_factor, state_rss); cqrrt_peak_rss_kb = cqrrt_mem.stop(); + delete[] R_rss; } + T* R_cqrrt = new T[n * n]; for (int64_t run = 0; run < num_runs; ++run) { - std::vector R_cqrrt(n * n, 0.0); + std::fill(R_cqrrt, R_cqrrt + n * n, (T)0); auto state_copy = run_states[run]; // Per-run RNG state RandLAPACK::CQRRT_linops CQRRT_QR(true, tol, false); // timing=true, test_mode=false CQRRT_QR.nnz = sketch_nnz; CQRRT_QR.block_size = block_size; - CQRRT_QR.call(A_linop, R_cqrrt.data(), n, d_factor, state_copy); + CQRRT_QR.call(A_linop, R_cqrrt, n, d_factor, state_copy); results[run].cqrrt.time = CQRRT_QR.times[10]; // total_t_dur results[run].cqrrt.peak_rss_kb = cqrrt_peak_rss_kb; results[run].cqrrt.breakdown.assign(CQRRT_QR.times.begin(), CQRRT_QR.times.begin() + 10); // Uniform Q computation for every run: Q = A * R^{-1} via operator - compute_Q_from_R(A_linop, R_cqrrt.data(), n, Q_uniform.data(), m, n); - results[run].cqrrt.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform.data(), m, n); + compute_Q_from_R(A_linop, R_cqrrt, n, Q_uniform, m, n); + results[run].cqrrt.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform, m, n); results[run].cqrrt.is_orthonormal = (results[run].cqrrt.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - results[run].cqrrt.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform.data(), m, n); + results[run].cqrrt.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform, m, n); } + delete[] R_cqrrt; } // ============================================================ @@ -157,47 +161,51 @@ static std::vector> run_algorithms( // RSS measurement (test_mode=false) long cholqr_peak_rss_kb = 0; { - std::vector R_rss(n * n, 0.0); + T* R_rss = new T[n * n](); RandLAPACK::CholQR_linops CholQR_rss(false, tol, false); CholQR_rss.block_size = block_size; RandLAPACK::PeakRSSTracker cholqr_mem; cholqr_mem.start(); - CholQR_rss.call(A_linop, R_rss.data(), n); + CholQR_rss.call(A_linop, R_rss, n); cholqr_peak_rss_kb = cholqr_mem.stop(); + delete[] R_rss; } + T* R_cholqr = new T[n * n]; for (int64_t run = 0; run < num_runs; ++run) { - std::vector R_cholqr(n * n, 0.0); + std::fill(R_cholqr, R_cholqr + n * n, (T)0); RandLAPACK::CholQR_linops CholQR_alg(true, tol, false); // timing=true, test_mode=false CholQR_alg.block_size = block_size; - CholQR_alg.call(A_linop, R_cholqr.data(), n); + CholQR_alg.call(A_linop, R_cholqr, n); results[run].cholqr.time = CholQR_alg.times[5]; // total results[run].cholqr.peak_rss_kb = cholqr_peak_rss_kb; results[run].cholqr.breakdown.assign(CholQR_alg.times.begin(), CholQR_alg.times.begin() + 5); // Uniform Q computation for every run - compute_Q_from_R(A_linop, R_cholqr.data(), n, Q_uniform.data(), m, n); - results[run].cholqr.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform.data(), m, n); + compute_Q_from_R(A_linop, R_cholqr, n, Q_uniform, m, n); + results[run].cholqr.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform, m, n); results[run].cholqr.is_orthonormal = (results[run].cholqr.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - results[run].cholqr.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform.data(), m, n); + results[run].cholqr.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform, m, n); } + delete[] R_cholqr; } // ============================================================ // Run sCholQR3 (shifted Cholesky QR with 3 iterations) - multiple runs // ============================================================ { + T* R_scholqr3 = new T[n * n]; for (int64_t run = 0; run < num_runs; ++run) { - std::vector R_scholqr3(n * n, 0.0); + std::fill(R_scholqr3, R_scholqr3 + n * n, (T)0); RandLAPACK::sCholQR3_linops sCholQR3_alg(true, tol, false); // timing=true, test_mode=false sCholQR3_alg.block_size = block_size; RandLAPACK::PeakRSSTracker scholqr3_mem; scholqr3_mem.start(); - sCholQR3_alg.call(A_linop, R_scholqr3.data(), n); + sCholQR3_alg.call(A_linop, R_scholqr3, n); results[run].scholqr3.peak_rss_kb = scholqr3_mem.stop(); results[run].scholqr3.time = sCholQR3_alg.times[17]; // total @@ -205,11 +213,12 @@ static std::vector> run_algorithms( results[run].scholqr3.breakdown.assign(sCholQR3_alg.times.begin(), sCholQR3_alg.times.begin() + 17); // Uniform Q computation (same as all other algorithms) - compute_Q_from_R(A_linop, R_scholqr3.data(), n, Q_uniform.data(), m, n); - results[run].scholqr3.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform.data(), m, n); + compute_Q_from_R(A_linop, R_scholqr3, n, Q_uniform, m, n); + results[run].scholqr3.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform, m, n); results[run].scholqr3.is_orthonormal = (results[run].scholqr3.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - results[run].scholqr3.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform.data(), m, n); + results[run].scholqr3.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform, m, n); } + delete[] R_scholqr3; } // ============================================================ @@ -218,30 +227,31 @@ static std::vector> run_algorithms( // Peak RSS with compute_Q=true is correct: Q overwrites A_materialized in-place (no extra allocation). // Skipped when skip_dense=true (e.g., for large file-input matrices where m*n dense doesn't fit in memory). if (!skip_dense) { + T* I_mat = new T[n * n](); + RandLAPACK::util::eye(n, n, I_mat); + T* R_dense = new T[n * n]; for (int64_t run = 0; run < num_runs; ++run) { RandLAPACK::PeakRSSTracker dense_mem; dense_mem.start(); // Step 1: Materialize the operator by multiplying with identity - std::vector I_mat(n * n, T(0)); - RandLAPACK::util::eye(n, n, I_mat.data()); T* A_materialized = new T[m * n](); auto materialize_start = steady_clock::now(); A_linop(Side::Left, Layout::ColMajor, Op::NoTrans, Op::NoTrans, - m, n, n, (T)1.0, I_mat.data(), n, (T)0.0, A_materialized, m); + m, n, n, (T)1.0, I_mat, n, (T)0.0, A_materialized, m); auto materialize_stop = steady_clock::now(); long materialize_time = duration_cast(materialize_stop - materialize_start).count(); // Step 2: Call rl_cqrrt with timing, Q-factor disabled (computed uniformly below) // Uses same per-run RNG state as CQRRT_linop for fair comparison - std::vector R_dense(n * n, 0.0); + std::fill(R_dense, R_dense + n * n, (T)0); auto state_copy = run_states[run]; // Same RNG state as CQRRT_linop's run RandLAPACK::CQRRT dense_alg(true, tol); // timing=true dense_alg.compute_Q = false; dense_alg.orthogonalization = false; dense_alg.nnz = sketch_nnz; - dense_alg.call(m, n, A_materialized, m, R_dense.data(), n, d_factor, state_copy); + dense_alg.call(m, n, A_materialized, m, R_dense, n, d_factor, state_copy); results[run].dense_cqrrt.peak_rss_kb = dense_mem.stop(); @@ -264,11 +274,13 @@ static std::vector> run_algorithms( }; // Uniform Q computation for every run - compute_Q_from_R(A_linop, R_dense.data(), n, Q_uniform.data(), m, n); - results[run].dense_cqrrt.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform.data(), m, n); + compute_Q_from_R(A_linop, R_dense, n, Q_uniform, m, n); + results[run].dense_cqrrt.orth_error = RandLAPACK::testing::orthogonality_error(Q_uniform, m, n); results[run].dense_cqrrt.is_orthonormal = (results[run].dense_cqrrt.orth_error <= std::pow(std::numeric_limits::epsilon(), (T)0.75)); - results[run].dense_cqrrt.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform.data(), m, n); + results[run].dense_cqrrt.max_orth_cols = RandLAPACK::testing::max_orthonormal_cols(Q_uniform, m, n); } + delete[] I_mat; + delete[] R_dense; } // if (!skip_dense) // Compute analytical peak working memory for each algorithm (same for all runs) @@ -283,6 +295,7 @@ static std::vector> run_algorithms( results[r].dense_cqrrt.analytical_kb = dense_cqrrt_akb; } + delete[] Q_uniform; return results; } From 3a934980d310caf7065028eefb0390bd7c28253a Mon Sep 17 00:00:00 2001 From: mmelnich Date: Tue, 2 Jun 2026 15:55:19 -0700 Subject: [PATCH 32/47] Extract small benchmark I/O helpers into cqrrt_bench_common.hh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two helpers added to the shared header: - load_csr_verbose: load_csr + the "Loading