blob: c06b76e35edaec061a50d2cd4169d99e5c00c64c [file] [edit]
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
// KroneckerOperator products and solves, (A (x) B) vec(X) = vec(B X A^T), with
// n x n factors and r right-hand sides. The product GFLOPS count
// 2 r (work(B) n1 + m2 work(A)) flops, work(F) = stored entries of F. Each
// benchmark first checks its result outside the timed loop.
#include <benchmark/benchmark.h>
#include <Eigen/Dense>
#include <Eigen/Sparse>
#include <contrib/Eigen/StructuredMatrices>
using namespace Eigen;
using Mat = MatrixXd;
using SpMat = SparseMatrix<double>;
using Diag = DiagonalMatrix<double, Dynamic>;
namespace {
// The 1-D Laplacian tridiag(-1, 2, -1): its Kronecker sums are the 2-D
// finite-difference operators.
SpMat laplacian(Index n) {
SpMat L(n, n);
L.reserve(VectorXi::Constant(n, 3));
for (Index j = 0; j < n; ++j) {
if (j > 0) L.insert(j - 1, j) = -1.0;
L.insert(j, j) = 2.0;
if (j + 1 < n) L.insert(j + 1, j) = -1.0;
}
L.makeCompressed();
return L;
}
Mat dominant(Index n) { return Mat::Random(n, n) + double(2 * n) * Mat::Identity(n, n); }
void setFlops(benchmark::State& state, double flopsPerIteration) {
state.counters["GFLOPS"] =
benchmark::Counter(1e-9 * flopsPerIteration, benchmark::Counter::kIsIterationInvariantRate);
}
// The batched result must match the operator applied to one column at a time.
template <typename Lhs, typename Rhs>
bool productMatchesColumns(const KroneckerOperator<Lhs, Rhs>& K, const Mat& X, const Mat& Y) {
Mat expected(K.rows(), X.cols());
for (Index k = 0; k < X.cols(); ++k) expected.col(k) = K * X.col(k);
return (Y - expected).norm() <= 1e-10 * expected.norm();
}
// K x = b up to the conditioning of the (diagonally dominant) factors.
template <typename Lhs, typename Rhs>
bool solveHasSmallResidual(const KroneckerOperator<Lhs, Rhs>& K, const Mat& x, const Mat& b) {
const Mat residual = K * x - b;
return residual.norm() <= 1e-8 * b.norm();
}
template <typename Lhs, typename Rhs>
void runProduct(benchmark::State& state, const KroneckerOperator<Lhs, Rhs>& K, double workA, double workB) {
const Index r = state.range(1);
const Mat X = Mat::Random(K.cols(), r);
Mat Y = K * X;
if (!productMatchesColumns(K, X, Y)) {
state.SkipWithError("batched product differs from the column-by-column product");
return;
}
for (auto _ : state) {
Y.noalias() = K * X;
benchmark::DoNotOptimize(Y.data());
benchmark::ClobberMemory();
}
const double n1 = double(K.lhs().cols()), m2 = double(K.rhs().rows());
setFlops(state, 2.0 * double(r) * (workB * n1 + m2 * workA));
}
void BM_ProductDense(benchmark::State& state) {
const Index n = state.range(0);
const KroneckerOperator<Mat, Mat> K(Mat::Random(n, n), Mat::Random(n, n));
runProduct(state, K, double(n * n), double(n * n));
}
void BM_ProductSparse(benchmark::State& state) {
const Index n = state.range(0);
const SpMat L = laplacian(n);
const KroneckerOperator<SpMat, SpMat> K(L, L);
runProduct(state, K, double(L.nonZeros()), double(L.nonZeros()));
}
// I_n (x) B, the block-diagonal operator of n independent n x n systems.
void BM_ProductIdentityDense(benchmark::State& state) {
const Index n = state.range(0);
const KroneckerOperator<Diag, Mat> K(Diag(VectorXd::Ones(n)), Mat::Random(n, n));
runProduct(state, K, double(n), double(n * n));
}
// L (x) I_n and I_n (x) L: a sparse factor against a diagonal one.
void BM_ProductSparseIdentity(benchmark::State& state) {
const SpMat L = laplacian(state.range(0));
const KroneckerOperator<SpMat, Diag> K(L, Diag(VectorXd::Ones(L.rows())));
runProduct(state, K, double(L.nonZeros()), double(L.rows()));
}
void BM_ProductIdentitySparse(benchmark::State& state) {
const SpMat L = laplacian(state.range(0));
const KroneckerOperator<Diag, SpMat> K(Diag(VectorXd::Ones(L.rows())), L);
runProduct(state, K, double(L.rows()), double(L.nonZeros()));
}
template <typename Lhs, typename Rhs>
void runSolve(benchmark::State& state, const KroneckerOperator<Lhs, Rhs>& K) {
const Mat b = Mat::Random(K.rows(), state.range(1));
Mat x = K.solve(b);
if (!solveHasSmallResidual(K, x, b)) {
state.SkipWithError("solve residual too large");
return;
}
for (auto _ : state) {
x = K.solve(b);
benchmark::DoNotOptimize(x.data());
benchmark::ClobberMemory();
}
}
void BM_SolveDense(benchmark::State& state) {
const Index n = state.range(0);
runSolve(state, KroneckerOperator<Mat, Mat>(dominant(n), dominant(n)));
}
void BM_SolveSparse(benchmark::State& state) {
const SpMat L = laplacian(state.range(0));
runSolve(state, KroneckerOperator<SpMat, SpMat>(L, L));
}
void BM_LeastSquaresDense(benchmark::State& state) {
const Index n = state.range(0);
const KroneckerOperator<Mat, Mat> K(dominant(n), dominant(n)); // full rank: least squares is the solve
const Mat b = Mat::Random(K.rows(), state.range(1));
Mat x = K.leastSquaresSolve(b);
if (!solveHasSmallResidual(K, x, b)) {
state.SkipWithError("least-squares residual too large");
return;
}
for (auto _ : state) {
x = K.leastSquaresSolve(b);
benchmark::DoNotOptimize(x.data());
benchmark::ClobberMemory();
}
}
} // namespace
BENCHMARK(BM_ProductDense)->ArgsProduct({{8, 32, 128}, {1, 32}});
BENCHMARK(BM_ProductSparse)->ArgsProduct({{64, 512}, {1, 32}});
BENCHMARK(BM_ProductIdentityDense)->ArgsProduct({{8, 32, 128}, {1, 32}});
BENCHMARK(BM_ProductSparseIdentity)->ArgsProduct({{64, 512}, {1, 32}});
BENCHMARK(BM_ProductIdentitySparse)->ArgsProduct({{64, 512}, {1, 32}});
BENCHMARK(BM_SolveDense)->ArgsProduct({{8, 32, 128}, {1, 32}});
BENCHMARK(BM_SolveSparse)->ArgsProduct({{64, 512}, {1, 32}});
BENCHMARK(BM_LeastSquaresDense)->ArgsProduct({{32}, {1, 32}});