| // Benchmark: GPU Conjugate Gradient via gpu::DeviceMatrix operators. | |
| // | |
| // Shows the path to running Eigen's CG on GPU with minimal code changes. | |
| // The gpu::DeviceMatrix benchmark mirrors Eigen's conjugate_gradient() line-by-line. | |
| // A raw cuBLAS device-pointer-mode implementation is included as a lower bound. | |
| // | |
| // The only change needed in Eigen's CG template to support gpu::DeviceMatrix: | |
| // Line 34: typedef Dest VectorType; (instead of Matrix<Scalar, Dynamic, 1>) | |
| // | |
| // Usage: | |
| // cmake --build build-bench-gpu --target bench_gpu_cg_sync | |
| // ./build-bench-gpu/bench_gpu_cg_sync | |
| // SPDX-FileCopyrightText: The Eigen Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| #include <benchmark/benchmark.h> | |
| #include <Eigen/Sparse> | |
| #include <contrib/Eigen/GPU> | |
| #include <cusparse.h> | |
| using namespace Eigen; | |
| using Scalar = double; | |
| using RealScalar = double; | |
| using Vec = Matrix<Scalar, Dynamic, 1>; | |
| using SpMat = SparseMatrix<Scalar, ColMajor, int>; | |
| static SpMat make_spd(Index n) { | |
| SpMat A(n, n); | |
| A.reserve(VectorXi::Constant(n, 3)); | |
| for (Index i = 0; i < n; ++i) { | |
| A.insert(i, i) = 4.0; | |
| if (i > 0) A.insert(i, i - 1) = -1.0; | |
| if (i < n - 1) A.insert(i, i + 1) = -1.0; | |
| } | |
| A.makeCompressed(); | |
| return A; | |
| } | |
| static void cuda_warmup() { | |
| static bool done = false; | |
| if (!done) { | |
| void* p; | |
| cudaMalloc(&p, 1); | |
| cudaFree(p); | |
| done = true; | |
| } | |
| } | |
| // ========================================================================== | |
| // GPU CG using gpu::DeviceMatrix operators — mirrors Eigen's conjugate_gradient() | |
| // ========================================================================== | |
| // | |
| // Compare with Eigen/src/IterativeLinearSolvers/ConjugateGradient.h lines 29-84. | |
| // Left column: Eigen CG code. Right column: this benchmark. | |
| // | |
| // Eigen CG GPU CG (this benchmark) | |
| // -------- ----------------------- | |
| // VectorType residual = rhs - mat * x; residual.copyFrom(ctx, rhs); [x=0 so r=b] | |
| // RealScalar rhsNorm2 = rhs.sqNorm(); RealScalar rhsNorm2 = rhs.squaredNorm(); | |
| // ... | |
| // tmp.noalias() = mat * p; tmp.noalias() = mat * p; [identical] | |
| // Scalar alpha = absNew / p.dot(tmp); Scalar alpha = absNew / p.dot(tmp); [identical] | |
| // x += alpha * p; x += alpha * p; [identical] | |
| // residual -= alpha * tmp; residual -= alpha * tmp; [identical] | |
| // residualNorm2 = residual.sqNorm(); residualNorm2 = residual.squaredNorm(); [identical] | |
| // ... | |
| // p = z + beta * p; p *= beta; p += z; [equivalent, no alloc] | |
| static void BM_CG_DeviceMatrixOps(benchmark::State& state) { | |
| cuda_warmup(); | |
| const Index n = state.range(0); | |
| SpMat A = make_spd(n); | |
| Vec b = Vec::Random(n); | |
| // One shared context: SpMV + BLAS-1 on same stream, zero event overhead. | |
| gpu::Context ctx; | |
| gpu::Context::setThreadLocal(&ctx); | |
| gpu::SparseContext<Scalar> spmv(ctx); | |
| auto mat = spmv.deviceView(A); | |
| // Upload RHS once. | |
| auto rhs = gpu::DeviceMatrix<Scalar>::fromHost(b, ctx.stream()); | |
| for (auto _ : state) { | |
| // --- Eigen CG lines 34-63: initialization --- | |
| // typedef Dest VectorType; // GPU CHANGE: was Matrix<Scalar,Dynamic,1> | |
| // VectorType residual = rhs - mat * x; // x=0, so residual = rhs | |
| gpu::DeviceMatrix<Scalar> x(n, 1); | |
| x.setZero(); | |
| gpu::DeviceMatrix<Scalar> residual(n, 1); | |
| residual.copyFrom(ctx, rhs); | |
| // RealScalar rhsNorm2 = rhs.squaredNorm(); | |
| RealScalar rhsNorm2 = rhs.squaredNorm(); | |
| if (rhsNorm2 == 0) continue; | |
| RealScalar tol = 1e-10; | |
| const RealScalar considerAsZero = (std::numeric_limits<RealScalar>::min)(); | |
| RealScalar threshold = numext::maxi(RealScalar(tol * tol * rhsNorm2), considerAsZero); | |
| // RealScalar residualNorm2 = residual.squaredNorm(); | |
| RealScalar residualNorm2 = residual.squaredNorm(); | |
| if (residualNorm2 < threshold) continue; | |
| // VectorType p(n); | |
| // p = precond.solve(residual); // no preconditioner: p = residual | |
| gpu::DeviceMatrix<Scalar> p(n, 1); | |
| p.copyFrom(ctx, residual); | |
| // VectorType z(n), tmp(n); | |
| gpu::DeviceMatrix<Scalar> z(n, 1), tmp(n, 1); | |
| // auto absNew = numext::real(residual.dot(p)); | |
| // gpu::DeviceScalar — stays on device, no sync. | |
| auto absNew = residual.dot(p); // gpu::DeviceScalar, no sync | |
| // while (i < maxIters) { | |
| Index maxIters = 200; | |
| Index i = 0; | |
| while (i < maxIters) { | |
| // tmp.noalias() = mat * p; | |
| tmp.noalias() = mat * p; // SpMV, device-resident | |
| // auto alpha = absNew / p.dot(tmp); | |
| // gpu::DeviceScalar / gpu::DeviceScalar → device kernel, no sync! | |
| auto alpha = absNew / p.dot(tmp); // gpu::DeviceScalar, no sync | |
| // x += alpha * p; | |
| // gpu::DeviceScalar * gpu::DeviceMatrix → device-pointer axpy, no sync! | |
| x += alpha * p; | |
| // residual -= alpha * tmp; | |
| residual -= alpha * tmp; // device-pointer axpy, no sync | |
| // residualNorm2 = residual.squaredNorm(); | |
| residualNorm2 = residual.squaredNorm(); // THE one sync per iteration | |
| // if (residualNorm2 < threshold) break; | |
| if (residualNorm2 < threshold) break; | |
| // z = precond.solve(residual); | |
| z.copyFrom(ctx, residual); // no preconditioner | |
| // auto absOld = std::move(absNew); | |
| auto absOld = std::move(absNew); // no sync, no alloc | |
| // absNew = numext::real(residual.dot(z)); | |
| absNew = residual.dot(z); // gpu::DeviceScalar, no sync | |
| // auto beta = absNew / absOld; | |
| // gpu::DeviceScalar / gpu::DeviceScalar → device kernel, no sync! | |
| auto beta = absNew / absOld; // gpu::DeviceScalar, no sync | |
| // p = z + beta * p; | |
| p *= beta; // device-pointer scal, no host sync | |
| p += z; | |
| i++; | |
| } | |
| } | |
| gpu::Context::setThreadLocal(nullptr); | |
| state.SetItemsProcessed(state.iterations() * 200); | |
| } | |
| BENCHMARK(BM_CG_DeviceMatrixOps)->RangeMultiplier(4)->Range(1 << 10, 1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| // ========================================================================== | |
| // Raw cuBLAS device-pointer-mode CG (1 sync/iter) — performance lower bound | |
| // ========================================================================== | |
| __global__ void scalar_div_kernel(const Scalar* a, const Scalar* b, Scalar* out) { *out = *a / *b; } | |
| __global__ void scalar_neg_kernel(const Scalar* in, Scalar* out) { *out = -(*in); } | |
| static void BM_CG_DevicePointerMode(benchmark::State& state) { | |
| cuda_warmup(); | |
| const Index n = state.range(0); | |
| const int maxIters = 200; | |
| SpMat A = make_spd(n); | |
| Vec b = Vec::Random(n); | |
| cudaStream_t stream; | |
| cudaStreamCreate(&stream); | |
| cublasHandle_t cublas; | |
| cublasCreate(&cublas); | |
| cublasSetStream(cublas, stream); | |
| cusparseHandle_t cusparse; | |
| cusparseCreate(&cusparse); | |
| cusparseSetStream(cusparse, stream); | |
| gpu::internal::DeviceBuffer d_outer((n + 1) * sizeof(int)); | |
| gpu::internal::DeviceBuffer d_inner(A.nonZeros() * sizeof(int)); | |
| gpu::internal::DeviceBuffer d_vals(A.nonZeros() * sizeof(Scalar)); | |
| cudaMemcpy(d_outer.get(), A.outerIndexPtr(), (n + 1) * sizeof(int), cudaMemcpyHostToDevice); | |
| cudaMemcpy(d_inner.get(), A.innerIndexPtr(), A.nonZeros() * sizeof(int), cudaMemcpyHostToDevice); | |
| cudaMemcpy(d_vals.get(), A.valuePtr(), A.nonZeros() * sizeof(Scalar), cudaMemcpyHostToDevice); | |
| cusparseSpMatDescr_t matA; | |
| cusparseCreateCsc(&matA, n, n, A.nonZeros(), d_outer.get(), d_inner.get(), d_vals.get(), CUSPARSE_INDEX_32I, | |
| CUSPARSE_INDEX_32I, CUSPARSE_INDEX_BASE_ZERO, CUDA_R_64F); | |
| gpu::internal::DeviceBuffer d_tmp_buf(n * sizeof(Scalar)); | |
| cusparseDnVecDescr_t tmp_x, tmp_y; | |
| cusparseCreateDnVec(&tmp_x, n, d_tmp_buf.get(), CUDA_R_64F); | |
| cusparseCreateDnVec(&tmp_y, n, d_tmp_buf.get(), CUDA_R_64F); | |
| Scalar spmv_alpha = 1.0, spmv_beta = 0.0; | |
| size_t ws_size = 0; | |
| cusparseSpMV_bufferSize(cusparse, CUSPARSE_OPERATION_NON_TRANSPOSE, &spmv_alpha, matA, tmp_x, &spmv_beta, tmp_y, | |
| CUDA_R_64F, CUSPARSE_SPMV_ALG_DEFAULT, &ws_size); | |
| gpu::internal::DeviceBuffer d_workspace(ws_size); | |
| cusparseDestroyDnVec(tmp_x); | |
| cusparseDestroyDnVec(tmp_y); | |
| gpu::internal::DeviceBuffer d_x(n * sizeof(Scalar)), d_r(n * sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_p(n * sizeof(Scalar)), d_tmp(n * sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_b(n * sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_absNew(sizeof(Scalar)), d_absOld(sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_pdot(sizeof(Scalar)), d_alpha(sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_neg_alpha(sizeof(Scalar)), d_beta(sizeof(Scalar)); | |
| gpu::internal::DeviceBuffer d_rnorm(sizeof(RealScalar)); | |
| cudaMemcpy(d_b.get(), b.data(), n * sizeof(Scalar), cudaMemcpyHostToDevice); | |
| auto spmv = [&](Scalar* x_ptr, Scalar* y_ptr) { | |
| cusparseDnVecDescr_t vx, vy; | |
| cusparseCreateDnVec(&vx, n, x_ptr, CUDA_R_64F); | |
| cusparseCreateDnVec(&vy, n, y_ptr, CUDA_R_64F); | |
| cusparseSpMV(cusparse, CUSPARSE_OPERATION_NON_TRANSPOSE, &spmv_alpha, matA, vx, &spmv_beta, vy, CUDA_R_64F, | |
| CUSPARSE_SPMV_ALG_DEFAULT, d_workspace.get()); | |
| cusparseDestroyDnVec(vx); | |
| cusparseDestroyDnVec(vy); | |
| }; | |
| for (auto _ : state) { | |
| cudaMemsetAsync(static_cast<Scalar*>(d_x.get()), 0, n * sizeof(Scalar), stream); | |
| cudaMemcpyAsync(d_r.get(), d_b.get(), n * sizeof(Scalar), cudaMemcpyDeviceToDevice, stream); | |
| cudaMemcpyAsync(d_p.get(), d_b.get(), n * sizeof(Scalar), cudaMemcpyDeviceToDevice, stream); | |
| cublasSetPointerMode(cublas, CUBLAS_POINTER_MODE_DEVICE); | |
| cublasDdot(cublas, n, static_cast<Scalar*>(d_r.get()), 1, static_cast<Scalar*>(d_p.get()), 1, | |
| static_cast<Scalar*>(d_absNew.get())); | |
| for (int i = 0; i < maxIters; ++i) { | |
| spmv(static_cast<Scalar*>(d_p.get()), static_cast<Scalar*>(d_tmp.get())); | |
| cublasDdot(cublas, n, static_cast<Scalar*>(d_p.get()), 1, static_cast<Scalar*>(d_tmp.get()), 1, | |
| static_cast<Scalar*>(d_pdot.get())); | |
| scalar_div_kernel<<<1, 1, 0, stream>>>(static_cast<Scalar*>(d_absNew.get()), static_cast<Scalar*>(d_pdot.get()), | |
| static_cast<Scalar*>(d_alpha.get())); | |
| scalar_neg_kernel<<<1, 1, 0, stream>>>(static_cast<Scalar*>(d_alpha.get()), | |
| static_cast<Scalar*>(d_neg_alpha.get())); | |
| cublasDaxpy(cublas, n, static_cast<Scalar*>(d_alpha.get()), static_cast<Scalar*>(d_p.get()), 1, | |
| static_cast<Scalar*>(d_x.get()), 1); | |
| cublasDaxpy(cublas, n, static_cast<Scalar*>(d_neg_alpha.get()), static_cast<Scalar*>(d_tmp.get()), 1, | |
| static_cast<Scalar*>(d_r.get()), 1); | |
| cublasDnrm2(cublas, n, static_cast<Scalar*>(d_r.get()), 1, static_cast<RealScalar*>(d_rnorm.get())); | |
| RealScalar rnorm; | |
| cudaMemcpyAsync(&rnorm, d_rnorm.get(), sizeof(RealScalar), cudaMemcpyDeviceToHost, stream); | |
| cudaStreamSynchronize(stream); | |
| if (rnorm * rnorm < 1e-20) break; | |
| cudaMemcpyAsync(d_absOld.get(), d_absNew.get(), sizeof(Scalar), cudaMemcpyDeviceToDevice, stream); | |
| cublasDdot(cublas, n, static_cast<Scalar*>(d_r.get()), 1, static_cast<Scalar*>(d_r.get()), 1, | |
| static_cast<Scalar*>(d_absNew.get())); | |
| scalar_div_kernel<<<1, 1, 0, stream>>>(static_cast<Scalar*>(d_absNew.get()), static_cast<Scalar*>(d_absOld.get()), | |
| static_cast<Scalar*>(d_beta.get())); | |
| cublasDscal(cublas, n, static_cast<Scalar*>(d_beta.get()), static_cast<Scalar*>(d_p.get()), 1); | |
| cublasSetPointerMode(cublas, CUBLAS_POINTER_MODE_HOST); | |
| Scalar one = 1.0; | |
| cublasDaxpy(cublas, n, &one, static_cast<Scalar*>(d_r.get()), 1, static_cast<Scalar*>(d_p.get()), 1); | |
| cublasSetPointerMode(cublas, CUBLAS_POINTER_MODE_DEVICE); | |
| } | |
| cudaStreamSynchronize(stream); | |
| } | |
| state.SetItemsProcessed(state.iterations() * maxIters); | |
| cusparseDestroySpMat(matA); | |
| cusparseDestroy(cusparse); | |
| cublasDestroy(cublas); | |
| cudaStreamDestroy(stream); | |
| } | |
| BENCHMARK(BM_CG_DevicePointerMode)->RangeMultiplier(4)->Range(1 << 10, 1 << 20)->UseRealTime()->MinWarmUpTime(0.5); |