blob: fed40e48714a309743290470fbf1fd7fd5567fae [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
// Unified GPU execution context: a CUDA stream plus the NVIDIA library handles
// used by gpu::DeviceMatrix operations.
#ifndef EIGEN_GPU_CONTEXT_H
#define EIGEN_GPU_CONTEXT_H
// IWYU pragma: private
#include "./InternalHeaderCheck.h"
#include "./CuBlasSupport.h"
#include "./CuSolverSupport.h"
#include <cusparse.h>
#include <vector>
namespace Eigen {
namespace gpu {
namespace internal {
// cuSOLVER writes the factorization/solve status words to device memory in
// every build, so d_info must exist even under EIGEN_NO_DEBUG. Only the
// pinned host mirror is debug-only: it feeds the oneshot_check_info assert,
// which release builds compile out.
constexpr size_t kOneShotInfoBytes = 2 * sizeof(int);
#ifdef EIGEN_NO_DEBUG
constexpr size_t kOneShotHostInfoBytes = 0;
#else
constexpr size_t kOneShotHostInfoBytes = kOneShotInfoBytes;
#endif
// Grow-only scratch shared by the one-shot solver expressions
// (d_A.llt().solve(d_B), d_A.lu().solve(d_B)), so repeated one-shot solves on
// a Context perform no per-call device or pinned-host allocations. Holds only
// CUDA-runtime types (no cuSOLVER types) to keep the lazy-linking property of
// Context. Used by the one-shot solve dispatches in DeviceDispatch.h.
struct OneShotSolverScratch {
DeviceBuffer d_factor;
DeviceBuffer d_ipiv;
DeviceBuffer d_workspace;
DeviceBuffer d_info{kOneShotInfoBytes}; // 2 ints: {factorization, solve}
PinnedHostBuffer h_info{kOneShotHostInfoBytes}; // debug-build info check only
std::vector<char> h_workspace;
};
inline void ensure_sized(DeviceBuffer& buf, size_t needed) {
if (needed > buf.size()) {
// Replacing an in-use buffer is safe: device_free is stream-ordered (or
// fully synchronous on the cudaMalloc fallback path) and DeviceBufferPool
// holds a released block back until the device has retired the work
// enqueued before the release, so nothing reuses the old buffer early.
buf = DeviceBuffer(needed);
}
}
} // namespace internal
/** \ingroup GPU_Module
* \class Context
* \brief Unified GPU execution context owning a CUDA stream and library handles.
*
* Each Context creates a dedicated CUDA stream and an eager cuBLAS handle bound
* to it; multiple contexts run concurrently on independent streams. The
* cuSOLVER, cuBLASLt, and cuSPARSE handles are created on first use, so a
* translation unit that never touches cuSOLVER (the cuFFT test, say) does not
* need it at link time. threadLocal() supplies a lazily-created default for
* simple single-stream usage.
*
* A Context is not thread-safe: the library handles are not thread-safe per
* handle and the lazy initialization above is racy, so use one per thread or
* synchronize externally.
*/
class Context {
public:
/** Create a new context with a dedicated CUDA stream. */
Context() {
cudaStream_t s = nullptr;
EIGEN_CUDA_RUNTIME_CHECK(cudaStreamCreate(&s));
stream_ = internal::UniqueStream(s);
init_cublas();
}
/** Create a context on an existing stream (e.g., stream 0 = nullptr).
* The caller retains ownership of the stream — this context will not destroy it. */
explicit Context(cudaStream_t stream) : stream_(stream, internal::CudaStreamDeleter{/*owns=*/false}) {
init_cublas();
}
~Context() = default;
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
Context(Context&&) = delete;
Context& operator=(Context&&) = delete;
/** Get the thread-local default context.
*
* If setThreadLocal() has been called, returns that context.
* Otherwise lazily creates a new context with a dedicated stream.
*
* \note The thread-local instance is destroyed when the thread exits (or at
* static destruction time for the main thread). On some CUDA driver
* configurations this may print "CUDA_ERROR_DEINITIALIZED" to stderr if the
* CUDA context has already been torn down. These errors are harmless and are
* suppressed in the destructor, but they can produce noise in test output.
* To avoid this, call cudaDeviceReset() only after all Context instances
* (including thread-local ones) have been destroyed — or create and own a
* Context and install it with setThreadLocal(): the lazily-created default
* is then never constructed, and teardown order is fully under application
* control. */
static Context& threadLocal() {
Context* override = tl_override_ptr();
if (override) return *override;
thread_local Context ctx;
return ctx;
}
/** Override the thread-local default context for this thread.
* The caller retains ownership of \p ctx — it must outlive all uses.
* Pass nullptr to restore the lazily-created default. */
static void setThreadLocal(Context* ctx) { tl_override_ptr() = ctx; }
cudaStream_t stream() const { return stream_.get(); }
cublasHandle_t cublasHandle() const { return cublas_.get(); }
/** Returns the cuSOLVER handle, creating it on first call. */
cusolverDnHandle_t cusolverHandle() {
if (!cusolver_) {
cusolverDnHandle_t h = nullptr;
EIGEN_CUSOLVER_CHECK(cusolverDnCreate(&h));
cusolver_ = LazyCusolverHandle(h, &destroyCusolver);
EIGEN_CUSOLVER_CHECK(cusolverDnSetStream(h, stream_.get()));
}
return cusolver_.get();
}
/** cuBLASLt handle (lazy-initialized on first GEMM call). */
cublasLtHandle_t cublasLtHandle() {
if (!cublas_lt_) {
cublasLtHandle_t h = nullptr;
EIGEN_CUBLAS_CHECK(cublasLtCreate(&h));
cublas_lt_ = internal::UniqueCublasLtHandle(h);
}
return cublas_lt_.get();
}
/** Workspace buffer for cublasLtMatmul (grown lazily by cublaslt_gemm).
* Not thread-safe — all GEMM calls must be on this context's stream. */
internal::DeviceBuffer& gemmWorkspace() { return gemm_workspace_; }
/** Plan cache for cublasLtMatmul (caches descriptors and selected algorithm
* by shape to avoid per-call overhead). Same thread-safety as workspace. */
internal::CublasLtPlanCache& gemmPlanCache() { return gemm_plan_cache_; }
/** Grow-only scratch for the one-shot solver expressions
* (d_A.llt().solve(d_B), d_A.lu().solve(d_B)). Same thread-safety rules as
* the GEMM workspace: all uses must be on this context's stream. */
internal::OneShotSolverScratch& oneshotSolverScratch() { return oneshot_solver_scratch_; }
/** Workspace ceiling passed to the cublasLtMatmul heuristic at plan-creation time.
* Defaults to internal::kCublasLtMaxWorkspaceBytes (compile-time configurable via
* EIGEN_CUDA_CUBLASLT_MAX_WORKSPACE_BYTES). */
std::size_t cublasLtMaxWorkspaceBytes() const { return cublaslt_max_workspace_bytes_; }
/** Override the workspace ceiling for future plan-cache misses on this context.
* The cap is consulted at plan-creation time only; pre-existing cached plans
* keep the cap they were built with. Call gemmPlanCache().clear() to force
* re-selection under the new cap. */
void setCublasLtMaxWorkspaceBytes(std::size_t bytes) { cublaslt_max_workspace_bytes_ = bytes; }
/** cuSPARSE handle, created on first use. */
cusparseHandle_t cusparseHandle() {
if (!cusparse_) {
cusparseHandle_t h = nullptr;
cusparseStatus_t s1 = cusparseCreate(&h);
eigen_assert(s1 == CUSPARSE_STATUS_SUCCESS && "cusparseCreate failed");
EIGEN_UNUSED_VARIABLE(s1);
cusparse_ = LazyCusparseHandle(h, &destroyCusparse);
cusparseStatus_t s2 = cusparseSetStream(h, stream_.get());
eigen_assert(s2 == CUSPARSE_STATUS_SUCCESS && "cusparseSetStream failed");
EIGEN_UNUSED_VARIABLE(s2);
}
return cusparse_.get();
}
private:
static cusolverStatus_t destroyCusolver(cusolverDnHandle_t h) { return cusolverDnDestroy(h); }
static cusparseStatus_t destroyCusparse(cusparseHandle_t h) { return cusparseDestroy(h); }
// Function-pointer deleters keep cusolverDnDestroy / cusparseDestroy referenced only by TUs that create handles.
using LazyCusolverHandle =
std::unique_ptr<std::remove_pointer_t<cusolverDnHandle_t>, cusolverStatus_t (*)(cusolverDnHandle_t)>;
using LazyCusparseHandle =
std::unique_ptr<std::remove_pointer_t<cusparseHandle_t>, cusparseStatus_t (*)(cusparseHandle_t)>;
// Destroyed in reverse declaration order: the plan cache before the cuBLASLt handle, the stream last.
internal::UniqueStream stream_;
internal::UniqueCublasHandle cublas_;
LazyCusolverHandle cusolver_{nullptr, nullptr};
LazyCusparseHandle cusparse_{nullptr, nullptr};
internal::UniqueCublasLtHandle cublas_lt_; // lazy
internal::DeviceBuffer gemm_workspace_; // lazy
internal::CublasLtPlanCache gemm_plan_cache_{internal::kCublasLtPlanCacheCapacity};
internal::OneShotSolverScratch oneshot_solver_scratch_; // grow-only
std::size_t cublaslt_max_workspace_bytes_ = internal::kCublasLtMaxWorkspaceBytes;
static Context*& tl_override_ptr() {
thread_local Context* ptr = nullptr;
return ptr;
}
void init_cublas() {
cublasHandle_t h = nullptr;
EIGEN_CUBLAS_CHECK(cublasCreate(&h));
cublas_ = internal::UniqueCublasHandle(h);
EIGEN_CUBLAS_CHECK(cublasSetStream(h, stream_.get()));
}
};
} // namespace gpu
} // namespace Eigen
#endif // EIGEN_GPU_CONTEXT_H