| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com> | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla | |
| // Public License v. 2.0. If a copy of the MPL was not distributed | |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Micro-benchmarks for per-call overhead in the GPU module's core layer: | |
| // allocation traffic, hidden host synchronization, and host-side copies. | |
| // Each benchmark pairs the current API path against a hand-rolled baseline | |
| // that shows what the same operation costs without the overhead, so the gap | |
| // is directly attributable. | |
| // | |
| // FromHost / FromHostAsync — extra host-side PlainMatrix copy in fromHost() | |
| // GemmFreshDst / PreallocDst — cudaMalloc/cudaFree per GEMM temporary | |
| // DotDeviceScalar / DotRaw — DeviceScalar wrapper cost per reduction | |
| // DotIntoDeviceScalar — the same reduction into an existing DeviceScalar | |
| // NormRead / StableNormRead — norm() (dot + NPP sqrt) vs stableNorm() (nrm2), read back | |
| // OneShotLltExpr / CachedLlt / RawPotrs — expression-solve sync + allocs | |
| // CudaMalloc / CudaMallocAsync — stream-ordered allocation as a remedy | |
| // SmallBuffer* / PoolAllocFree — small DeviceBuffer round trip (idle, behind in-flight work) and the pool itself | |
| // | |
| // Build (standalone project, see CMakeLists.txt in this directory): | |
| // cmake -G Ninja -B build-bench-gpu -S contrib/benchmarks/GPU \ | |
| // -DCMAKE_CUDA_ARCHITECTURES=89 | |
| // cmake --build build-bench-gpu --target bench_overhead | |
| // ./build-bench-gpu/bench_overhead | |
| #ifndef EIGEN_USE_GPU | |
| #define EIGEN_USE_GPU | |
| #endif | |
| #include <Eigen/Cholesky> | |
| #include <Eigen/SparseCore> | |
| #include <contrib/Eigen/GPU> | |
| #include <benchmark/benchmark.h> | |
| #include <cuda_runtime.h> | |
| #include <cusolverDn.h> | |
| #ifndef SCALAR | |
| #define SCALAR double | |
| #endif | |
| using Scalar = SCALAR; | |
| using Eigen::Index; | |
| namespace gpu = Eigen::gpu; | |
| using DeviceMatrix = gpu::DeviceMatrix<Scalar>; | |
| using HostMatrix = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>; | |
| namespace { | |
| HostMatrix spdMatrix(Index n) { | |
| HostMatrix A = HostMatrix::Random(n, n); | |
| HostMatrix S = A * A.transpose() + HostMatrix::Identity(n, n) * Scalar(n); | |
| return S; | |
| } | |
| void syncStream(cudaStream_t s) { EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(s)); } | |
| } // namespace | |
| // --------------------------------------------------------------------------- | |
| // 1. Host->device upload: fromHost(MatrixBase) evaluates the input into a | |
| // fresh PlainMatrix even when it is already a plain contiguous matrix, | |
| // adding a full host-side copy before the H2D transfer. Baseline uploads | |
| // straight from the existing host buffer. | |
| // --------------------------------------------------------------------------- | |
| static void BM_FromHost(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| HostMatrix A = HostMatrix::Random(n, n); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| for (auto _ : state) { | |
| DeviceMatrix d_A = DeviceMatrix::fromHost(A, ctx.stream()); | |
| benchmark::DoNotOptimize(d_A.data()); | |
| } | |
| state.SetBytesProcessed(state.iterations() * n * n * sizeof(Scalar)); | |
| } | |
| BENCHMARK(BM_FromHost)->Arg(256)->Arg(1024)->Arg(4096)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_FromHostRawPointer(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| HostMatrix A = HostMatrix::Random(n, n); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| for (auto _ : state) { | |
| DeviceMatrix d_A = DeviceMatrix::fromHostAsync(A.data(), n, n, ctx.stream()); | |
| syncStream(ctx.stream()); | |
| benchmark::DoNotOptimize(d_A.data()); | |
| } | |
| state.SetBytesProcessed(state.iterations() * n * n * sizeof(Scalar)); | |
| } | |
| BENCHMARK(BM_FromHostRawPointer)->Arg(256)->Arg(1024)->Arg(4096)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 2. GEMM destination allocation: assigning into a fresh (or wrong-shaped) | |
| // DeviceMatrix triggers cudaMalloc in resize() and cudaFree in the | |
| // destructor on every iteration; a pre-allocated same-shape destination | |
| // skips both. | |
| // --------------------------------------------------------------------------- | |
| static void BM_GemmFreshDst(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_A = DeviceMatrix::fromHost(HostMatrix::Random(n, n), ctx.stream()); | |
| DeviceMatrix d_B = DeviceMatrix::fromHost(HostMatrix::Random(n, n), ctx.stream()); | |
| for (auto _ : state) { | |
| DeviceMatrix d_C; // fresh: resize() -> cudaMalloc; dtor -> cudaFree | |
| d_C.device(ctx) = d_A * d_B; | |
| syncStream(ctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_GemmFreshDst)->Arg(64)->Arg(256)->Arg(1024)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_GemmPreallocDst(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_A = DeviceMatrix::fromHost(HostMatrix::Random(n, n), ctx.stream()); | |
| DeviceMatrix d_B = DeviceMatrix::fromHost(HostMatrix::Random(n, n), ctx.stream()); | |
| DeviceMatrix d_C(n, n); | |
| for (auto _ : state) { | |
| d_C.device(ctx) = d_A * d_B; | |
| syncStream(ctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_GemmPreallocDst)->Arg(64)->Arg(256)->Arg(1024)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 3. BLAS-1 reduction wrapper: dot() constructs a DeviceScalar initialized to | |
| // zero (a small H2D upload that the following cublasXdot immediately | |
| // overwrites) and toggles the cuBLAS pointer mode with three API calls. | |
| // Baseline: cublasDdot into a pre-allocated device slot with pointer mode | |
| // set once. Both enqueue only (no readback); sync once per iteration. | |
| // --------------------------------------------------------------------------- | |
| static void BM_DotDeviceScalar(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| DeviceMatrix d_y = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| for (auto _ : state) { | |
| gpu::DeviceScalar<Scalar> r = d_x.dot(ctx, d_y); | |
| benchmark::DoNotOptimize(r.devicePtr()); | |
| syncStream(ctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_DotDeviceScalar)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_DotRawCublas(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| DeviceMatrix d_y = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| Scalar* d_result = nullptr; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMalloc(&d_result, sizeof(Scalar))); | |
| EIGEN_CUBLAS_CHECK(cublasSetPointerMode(ctx.cublasHandle(), CUBLAS_POINTER_MODE_DEVICE)); | |
| for (auto _ : state) { | |
| EIGEN_CUBLAS_CHECK( | |
| gpu::internal::cublasXdot(ctx.cublasHandle(), static_cast<int>(n), d_x.data(), 1, d_y.data(), 1, d_result)); | |
| syncStream(ctx.stream()); | |
| } | |
| EIGEN_CUBLAS_CHECK(cublasSetPointerMode(ctx.cublasHandle(), CUBLAS_POINTER_MODE_HOST)); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaFree(d_result)); | |
| } | |
| BENCHMARK(BM_DotRawCublas)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| // The reduction into a DeviceScalar allocated once: no allocation per call. | |
| static void BM_DotIntoDeviceScalar(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| DeviceMatrix d_y = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| gpu::DeviceScalar<Scalar> r(ctx.stream()); | |
| for (auto _ : state) { | |
| d_x.dot(ctx, d_y, r); | |
| benchmark::DoNotOptimize(r.devicePtr()); | |
| syncStream(ctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_DotIntoDeviceScalar)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| // norm() is a dot and a one-element NPP sqrt; stableNorm() is cuBLAS nrm2's | |
| // scaled accumulation. Each iteration reads the norm back to the host, as the | |
| // convergence test of an iterative method does. | |
| static void BM_NormRead(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| gpu::DeviceScalar<Scalar> r(ctx.stream()); | |
| for (auto _ : state) { | |
| d_x.norm(ctx, r); | |
| Scalar value = r; | |
| benchmark::DoNotOptimize(value); | |
| } | |
| } | |
| BENCHMARK(BM_NormRead)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_StableNormRead(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), ctx.stream()); | |
| gpu::DeviceScalar<Scalar> r(ctx.stream()); | |
| for (auto _ : state) { | |
| d_x.stableNorm(ctx, r); | |
| Scalar value = r; | |
| benchmark::DoNotOptimize(value); | |
| } | |
| } | |
| BENCHMARK(BM_StableNormRead)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 4. Cholesky solve paths. | |
| // OneShotLltExpr: d_X = d_A.llt().solve(d_B) — re-factorizes, allocates | |
| // d_factor/workspace/pinned-info per call, and ends in a mandatory | |
| // cudaStreamSynchronize inside the dispatch. | |
| // CachedLltSolve: gpu::LLT computed once; solve() still allocates a fresh | |
| // RHS DeviceBuffer per call and syncs info on the first call. | |
| // RawPotrs: baseline — potrs into pre-allocated buffers, nothing else. | |
| // The gap between CachedLltSolve and RawPotrs is the per-solve overhead | |
| // attributable to the wrapper (allocation + event bookkeeping). | |
| // --------------------------------------------------------------------------- | |
| static void BM_OneShotLltExpr(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| const Index nrhs = 8; | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_A = DeviceMatrix::fromHost(spdMatrix(n), ctx.stream()); | |
| DeviceMatrix d_B = DeviceMatrix::fromHost(HostMatrix::Random(n, nrhs), ctx.stream()); | |
| DeviceMatrix d_X(n, nrhs); | |
| for (auto _ : state) { | |
| d_X.device(ctx) = d_A.llt().solve(d_B); | |
| syncStream(ctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_OneShotLltExpr)->Arg(256)->Arg(1024)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_CachedLltSolve(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| const Index nrhs = 8; | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| DeviceMatrix d_A = DeviceMatrix::fromHost(spdMatrix(n), ctx.stream()); | |
| DeviceMatrix d_B = DeviceMatrix::fromHost(HostMatrix::Random(n, nrhs), ctx.stream()); | |
| gpu::LLT<Scalar> llt; | |
| llt.compute(d_A); | |
| benchmark::DoNotOptimize(llt.info()); // factor + first-solve sync out of the loop | |
| for (auto _ : state) { | |
| DeviceMatrix d_X = llt.solve(d_B); | |
| syncStream(llt.stream()); | |
| benchmark::DoNotOptimize(d_X.data()); | |
| } | |
| } | |
| BENCHMARK(BM_CachedLltSolve)->Arg(256)->Arg(1024)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_RawPotrs(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| const Index nrhs = 8; | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| cusolverDnHandle_t solver = ctx.cusolverHandle(); | |
| // Factor once with the module (correctness is not under test here), then | |
| // extract the factor into a raw buffer for repeated potrs calls. | |
| HostMatrix A = spdMatrix(n); | |
| Eigen::LLT<HostMatrix> hostLlt(A); | |
| HostMatrix L = HostMatrix(hostLlt.matrixL()); | |
| DeviceMatrix d_L = DeviceMatrix::fromHost(L, ctx.stream()); | |
| DeviceMatrix d_B = DeviceMatrix::fromHost(HostMatrix::Random(n, nrhs), ctx.stream()); | |
| DeviceMatrix d_X(n, nrhs); | |
| gpu::internal::CusolverParams params; | |
| int* d_info = nullptr; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMalloc(&d_info, sizeof(int))); | |
| constexpr cudaDataType_t dtype = gpu::internal::cuda_data_type<Scalar>::value; | |
| for (auto _ : state) { | |
| EIGEN_CUDA_RUNTIME_CHECK( | |
| cudaMemcpyAsync(d_X.data(), d_B.data(), sizeof(Scalar) * n * nrhs, cudaMemcpyDeviceToDevice, ctx.stream())); | |
| EIGEN_CUSOLVER_CHECK(cusolverDnXpotrs(solver, params.p, CUBLAS_FILL_MODE_LOWER, n, nrhs, dtype, d_L.data(), n, | |
| dtype, d_X.data(), n, d_info)); | |
| syncStream(ctx.stream()); | |
| } | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaFree(d_info)); | |
| } | |
| BENCHMARK(BM_RawPotrs)->Arg(256)->Arg(1024)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 4b. Device-resident SpMV in a loop (the CG hot path): measures per-call | |
| // cuSPARSE descriptor/bufferSize overhead on the cached deviceView path. | |
| // --------------------------------------------------------------------------- | |
| static void BM_DeviceSpMV(benchmark::State& state) { | |
| const Index n = state.range(0); | |
| using SpMat = Eigen::SparseMatrix<Scalar, Eigen::ColMajor, int>; | |
| SpMat A(n, n); | |
| // 1D Laplacian stencil. | |
| A.reserve(Eigen::VectorXi::Constant(n, 3)); | |
| for (Index i = 0; i < n; ++i) { | |
| A.insert(i, i) = Scalar(2); | |
| if (i > 0) A.insert(i - 1, i) = Scalar(-1); | |
| if (i + 1 < n) A.insert(i + 1, i) = Scalar(-1); | |
| } | |
| A.makeCompressed(); | |
| gpu::Context& gctx = gpu::Context::threadLocal(); | |
| gpu::SparseContext<Scalar> ctx(gctx); | |
| auto view = ctx.deviceView(A); | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1), gctx.stream()); | |
| DeviceMatrix d_y(n, 1); | |
| for (auto _ : state) { | |
| d_y = view * d_x; | |
| syncStream(gctx.stream()); | |
| } | |
| } | |
| BENCHMARK(BM_DeviceSpMV)->Arg(1 << 12)->Arg(1 << 20)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 5. Allocator microbenchmark: cudaMalloc/cudaFree (what DeviceMatrix uses | |
| // today) vs cudaMallocAsync/cudaFreeAsync (stream-ordered pool). Supports | |
| // the proposal to switch DeviceMatrix storage to stream-ordered allocation. | |
| // --------------------------------------------------------------------------- | |
| static void BM_CudaMallocFree(benchmark::State& state) { | |
| const size_t bytes = static_cast<size_t>(state.range(0)); | |
| for (auto _ : state) { | |
| void* p = nullptr; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMalloc(&p, bytes)); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaFree(p)); | |
| } | |
| } | |
| BENCHMARK(BM_CudaMallocFree)->Arg(1 << 10)->Arg(1 << 20)->Arg(1 << 26)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_CudaMallocAsyncFree(benchmark::State& state) { | |
| const size_t bytes = static_cast<size_t>(state.range(0)); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| int pools_supported = 0; | |
| int device = 0; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaGetDevice(&device)); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaDeviceGetAttribute(&pools_supported, cudaDevAttrMemoryPoolsSupported, device)); | |
| if (!pools_supported) { | |
| state.SkipWithError("cudaMallocAsync not supported on this device"); | |
| return; | |
| } | |
| for (auto _ : state) { | |
| void* p = nullptr; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMallocAsync(&p, bytes, ctx.stream())); | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaFreeAsync(p, ctx.stream())); | |
| } | |
| syncStream(ctx.stream()); | |
| } | |
| BENCHMARK(BM_CudaMallocAsyncFree)->Arg(1 << 10)->Arg(1 << 20)->Arg(1 << 26)->UseRealTime()->MinWarmUpTime(0.5); | |
| // --------------------------------------------------------------------------- | |
| // 6. Small buffers: a DeviceBuffer below DeviceBufferPool's threshold takes | |
| // the stream-ordered allocator where memory pools exist and the thread-local | |
| // pool otherwise; PoolAllocFree drives the pool directly, paying one event | |
| // record per release and one query per reuse. Behind an in-flight kernel a | |
| // release trails that kernel, so a reuse finds it retired or falls through | |
| // to the allocator (see CudaMallocAsyncFree). | |
| // --------------------------------------------------------------------------- | |
| static void BM_SmallBufferAllocFree(benchmark::State& state) { | |
| const size_t bytes = static_cast<size_t>(state.range(0)); | |
| for (auto _ : state) { | |
| gpu::internal::DeviceBuffer b(bytes); | |
| benchmark::DoNotOptimize(b.get()); | |
| } | |
| } | |
| BENCHMARK(BM_SmallBufferAllocFree)->Arg(8)->Arg(64)->Arg(256)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_PoolAllocFree(benchmark::State& state) { | |
| const size_t bytes = static_cast<size_t>(state.range(0)); | |
| auto& pool = gpu::internal::DeviceBufferPool<>::threadLocal(); | |
| for (auto _ : state) { | |
| void* p = pool.allocate(bytes); | |
| benchmark::DoNotOptimize(p); | |
| pool.deallocate(p, bytes); | |
| } | |
| } | |
| BENCHMARK(BM_PoolAllocFree)->Arg(8)->Arg(256)->UseRealTime()->MinWarmUpTime(0.5); | |
| static void BM_SmallBufferAllocFreeBusyStream(benchmark::State& state) { | |
| const size_t bytes = static_cast<size_t>(state.range(0)); | |
| gpu::Context& ctx = gpu::Context::threadLocal(); | |
| const Index n = Index(1) << 22; | |
| DeviceMatrix d_x = DeviceMatrix::fromHost(HostMatrix::Random(n, 1)); | |
| DeviceMatrix d_y = DeviceMatrix::fromHost(HostMatrix::Random(n, 1)); | |
| for (auto _ : state) { | |
| // ~1 ms of work ahead of each release, as in an iterative solver loop. | |
| gpu::DeviceScalar<Scalar> r = d_x.dot(ctx, d_y); | |
| gpu::internal::DeviceBuffer b(bytes); | |
| benchmark::DoNotOptimize(b.get()); | |
| benchmark::DoNotOptimize(r.devicePtr()); | |
| } | |
| syncStream(ctx.stream()); | |
| } | |
| BENCHMARK(BM_SmallBufferAllocFreeBusyStream)->Arg(8)->Arg(64)->UseRealTime()->MinWarmUpTime(0.5); |