// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // blas.hpp // BLAS level 1/2/3 interface // // Provides a BLAS (Basic Linear Algebra Subprograms) style API. // Foundation for switching to an MKL/OpenBLAS backend in the future. // Currently a native implementation (operates directly on sangi's Vector/Matrix). #ifndef SANGI_BLAS_HPP #define SANGI_BLAS_HPP #include #include #include #include #include #include #include #include namespace sangi { namespace blas { // ==================================================================== // Level 1: vector-vector operations // ==================================================================== /** * @brief Dot product: result = x^T * y */ template requires sangi::BaseVectorLike && sangi::BaseVectorLike && std::same_as, sangi::element_t> sangi::element_t dot(const VX& x, const VY& y) { using T = sangi::element_t; // Compile-time size check when both vectors carry static sizes. if constexpr (sangi::dims_both_static( std::remove_cvref_t::static_size, std::remove_cvref_t::static_size)) { static_assert(std::remove_cvref_t::static_size == std::remove_cvref_t::static_size, "sangi::blas::dot: vector sizes must match (static check)"); } if (x.size() != y.size()) { assert(false && "DimensionError: blas::dot: size mismatch"); throw DimensionError("blas::dot: size mismatch"); } if constexpr (std::is_same_v || std::is_same_v) { if (x.is_contiguous() && y.is_contiguous()) return computation::simd::dot_product_simd(x.data(), y.data(), x.size()); } T result = T{0}; for (std::size_t i = 0; i < x.size(); ++i) result += x[i] * y[i]; return result; } /** * @brief 2-norm: result = ‖x‖₂ */ template requires sangi::BaseVectorLike sangi::element_t nrm2(const VX& x) { using T = sangi::element_t; if constexpr (std::is_same_v || std::is_same_v) { if (x.is_contiguous()) { T sum = computation::simd::dot_product_simd(x.data(), x.data(), x.size()); return std::sqrt(sum); } T sum = T{0}; for (std::size_t i = 0; i < x.size(); ++i) sum += x[i] * x[i]; return std::sqrt(sum); } else { T sum = T{0}; for (std::size_t i = 0; i < x.size(); ++i) sum += x[i] * x[i]; return static_cast(std::sqrt(static_cast(sum))); } } /** * @brief 1-norm (sum of absolute values): result = ‖x‖₁ = Σ|x_i| * * Equivalent to BLAS dasum. */ template requires sangi::BaseVectorLike sangi::element_t asum(const VX& x) { using T = sangi::element_t; T sum = T{0}; for (std::size_t i = 0; i < x.size(); ++i) sum += static_cast(std::abs(static_cast(x[i]))); return sum; } /** * @brief Index of the element with largest absolute value: result = argmax_i |x_i| * * Equivalent to BLAS idamax. Returns 0 for an empty vector. */ template requires sangi::BaseVectorLike std::size_t iamax(const VX& x) { using T = sangi::element_t; if (x.size() == 0) return 0; std::size_t idx = 0; double max_val = std::abs(static_cast(x[0])); for (std::size_t i = 1; i < x.size(); ++i) { double ai = std::abs(static_cast(x[i])); if (ai > max_val) { max_val = ai; idx = i; } } return idx; } /** * @brief Scaling: x ← alpha * x */ template void scal(T alpha, Vector& x) { if constexpr (std::is_same_v || std::is_same_v) { computation::simd::scale_simd(x.data(), alpha, x.size()); } else { for (std::size_t i = 0; i < x.size(); ++i) x[i] *= alpha; } } /** * @brief AXPY: y ← alpha * x + y */ template void axpy(T alpha, const BaseVector& x, Vector& y) { if (x.size() != y.size()) { assert(false && "DimensionError: blas::axpy: size mismatch"); throw DimensionError("blas::axpy: size mismatch"); } if constexpr (std::is_same_v || std::is_same_v) { if (x.is_contiguous()) { computation::simd::axpy_simd(y.data(), alpha, x.data(), x.size()); return; } } for (std::size_t i = 0; i < x.size(); ++i) y[i] += alpha * x[i]; } /** * @brief Copy: y ← x */ template void copy(const BaseVector& x, Vector& y) { if (x.size() != y.size()) { assert(false && "DimensionError: blas::copy: size mismatch"); throw DimensionError("blas::copy: size mismatch"); } for (std::size_t i = 0; i < x.size(); ++i) y[i] = x[i]; } /** * @brief Swap: x ↔ y */ template void swap(Vector& x, Vector& y) { if (x.size() != y.size()) { assert(false && "DimensionError: blas::swap: size mismatch"); throw DimensionError("blas::swap: size mismatch"); } for (std::size_t i = 0; i < x.size(); ++i) std::swap(x[i], y[i]); } // ==================================================================== // Level 2: matrix-vector operations // ==================================================================== /** * @brief General matrix-vector product: y ← alpha * op(A) * x + beta * y * * @param trans false: op(A) = A, true: op(A) = A^T * @param alpha scalar coefficient * @param A m×n matrix * @param x input vector * @param beta scalar coefficient * @param y output vector (updated in-place) */ template void gemv(bool trans, T alpha, const BaseMatrix& A, const BaseVector& x, T beta, BaseVector& y) { const auto m = A.rows(); const auto n = A.cols(); // The SIMD path is only valid when each row of A is contiguous (= col_stride == 1) // and y is contiguous. Safe even when a transposed view, etc. arrives via BaseMatrix. const bool A_row_contig = (A.col_stride() == 1); if (!trans) { // y ← alpha * A * x + beta * y if (x.size() != n || y.size() != m) { assert(false && "DimensionError: blas::gemv: dimension mismatch"); throw DimensionError("blas::gemv: dimension mismatch"); } if constexpr (std::is_same_v || std::is_same_v) { if (A_row_contig && x.is_contiguous() && y.is_contiguous()) { for (std::size_t i = 0; i < m; ++i) { T sum = computation::simd::dot_product_simd(&A(i, 0), x.data(), n); y[i] = alpha * sum + beta * y[i]; } return; } } for (std::size_t i = 0; i < m; ++i) { T sum = T{0}; for (std::size_t j = 0; j < n; ++j) sum += A(i, j) * x[j]; y[i] = alpha * sum + beta * y[i]; } } else { // y ← alpha * A^T * x + beta * y if (x.size() != m || y.size() != n) { assert(false && "DimensionError: blas::gemv: dimension mismatch (transpose)"); throw DimensionError("blas::gemv: dimension mismatch (transpose)"); } if constexpr (std::is_same_v || std::is_same_v) { if (A_row_contig && y.is_contiguous()) { computation::simd::scale_simd(y.data(), beta, n); for (std::size_t i = 0; i < m; ++i) { T ax = alpha * x[i]; computation::simd::axpy_simd(y.data(), ax, &A(i, 0), n); } return; } } for (std::size_t j = 0; j < n; ++j) y[j] *= beta; for (std::size_t i = 0; i < m; ++i) { T ax = alpha * x[i]; for (std::size_t j = 0; j < n; ++j) y[j] += ax * A(i, j); } } } /** * @brief Solves a triangular system of equations: x ← op(A)⁻¹ * x * * @param upper true: upper triangular, false: lower triangular * @param trans true: A^T, false: A * @param unit_diag true: treat diagonal entries as 1 * @param A triangular matrix * @param x right-hand-side vector (returns the solution via in-place update) */ template void trsv(bool upper, bool trans, bool unit_diag, const BaseMatrix& A, BaseVector& x) { const auto n = A.rows(); if (A.cols() != n || x.size() != n) { assert(false && "DimensionError: blas::trsv: dimension mismatch"); throw DimensionError("blas::trsv: dimension mismatch"); } if (!trans) { if (upper) { // Upper triangular, back substitution for (std::size_t ii = 0; ii < n; ++ii) { std::size_t i = n - 1 - ii; for (std::size_t j = i + 1; j < n; ++j) x[i] -= A(i, j) * x[j]; if (!unit_diag) x[i] /= A(i, i); } } else { // Lower triangular, forward substitution for (std::size_t i = 0; i < n; ++i) { for (std::size_t j = 0; j < i; ++j) x[i] -= A(i, j) * x[j]; if (!unit_diag) x[i] /= A(i, i); } } } else { if (upper) { // Upper triangular transposed → forward substitution as lower triangular for (std::size_t i = 0; i < n; ++i) { for (std::size_t j = 0; j < i; ++j) x[i] -= A(j, i) * x[j]; if (!unit_diag) x[i] /= A(i, i); } } else { // Lower triangular transposed → back substitution as upper triangular for (std::size_t ii = 0; ii < n; ++ii) { std::size_t i = n - 1 - ii; for (std::size_t j = i + 1; j < n; ++j) x[i] -= A(j, i) * x[j]; if (!unit_diag) x[i] /= A(i, i); } } } } /** * @brief Rank-1 update: A ← alpha * x * y^T + A */ template void ger(T alpha, const BaseVector& x, const BaseVector& y, BaseMatrix& A) { if (x.size() != A.rows() || y.size() != A.cols()) { assert(false && "DimensionError: blas::ger: dimension mismatch"); throw DimensionError("blas::ger: dimension mismatch"); } for (std::size_t i = 0; i < A.rows(); ++i) { T ax = alpha * x[i]; for (std::size_t j = 0; j < A.cols(); ++j) A(i, j) += ax * y[j]; } } /** * @brief Symmetric rank-1 update: A ← alpha * x * x^T + A * * A is a symmetric matrix (update only the upper triangle, then copy to the lower). */ template void syr(bool upper, T alpha, const BaseVector& x, BaseMatrix& A) { const auto n = x.size(); if (A.rows() != n || A.cols() != n) { assert(false && "DimensionError: blas::syr: dimension mismatch"); throw DimensionError("blas::syr: dimension mismatch"); } if (upper) { for (std::size_t i = 0; i < n; ++i) { T ax = alpha * x[i]; for (std::size_t j = i; j < n; ++j) { A(i, j) += ax * x[j]; if (i != j) A(j, i) = A(i, j); } } } else { for (std::size_t j = 0; j < n; ++j) { T ax = alpha * x[j]; for (std::size_t i = j; i < n; ++i) { A(i, j) += ax * x[i]; if (i != j) A(j, i) = A(i, j); } } } } // ==================================================================== // Level 3: matrix-matrix operations // ==================================================================== /** * @brief General matrix product: C ← alpha * op(A) * op(B) + beta * C * * @param transA false: op(A) = A, true: op(A) = A^T * @param transB false: op(B) = B, true: op(B) = B^T */ template void gemm(bool transA, bool transB, T alpha, const BaseMatrix& A, const BaseMatrix& B, T beta, BaseMatrix& C) { const auto opA_rows = transA ? A.cols() : A.rows(); const auto opA_cols = transA ? A.rows() : A.cols(); const auto opB_rows = transB ? B.cols() : B.rows(); const auto opB_cols = transB ? B.rows() : B.cols(); if (opA_cols != opB_rows || C.rows() != opA_rows || C.cols() != opB_cols) { assert(false && "DimensionError: blas::gemm: dimension mismatch"); throw DimensionError("blas::gemm: dimension mismatch"); } const auto m = opA_rows; const auto n = opB_cols; const auto k = opA_cols; // The SIMD blocking path requires each row of B / C to be row-contiguous (col_stride == 1) const bool BC_row_contig = (B.col_stride() == 1) && (C.col_stride() == 1); // C ← beta * C if (beta == T{0}) { for (std::size_t i = 0; i < m; ++i) for (std::size_t j = 0; j < n; ++j) C(i, j) = T{0}; } else if (beta != T{1}) { for (std::size_t i = 0; i < m; ++i) for (std::size_t j = 0; j < n; ++j) C(i, j) *= beta; } // C += alpha * op(A) * op(B) // 4 transpose combinations if (!transA && !transB) { if constexpr (std::is_same_v || std::is_same_v) { if (BC_row_contig) { // Blocking + SIMD (ipj loop order) constexpr std::size_t BLK = 64; for (std::size_t i0 = 0; i0 < m; i0 += BLK) { const std::size_t i1 = (std::min)(i0 + BLK, m); for (std::size_t p0 = 0; p0 < k; p0 += BLK) { const std::size_t p1 = (std::min)(p0 + BLK, k); for (std::size_t j0 = 0; j0 < n; j0 += BLK) { const std::size_t j1 = (std::min)(j0 + BLK, n); const std::size_t jlen = j1 - j0; // Micro-kernel for (std::size_t i = i0; i < i1; ++i) { for (std::size_t p = p0; p < p1; ++p) { T aip = alpha * A(i, p); computation::simd::axpy_simd( &C(i, j0), aip, &B(p, j0), jlen); } } } } } } else { // Non-contiguous fallback for (std::size_t i = 0; i < m; ++i) for (std::size_t p = 0; p < k; ++p) { T aip = alpha * A(i, p); for (std::size_t j = 0; j < n; ++j) C(i, j) += aip * B(p, j); } } } else { for (std::size_t i = 0; i < m; ++i) for (std::size_t p = 0; p < k; ++p) { T aip = alpha * A(i, p); for (std::size_t j = 0; j < n; ++j) C(i, j) += aip * B(p, j); } } } else if (transA && !transB) { for (std::size_t i = 0; i < m; ++i) for (std::size_t p = 0; p < k; ++p) { T api = alpha * A(p, i); for (std::size_t j = 0; j < n; ++j) C(i, j) += api * B(p, j); } } else if (!transA && transB) { for (std::size_t i = 0; i < m; ++i) for (std::size_t j = 0; j < n; ++j) { T sum = T{0}; for (std::size_t p = 0; p < k; ++p) sum += A(i, p) * B(j, p); C(i, j) += alpha * sum; } } else { // transA && transB for (std::size_t i = 0; i < m; ++i) for (std::size_t j = 0; j < n; ++j) { T sum = T{0}; for (std::size_t p = 0; p < k; ++p) sum += A(p, i) * B(j, p); C(i, j) += alpha * sum; } } } /** * @brief Solves a triangular system of equations (matrix version): B ← op(A)⁻¹ * B or B ← B * op(A)⁻¹ * * @param side true: left (A⁻¹B), false: right (BA⁻¹) * @param upper true: upper triangular, false: lower triangular * @param trans true: A^T, false: A * @param unit_diag true: treat diagonal entries as 1 * @param alpha scalar coefficient (B ← alpha * ...) * @param A triangular matrix (n×n) * @param B input/output matrix */ template void trsm(bool side_left, bool upper, bool trans, bool unit_diag, T alpha, const BaseMatrix& A, BaseMatrix& B) { const auto n = A.rows(); if (A.cols() != n) { assert(false && "DimensionError: blas::trsm: A must be square"); throw DimensionError("blas::trsm: A must be square"); } // alpha scaling if (alpha != T{1}) { for (std::size_t i = 0; i < B.rows(); ++i) for (std::size_t j = 0; j < B.cols(); ++j) B(i, j) *= alpha; } if (side_left) { // B ← A⁻¹ * B: trsv each column independently if (B.rows() != n) { assert(false && "DimensionError: blas::trsm: dimension mismatch (left)"); throw DimensionError("blas::trsm: dimension mismatch (left)"); } for (std::size_t col = 0; col < B.cols(); ++col) { Vector b_col(n); for (std::size_t i = 0; i < n; ++i) b_col[i] = B(i, col); trsv(upper, trans, unit_diag, A, b_col); for (std::size_t i = 0; i < n; ++i) B(i, col) = b_col[i]; } } else { // B ← B * A⁻¹: trsv each row independently (equivalent to trsv of A^T) if (B.cols() != n) { assert(false && "DimensionError: blas::trsm: dimension mismatch (right)"); throw DimensionError("blas::trsm: dimension mismatch (right)"); } for (std::size_t row = 0; row < B.rows(); ++row) { Vector b_row(n); for (std::size_t j = 0; j < n; ++j) b_row[j] = B(row, j); trsv(upper, !trans, unit_diag, A, b_row); for (std::size_t j = 0; j < n; ++j) B(row, j) = b_row[j]; } } } /** * @brief Symmetric rank-k update: C ← alpha * A * A^T + beta * C (trans=false) * C ← alpha * A^T * A + beta * C (trans=true) * * C is a symmetric matrix (n×n). * trans=false: A is n×k, C ← alpha*A*A^T + beta*C * trans=true: A is k×n, C ← alpha*A^T*A + beta*C */ template void syrk(bool upper, bool trans, T alpha, const BaseMatrix& A, T beta, BaseMatrix& C) { const auto n = C.rows(); if (C.cols() != n) { assert(false && "DimensionError: blas::syrk: C must be square"); throw DimensionError("blas::syrk: C must be square"); } std::size_t k; if (!trans) { if (A.rows() != n) { assert(false && "DimensionError: blas::syrk: dimension mismatch"); throw DimensionError("blas::syrk: dimension mismatch"); } k = A.cols(); } else { if (A.cols() != n) { assert(false && "DimensionError: blas::syrk: dimension mismatch"); throw DimensionError("blas::syrk: dimension mismatch"); } k = A.rows(); } // C ← beta * C (upper or lower triangle only) for (std::size_t i = 0; i < n; ++i) { std::size_t j_start = upper ? i : 0; std::size_t j_end = upper ? n : i + 1; for (std::size_t j = j_start; j < j_end; ++j) C(i, j) *= beta; } // C += alpha * op(A) * op(A)^T if (!trans) { // C += alpha * A * A^T for (std::size_t i = 0; i < n; ++i) { std::size_t j_start = upper ? i : 0; std::size_t j_end = upper ? n : i + 1; for (std::size_t j = j_start; j < j_end; ++j) { T sum = T{0}; for (std::size_t p = 0; p < k; ++p) sum += A(i, p) * A(j, p); C(i, j) += alpha * sum; } } } else { // C += alpha * A^T * A for (std::size_t i = 0; i < n; ++i) { std::size_t j_start = upper ? i : 0; std::size_t j_end = upper ? n : i + 1; for (std::size_t j = j_start; j < j_end; ++j) { T sum = T{0}; for (std::size_t p = 0; p < k; ++p) sum += A(p, i) * A(p, j); C(i, j) += alpha * sum; } } } // Copy the symmetric part for (std::size_t i = 0; i < n; ++i) for (std::size_t j = i + 1; j < n; ++j) { if (upper) C(j, i) = C(i, j); else C(i, j) = C(j, i); } } } // namespace blas } // namespace sangi #endif // SANGI_BLAS_HPP