| // 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 | |
| // Generic CUDA runtime support shared by all GPU library integrations. | |
| // Depends only on <cuda_runtime.h>; no NVIDIA library headers. | |
| #ifndef EIGEN_GPU_SUPPORT_H | |
| #define EIGEN_GPU_SUPPORT_H | |
| // IWYU pragma: private | |
| #include "./InternalHeaderCheck.h" | |
| #include <cuda_runtime.h> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <vector> | |
| #include <limits> | |
| #include <memory> | |
| #include <type_traits> | |
| namespace Eigen { | |
| namespace gpu { | |
| // Transpose/adjoint flag for BLAS-, solver-, and sparse-style calls. Each | |
| // library's support header maps it to its own enum (cublasOperation_t, | |
| // cusparseOperation_t, ...) via a to_<lib>_op() helper. | |
| enum class GpuOp { NoTrans, Trans, ConjTrans }; | |
| namespace internal { | |
| // Prints `file:line: call: error` to stderr and stops: std::abort() where | |
| // assertions are compiled out, a failed eigen_assert otherwise. No build goes | |
| // on past a failed call: its work was not done, and a sticky error leaves the | |
| // context unusable for every later call. | |
| inline void gpu_check_failed(const char* error, const char* expression, const char* file, int line) { | |
| std::fprintf(stderr, "%s:%d: %s: %s\n", file, line, expression, error); | |
| #if defined(EIGEN_NO_DEBUG) | |
| std::abort(); | |
| #else | |
| eigen_assert(false && "GPU runtime or library call failed"); | |
| #endif | |
| } | |
| // Every check macro of the module reports failures here, in every build. It may | |
| // be defined to throw, so no destructor or noexcept function uses the checks. | |
| #ifndef EIGEN_GPU_CHECK_FAILED | |
| #define EIGEN_GPU_CHECK_FAILED(error, expression, file, line) \ | |
| ::Eigen::gpu::internal::gpu_check_failed(error, expression, file, line) | |
| #endif | |
| #define EIGEN_CUDA_RUNTIME_CHECK(expr) \ | |
| do { \ | |
| const cudaError_t _e = (expr); \ | |
| if (_e != cudaSuccess) EIGEN_GPU_CHECK_FAILED(cudaGetErrorName(_e), #expr, __FILE__, __LINE__); \ | |
| } while (0) | |
| // For the libraries without a status-to-string function: "<library> status <code>". | |
| inline void gpu_check_failed_code(const char* library, int status, const char* expression, const char* file, int line) { | |
| char error[64]; | |
| std::snprintf(error, sizeof(error), "%s status %d", library, status); | |
| // A user-defined EIGEN_GPU_CHECK_FAILED need not use every argument. | |
| EIGEN_UNUSED_VARIABLE(expression); | |
| EIGEN_UNUSED_VARIABLE(file); | |
| EIGEN_UNUSED_VARIABLE(line); | |
| EIGEN_GPU_CHECK_FAILED(error, expression, file, line); | |
| } | |
| // cuBLAS and the legacy cuSOLVER APIs take dimensions and leading dimensions as | |
| // 32-bit `int`, while Eigen's Index is 64-bit by default and GPU allocations can | |
| // exceed INT_MAX in one dimension. Narrow through this helper at every such call | |
| // site so an out-of-range value asserts instead of silently overflowing. | |
| inline int to_blas_int(int64_t v) { | |
| eigen_assert(v >= 0 && v <= static_cast<int64_t>((std::numeric_limits<int>::max)()) && | |
| "dimension exceeds the int range supported by cuBLAS / cuSOLVER"); | |
| return static_cast<int>(v); | |
| } | |
| // cudaMallocAsync / cudaFreeAsync (CUDA 11.2+) allocate from a stream-ordered | |
| // memory pool: both are cheap enqueues instead of the device-wide | |
| // synchronization performed by cudaMalloc / cudaFree. All module allocations | |
| // go through device_malloc / device_free on the *legacy default stream*: | |
| // legacy-stream ordering guarantees that work enqueued later on any blocking | |
| // stream observes the allocation, and that a free waits for all previously | |
| // enqueued work on blocking streams — the same lifetime guarantees callers | |
| // got from cudaMalloc / cudaFree, minus the host stalls. | |
| // | |
| // Caveat: streams created with cudaStreamNonBlocking do not synchronize with | |
| // the legacy stream. When borrowing such a stream (gpu::Context(stream)), | |
| // define EIGEN_GPU_NO_STREAM_ORDERED_ALLOC to fall back to cudaMalloc/cudaFree. | |
| // | |
| // Support is detected once per process, from the device current at first use. | |
| inline bool device_supports_memory_pools() { | |
| #ifdef EIGEN_GPU_NO_STREAM_ORDERED_ALLOC | |
| return false; | |
| #else | |
| static const bool supported = [] { | |
| int device = 0; | |
| if (cudaGetDevice(&device) != cudaSuccess) return false; | |
| int v = 0; | |
| if (cudaDeviceGetAttribute(&v, cudaDevAttrMemoryPoolsSupported, device) != cudaSuccess) return false; | |
| if (v == 0) return false; | |
| // Keep freed memory in the pool instead of trimming at every stream | |
| // synchronize — repeated alloc/free cycles (temporaries in loops) then | |
| // recycle at user-space speed. | |
| cudaMemPool_t pool = nullptr; | |
| if (cudaDeviceGetDefaultMemPool(&pool, device) == cudaSuccess) { | |
| // The attribute value type is cuuint64_t; use a same-size stand-in to | |
| // avoid requiring the driver-API header. | |
| unsigned long long threshold = ~0ULL; | |
| (void)cudaMemPoolSetAttribute(pool, cudaMemPoolAttrReleaseThreshold, &threshold); | |
| } | |
| return true; | |
| }(); | |
| return supported; | |
| #endif | |
| } | |
| inline void* device_malloc(size_t bytes) { | |
| void* p = nullptr; | |
| if (device_supports_memory_pools()) { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMallocAsync(&p, bytes, /*legacy default stream*/ nullptr)); | |
| } else { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMalloc(&p, bytes)); | |
| } | |
| return p; | |
| } | |
| inline void device_free(void* p) noexcept { | |
| if (!p) return; | |
| if (device_supports_memory_pools()) { | |
| (void)cudaFreeAsync(p, /*legacy default stream*/ nullptr); | |
| } else { | |
| (void)cudaFree(p); | |
| } | |
| } | |
| struct CudaFreeDeleter { | |
| // When `borrow == true`, the unique_ptr does not free the pointer. Used by | |
| // DeviceMatrix::view() to wrap a non-owning device pointer with the same | |
| // smart-pointer machinery as owning storage, without changing the type. | |
| bool borrow = false; | |
| void operator()(void* p) const noexcept { | |
| if (p && !borrow) device_free(p); | |
| } | |
| }; | |
| struct CudaFreeHostDeleter { | |
| void operator()(void* p) const noexcept { | |
| if (p) (void)cudaFreeHost(p); | |
| } | |
| }; | |
| // RAII CUDA stream; the ownership flag supports borrowed, caller-owned streams. | |
| struct CudaStreamDeleter { | |
| bool owns = true; | |
| void operator()(cudaStream_t s) const noexcept { | |
| if (owns && s) (void)cudaStreamDestroy(s); | |
| } | |
| }; | |
| using UniqueStream = std::unique_ptr<std::remove_pointer_t<cudaStream_t>, CudaStreamDeleter>; | |
| // Recycles allocations up to kSmallBufferThreshold bytes (e.g. DeviceScalar) to | |
| // avoid cudaMalloc/cudaFree overhead on devices without memory pools; where | |
| // cudaMallocAsync exists it is both stream-ordered and cheaper than this pool's | |
| // release event, so DeviceBuffer bypasses the pool there (bench_overhead: | |
| // CudaMallocAsyncFree vs PoolAllocFree). Larger allocations always bypass it. | |
| // Invariant: a block is recycled only after the device has retired every | |
| // operation enqueued before its release on any blocking stream: deallocate() | |
| // records an event on the legacy default stream (the ordering device_free | |
| // relies on), the free list stays in release order, and events on one stream | |
| // retire in order, so allocate() scans until the first pending entry. | |
| template <size_t SmallBufferThreshold = 256, size_t MaxPoolSize = 64> | |
| struct DeviceBufferPool { | |
| static constexpr size_t kSmallBufferThreshold = SmallBufferThreshold; | |
| static constexpr size_t kMaxPoolSize = MaxPoolSize; | |
| struct Entry { | |
| void* ptr; | |
| size_t bytes; | |
| cudaEvent_t release_event; | |
| }; | |
| // Lifetime marker for the thread-local pool. thread_local destruction runs | |
| // in reverse construction order, so a long-lived object holding pooled | |
| // buffers (e.g. the thread-local gpu::Context, or a static) can be | |
| // destroyed *after* the pool. The marker is trivially destructible — it | |
| // stays readable during TLS teardown — letting the deleter fall back to a | |
| // direct device_free once the pool is gone instead of touching a destroyed | |
| // vector. | |
| enum class State : signed char { kNotConstructed = 0, kAlive = 1, kDestroyed = 2 }; | |
| static State& threadState() { | |
| thread_local State state = State::kNotConstructed; | |
| return state; | |
| } | |
| DeviceBufferPool() { threadState() = State::kAlive; } | |
| ~DeviceBufferPool() { | |
| for (const Entry& entry : free_list_) freeBlock(entry.ptr, entry.release_event); | |
| for (cudaEvent_t event : spare_events_) (void)cudaEventDestroy(event); | |
| threadState() = State::kDestroyed; | |
| } | |
| // First fit among the retired blocks, oldest release first; the first pending | |
| // entry ends the scan because every later one is pending too. | |
| void* allocate(size_t bytes) { | |
| for (auto it = free_list_.begin(); it != free_list_.end(); ++it) { | |
| if (cudaEventQuery(it->release_event) != cudaSuccess) break; | |
| if (it->bytes >= bytes) { | |
| void* p = it->ptr; | |
| spare_events_.push_back(it->release_event); | |
| free_list_.erase(it); | |
| return p; | |
| } | |
| } | |
| return device_malloc(bytes); | |
| } | |
| // Called from a noexcept deleter: every failure falls back to device_free. | |
| void deallocate(void* p, size_t bytes) noexcept { | |
| if (free_list_.size() >= kMaxPoolSize) { | |
| device_free(p); | |
| return; | |
| } | |
| cudaEvent_t release_event = acquireEvent(); | |
| if (release_event == nullptr) { | |
| device_free(p); | |
| return; | |
| } | |
| if (cudaEventRecord(release_event, /*legacy default stream*/ nullptr) != cudaSuccess) { | |
| freeBlock(p, release_event); | |
| return; | |
| } | |
| free_list_.push_back({p, bytes, release_event}); | |
| } | |
| static DeviceBufferPool& threadLocal() { | |
| thread_local DeviceBufferPool pool; | |
| return pool; | |
| } | |
| private: | |
| // Returns a spare event, or a newly created one; nullptr if creation fails. | |
| cudaEvent_t acquireEvent() noexcept { | |
| if (!spare_events_.empty()) { | |
| cudaEvent_t event = spare_events_.back(); | |
| spare_events_.pop_back(); | |
| return event; | |
| } | |
| cudaEvent_t event = nullptr; | |
| if (cudaEventCreateWithFlags(&event, cudaEventDisableTiming) != cudaSuccess) return nullptr; | |
| return event; | |
| } | |
| // Gives up a block and the event tracking its release. Destroying a pending | |
| // event is non-blocking; the runtime defers it until the event completes. | |
| static void freeBlock(void* p, cudaEvent_t release_event) noexcept { | |
| (void)cudaEventDestroy(release_event); | |
| device_free(p); | |
| } | |
| std::vector<Entry> free_list_; | |
| // Events of recycled entries; free_list_.size() + spare_events_.size() <= kMaxPoolSize. | |
| std::vector<cudaEvent_t> spare_events_; | |
| }; | |
| // Stateful deleter that returns pooled buffers to the thread-local pool and | |
| // device_free's the rest. size==0 means "always device_free" (adopted pointers | |
| // and allocations that went straight to device_malloc). | |
| struct PooledCudaFreeDeleter { | |
| size_t size = 0; | |
| void operator()(void* p) const noexcept { | |
| if (!p) return; | |
| if (size > 0 && size <= DeviceBufferPool<>::kSmallBufferThreshold && | |
| DeviceBufferPool<>::threadState() == DeviceBufferPool<>::State::kAlive) { | |
| DeviceBufferPool<>::threadLocal().deallocate(p, size); | |
| } else { | |
| device_free(p); | |
| } | |
| } | |
| }; | |
| /** \brief Internal RAII owner for an untyped GPU device allocation. */ | |
| class DeviceBuffer { | |
| public: | |
| DeviceBuffer() = default; | |
| explicit DeviceBuffer(size_t bytes) : bytes_(bytes) { | |
| if (bytes > 0) { | |
| // The pool serves small blocks only on the cudaMalloc fallback path, and | |
| // not once its thread_local has been destroyed (allocation from a | |
| // static/TLS destructor). A deleter size of 0 keeps the other | |
| // allocations on the direct device_free path. | |
| const bool pooled = bytes <= DeviceBufferPool<>::kSmallBufferThreshold && !device_supports_memory_pools() && | |
| DeviceBufferPool<>::threadState() != DeviceBufferPool<>::State::kDestroyed; | |
| void* p = pooled ? DeviceBufferPool<>::threadLocal().allocate(bytes) : device_malloc(bytes); | |
| ptr_ = std::unique_ptr<void, PooledCudaFreeDeleter>(p, PooledCudaFreeDeleter{pooled ? bytes : 0}); | |
| } | |
| } | |
| // Explicit moves so a moved-from buffer reports size() == 0 (callers use | |
| // size() for grow-only reuse decisions; a stale size on a null buffer would | |
| // suppress the reallocation). | |
| DeviceBuffer(DeviceBuffer&& o) noexcept : ptr_(std::move(o.ptr_)), bytes_(o.bytes_) { o.bytes_ = 0; } | |
| DeviceBuffer& operator=(DeviceBuffer&& o) noexcept { | |
| if (this != &o) { | |
| ptr_ = std::move(o.ptr_); | |
| bytes_ = o.bytes_; | |
| o.bytes_ = 0; | |
| } | |
| return *this; | |
| } | |
| void* get() const noexcept { return ptr_.get(); } | |
| void* release() noexcept { | |
| bytes_ = 0; | |
| return ptr_.release(); | |
| } | |
| explicit operator bool() const noexcept { return static_cast<bool>(ptr_); } | |
| /** Logical allocation size in bytes, tracked for adopted pointers as well. */ | |
| size_t size() const noexcept { return bytes_; } | |
| // Adopt an existing device pointer of `bytes` usable bytes. Caller | |
| // relinquishes ownership. Adopted buffers bypass the pool on destruction | |
| // (deleter size == 0). | |
| static DeviceBuffer adopt(void* p, size_t bytes) noexcept { | |
| DeviceBuffer b; | |
| b.ptr_ = std::unique_ptr<void, PooledCudaFreeDeleter>(p, PooledCudaFreeDeleter{}); | |
| b.bytes_ = p ? bytes : 0; | |
| return b; | |
| } | |
| private: | |
| std::unique_ptr<void, PooledCudaFreeDeleter> ptr_; | |
| size_t bytes_ = 0; | |
| }; | |
| // cudaMemcpyAsync only overlaps with compute when the host side is pinned, so | |
| // async D2H staging goes through this buffer. | |
| class PinnedHostBuffer { | |
| public: | |
| PinnedHostBuffer() = default; | |
| explicit PinnedHostBuffer(size_t bytes) { | |
| if (bytes > 0) { | |
| void* p = nullptr; | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMallocHost(&p, bytes)); | |
| ptr_.reset(p); | |
| } | |
| } | |
| void* get() const noexcept { return ptr_.get(); } | |
| explicit operator bool() const noexcept { return static_cast<bool>(ptr_); } | |
| private: | |
| std::unique_ptr<void, CudaFreeHostDeleter> ptr_; | |
| }; | |
| // Upload a column-major host matrix whose strides are in elements. Ref<const | |
| // PlainMatrix> can bind any outer stride in place. Use a 2D DMA for ordinary | |
| // padded layouts; copy legal negative or overlapping Eigen strides one | |
| // contiguous column at a time because CUDA cannot express them as a pitch. | |
| template <typename Scalar> | |
| void upload_host_matrix(Scalar* dst, Index dst_outer_stride, const Scalar* src, Index src_outer_stride, Index rows, | |
| Index cols, cudaStream_t stream) { | |
| if (rows <= 0 || cols <= 0) return; | |
| eigen_assert(dst_outer_stride >= rows); | |
| const size_t column_bytes = static_cast<size_t>(rows) * sizeof(Scalar); | |
| if (src_outer_stride >= rows) { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy2DAsync(dst, static_cast<size_t>(dst_outer_stride) * sizeof(Scalar), src, | |
| static_cast<size_t>(src_outer_stride) * sizeof(Scalar), column_bytes, | |
| static_cast<size_t>(cols), cudaMemcpyHostToDevice, stream)); | |
| } else { | |
| for (Index col = 0; col < cols; ++col) { | |
| EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(dst + col * dst_outer_stride, src + col * src_outer_stride, column_bytes, | |
| cudaMemcpyHostToDevice, stream)); | |
| } | |
| } | |
| } | |
| // cudaDataType_t lives in library_types.h, pulled in transitively by | |
| // cuda_runtime.h, so this trait needs no NVIDIA library header of its own. | |
| template <typename Scalar> | |
| struct cuda_data_type; | |
| template <> | |
| struct cuda_data_type<float> { | |
| static constexpr cudaDataType_t value = CUDA_R_32F; | |
| }; | |
| template <> | |
| struct cuda_data_type<double> { | |
| static constexpr cudaDataType_t value = CUDA_R_64F; | |
| }; | |
| template <> | |
| struct cuda_data_type<std::complex<float>> { | |
| static constexpr cudaDataType_t value = CUDA_C_32F; | |
| }; | |
| template <> | |
| struct cuda_data_type<std::complex<double>> { | |
| static constexpr cudaDataType_t value = CUDA_C_64F; | |
| }; | |
| } // namespace internal | |
| } // namespace gpu | |
| } // namespace Eigen | |
| #endif // EIGEN_GPU_SUPPORT_H |