blob: 33b05cf4333b7b58a497880cce6b241f5dc12f9e [file]
// 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