| // 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 |
| |
| // Shared context for the dense GPU solvers. Each solver holds one by composition |
| // and delegates handle lifetime and scratch management to it. |
| |
| #ifndef EIGEN_GPU_SOLVER_CONTEXT_H |
| #define EIGEN_GPU_SOLVER_CONTEXT_H |
| |
| // IWYU pragma: private |
| #include "./InternalHeaderCheck.h" |
| |
| #include "./CuSolverSupport.h" |
| #include "./CuBlasSupport.h" |
| #include "./GpuContext.h" |
| #include <vector> |
| |
| namespace Eigen { |
| namespace gpu { |
| namespace internal { |
| |
| struct GpuSolverContext { |
| Context* bound_ctx_ = nullptr; |
| UniqueStream stream_; |
| UniqueCusolverHandle cusolver_; |
| UniqueCublasHandle cublas_; |
| UniqueCublasLtHandle cublas_lt_; // lazy: created on first GEMM-via-cublasLt call (standalone mode only) |
| CusolverParams params_; |
| DeviceBuffer d_scratch_; |
| std::vector<char> h_workspace_; |
| DeviceBuffer gemm_workspace_; // grown lazily by cublaslt_gemm |
| CublasLtPlanCache gemm_plan_cache_{kCublasLtPlanCacheCapacity}; |
| // Workspace ceiling fed to the cublasLtMatmul heuristic at plan-creation time. |
| // See gpu::Context::setCublasLtMaxWorkspaceBytes() for semantics. |
| std::size_t cublaslt_max_workspace_bytes_ = kCublasLtMaxWorkspaceBytes; |
| ComputationInfo info_ = InvalidInput; |
| PinnedHostBuffer pinned_info_{sizeof(int)}; // pinned host memory for async D2H of info word |
| bool info_synced_ = true; |
| |
| int& info_word() { return *static_cast<int*>(pinned_info_.get()); } |
| int info_word() const { return *static_cast<const int*>(pinned_info_.get()); } |
| |
| cudaStream_t stream() const { return stream_.get(); } |
| cusolverDnHandle_t cusolverHandle() const { return cusolver_.get(); } |
| cublasHandle_t cublasHandle() const { return cublas_.get(); } |
| |
| GpuSolverContext() { |
| cudaStream_t s = nullptr; |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamCreate(&s)); |
| stream_ = UniqueStream(s); |
| cusolverDnHandle_t solver = nullptr; |
| EIGEN_CUSOLVER_CHECK(cusolverDnCreate(&solver)); |
| cusolver_ = UniqueCusolverHandle(solver); |
| EIGEN_CUSOLVER_CHECK(cusolverDnSetStream(solver, s)); |
| cublasHandle_t blas = nullptr; |
| EIGEN_CUBLAS_CHECK(cublasCreate(&blas)); |
| cublas_ = UniqueCublasHandle(blas); |
| EIGEN_CUBLAS_CHECK(cublasSetStream(blas, s)); |
| ensure_scratch(0); |
| } |
| |
| /** Borrow stream and cuSOLVER/cuBLAS handles from a gpu::Context, so solver |
| * work runs on the same stream as the caller's other GPU operations (no |
| * cross-stream event waits, and the solver creates no stream/handles of its |
| * own). The cuBLASLt handle, GEMM plan cache, and GEMM workspace are shared |
| * with the Context as well. The Context must outlive this solver context. */ |
| explicit GpuSolverContext(Context& ctx) |
| : bound_ctx_(&ctx), |
| stream_(ctx.stream(), CudaStreamDeleter{/*owns=*/false}), |
| cusolver_(ctx.cusolverHandle(), CusolverHandleDeleter{/*owns=*/false}), |
| cublas_(ctx.cublasHandle(), CublasHandleDeleter{/*owns=*/false}) { |
| ensure_scratch(0); |
| } |
| |
| ~GpuSolverContext() = default; |
| GpuSolverContext(GpuSolverContext&& o) noexcept = default; |
| |
| GpuSolverContext& operator=(GpuSolverContext&& o) noexcept { |
| if (this != &o) { |
| // A pending info copy may still write pinned_info_, whose cudaFreeHost deleter is not stream-ordered. |
| if (!info_synced_ && pinned_info_) (void)cudaStreamSynchronize(stream()); |
| // Release plan-cache descriptors before the moves below replace the cuBLASLt handle they were built with. |
| gemm_plan_cache_.clear(); |
| bound_ctx_ = o.bound_ctx_; |
| stream_ = std::move(o.stream_); |
| cusolver_ = std::move(o.cusolver_); |
| cublas_ = std::move(o.cublas_); |
| cublas_lt_ = std::move(o.cublas_lt_); |
| params_ = std::move(o.params_); |
| d_scratch_ = std::move(o.d_scratch_); |
| h_workspace_ = std::move(o.h_workspace_); |
| gemm_workspace_ = std::move(o.gemm_workspace_); |
| gemm_plan_cache_ = std::move(o.gemm_plan_cache_); |
| cublaslt_max_workspace_bytes_ = o.cublaslt_max_workspace_bytes_; |
| info_ = o.info_; |
| pinned_info_ = std::move(o.pinned_info_); |
| info_synced_ = o.info_synced_; |
| o.bound_ctx_ = nullptr; |
| } |
| return *this; |
| } |
| |
| /** cuBLASLt handle: the bound Context's when borrowing, otherwise an owned |
| * handle lazy-initialized on first GEMM-via-cublasLt call. */ |
| cublasLtHandle_t cublasLtHandle() { |
| if (bound_ctx_) return bound_ctx_->cublasLtHandle(); |
| if (!cublas_lt_) { |
| cublasLtHandle_t h = nullptr; |
| EIGEN_CUBLAS_CHECK(cublasLtCreate(&h)); |
| cublas_lt_ = UniqueCublasLtHandle(h); |
| } |
| return cublas_lt_.get(); |
| } |
| |
| /** GEMM plan cache / workspace / workspace ceiling for cublaslt_gemm — |
| * shared with the bound Context when borrowing, owned otherwise. */ |
| CublasLtPlanCache& gemmPlanCache() { return bound_ctx_ ? bound_ctx_->gemmPlanCache() : gemm_plan_cache_; } |
| DeviceBuffer& gemmWorkspace() { return bound_ctx_ ? bound_ctx_->gemmWorkspace() : gemm_workspace_; } |
| std::size_t cublasLtMaxWorkspaceBytes() const { |
| return bound_ctx_ ? bound_ctx_->cublasLtMaxWorkspaceBytes() : cublaslt_max_workspace_bytes_; |
| } |
| |
| GpuSolverContext(const GpuSolverContext&) = delete; |
| GpuSolverContext& operator=(const GpuSolverContext&) = delete; |
| |
| // Scratch layout: [ workspace (aligned) | info_word (sizeof(int)) ]. |
| // Workspace size is rounded up to 16 bytes so the info word lands aligned. |
| static constexpr size_t kInfoBytes = sizeof(int); |
| static constexpr size_t kScratchAlign = 16; |
| |
| static size_t scratchBytesFor(size_t workspace_bytes) { |
| workspace_bytes = (workspace_bytes + kScratchAlign - 1) & ~(kScratchAlign - 1); |
| return workspace_bytes + kInfoBytes; |
| } |
| |
| // Ensure d_scratch_ holds at least `workspace_bytes` of scratch plus the trailing |
| // info word. Grows but never shrinks. Syncs the stream before reallocating to |
| // avoid freeing memory that async kernels may still be using. |
| void ensure_scratch(size_t workspace_bytes) { |
| size_t needed = scratchBytesFor(workspace_bytes); |
| if (needed > d_scratch_.size()) { |
| if (d_scratch_) EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream())); |
| d_scratch_ = DeviceBuffer(needed); |
| } |
| } |
| |
| void* scratch_workspace() const { return d_scratch_.get(); } |
| |
| int* scratch_info() const { |
| eigen_assert(d_scratch_ && d_scratch_.size() >= kInfoBytes); |
| return reinterpret_cast<int*>(static_cast<char*>(d_scratch_.get()) + d_scratch_.size() - kInfoBytes); |
| } |
| |
| // Mark a factorization as pending: its info word is not yet available. |
| void mark_pending() { |
| info_synced_ = false; |
| info_ = InvalidInput; |
| } |
| |
| // Common compute() prologue: reset info state. Returns false for the empty |
| // (n == 0) case, which is trivially successful — the caller returns early. |
| bool begin_compute(bool nonempty) { |
| info_ = InvalidInput; |
| if (!nonempty) { |
| info_ = Success; |
| info_synced_ = true; |
| return false; |
| } |
| return true; |
| } |
| |
| // Common factorize() epilogue: enqueue the async D2H copy of the info word |
| // into pinned host memory. Read later by the lazy sync_info(). |
| void enqueue_info_copy() { |
| EIGEN_CUDA_RUNTIME_CHECK( |
| cudaMemcpyAsync(&info_word(), scratch_info(), sizeof(int), cudaMemcpyDeviceToHost, stream())); |
| } |
| |
| // Synchronize the stream and interpret the info word; no-op once synced. |
| void sync_info() { |
| if (!info_synced_) { |
| EIGEN_CUDA_RUNTIME_CHECK(cudaStreamSynchronize(stream())); |
| info_ = (info_word() == 0) ? Success : NumericalIssue; |
| info_synced_ = true; |
| } |
| } |
| |
| ComputationInfo info() { |
| sync_info(); |
| return info_; |
| } |
| }; |
| |
| } // namespace internal |
| } // namespace gpu |
| } // namespace Eigen |
| |
| #endif // EIGEN_GPU_SOLVER_CONTEXT_H |