Eigen-Contrib  5.0.1
 
Loading...
Searching...
No Matches
GpuSupport.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com>
5//
6// This Source Code Form is subject to the terms of the Mozilla
7// Public License v. 2.0. If a copy of the MPL was not distributed
8// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9// SPDX-License-Identifier: MPL-2.0
10
11// Generic CUDA runtime support shared by all GPU library integrations.
12// Depends only on <cuda_runtime.h>; no NVIDIA library headers.
13
14#ifndef EIGEN_GPU_SUPPORT_H
15#define EIGEN_GPU_SUPPORT_H
16
17// IWYU pragma: private
18#include "./InternalHeaderCheck.h"
19
20#include <cuda_runtime.h>
21#include <cstdio>
22#include <cstdlib>
23#include <vector>
24
25#include <limits>
26#include <memory>
27#include <type_traits>
28
29namespace Eigen {
30namespace gpu {
31// Transpose/adjoint flag for BLAS-, solver-, and sparse-style calls. Each
32// library's support header maps it to its own enum (cublasOperation_t,
33// cusparseOperation_t, ...) via a to_<lib>_op() helper.
34enum class GpuOp { NoTrans, Trans, ConjTrans };
35
36namespace internal {
37// Prints `file:line: call: error` to stderr and stops: std::abort() where
38// assertions are compiled out, a failed eigen_assert otherwise. No build goes
39// on past a failed call: its work was not done, and a sticky error leaves the
40// context unusable for every later call.
41inline void gpu_check_failed(const char* error, const char* expression, const char* file, int line) {
42 std::fprintf(stderr, "%s:%d: %s: %s\n", file, line, expression, error);
43#if defined(EIGEN_NO_DEBUG)
44 std::abort();
45#else
46 eigen_assert(false && "GPU runtime or library call failed");
47#endif
48}
49
50// Every check macro of the module reports failures here, in every build. It may
51// be defined to throw, so no destructor or noexcept function uses the checks.
52#ifndef EIGEN_GPU_CHECK_FAILED
53#define EIGEN_GPU_CHECK_FAILED(error, expression, file, line) \
54 ::Eigen::gpu::internal::gpu_check_failed(error, expression, file, line)
55#endif
56
57#define EIGEN_CUDA_RUNTIME_CHECK(expr) \
58 do { \
59 const cudaError_t _e = (expr); \
60 if (_e != cudaSuccess) EIGEN_GPU_CHECK_FAILED(cudaGetErrorName(_e), #expr, __FILE__, __LINE__); \
61 } while (0)
62
63// For the libraries without a status-to-string function: "<library> status <code>".
64inline void gpu_check_failed_code(const char* library, int status, const char* expression, const char* file, int line) {
65 char error[64];
66 std::snprintf(error, sizeof(error), "%s status %d", library, status);
67 // A user-defined EIGEN_GPU_CHECK_FAILED need not use every argument.
68 EIGEN_UNUSED_VARIABLE(expression);
69 EIGEN_UNUSED_VARIABLE(file);
70 EIGEN_UNUSED_VARIABLE(line);
71 EIGEN_GPU_CHECK_FAILED(error, expression, file, line);
72}
73
74// cuBLAS and the legacy cuSOLVER APIs take dimensions and leading dimensions as
75// 32-bit `int`, while Eigen's Index is 64-bit by default and GPU allocations can
76// exceed INT_MAX in one dimension. Narrow through this helper at every such call
77// site so an out-of-range value asserts instead of silently overflowing.
78inline int to_blas_int(int64_t v) {
79 eigen_assert(v >= 0 && v <= static_cast<int64_t>((std::numeric_limits<int>::max)()) &&
80 "dimension exceeds the int range supported by cuBLAS / cuSOLVER");
81 return static_cast<int>(v);
82}
83
84// cudaMallocAsync / cudaFreeAsync (CUDA 11.2+) allocate from a stream-ordered
85// memory pool: both are cheap enqueues instead of the device-wide
86// synchronization performed by cudaMalloc / cudaFree. All module allocations
87// go through device_malloc / device_free on the *legacy default stream*:
88// legacy-stream ordering guarantees that work enqueued later on any blocking
89// stream observes the allocation, and that a free waits for all previously
90// enqueued work on blocking streams — the same lifetime guarantees callers
91// got from cudaMalloc / cudaFree, minus the host stalls.
92//
93// Caveat: streams created with cudaStreamNonBlocking do not synchronize with
94// the legacy stream. When borrowing such a stream (gpu::Context(stream)),
95// define EIGEN_GPU_NO_STREAM_ORDERED_ALLOC to fall back to cudaMalloc/cudaFree.
96//
97// Support is detected once per process, from the device current at first use.
98
99inline bool device_supports_memory_pools() {
100#ifdef EIGEN_GPU_NO_STREAM_ORDERED_ALLOC
101 return false;
102#else
103 static const bool supported = [] {
104 int device = 0;
105 if (cudaGetDevice(&device) != cudaSuccess) return false;
106 int v = 0;
107 if (cudaDeviceGetAttribute(&v, cudaDevAttrMemoryPoolsSupported, device) != cudaSuccess) return false;
108 if (v == 0) return false;
109 // Keep freed memory in the pool instead of trimming at every stream
110 // synchronize — repeated alloc/free cycles (temporaries in loops) then
111 // recycle at user-space speed.
112 cudaMemPool_t pool = nullptr;
113 if (cudaDeviceGetDefaultMemPool(&pool, device) == cudaSuccess) {
114 // The attribute value type is cuuint64_t; use a same-size stand-in to
115 // avoid requiring the driver-API header.
116 unsigned long long threshold = ~0ULL;
117 (void)cudaMemPoolSetAttribute(pool, cudaMemPoolAttrReleaseThreshold, &threshold);
118 }
119 return true;
120 }();
121 return supported;
122#endif
123}
124
125inline void* device_malloc(size_t bytes) {
126 void* p = nullptr;
127 if (device_supports_memory_pools()) {
128 EIGEN_CUDA_RUNTIME_CHECK(cudaMallocAsync(&p, bytes, /*legacy default stream*/ nullptr));
129 } else {
130 EIGEN_CUDA_RUNTIME_CHECK(cudaMalloc(&p, bytes));
131 }
132 return p;
133}
134
135inline void device_free(void* p) noexcept {
136 if (!p) return;
137 if (device_supports_memory_pools()) {
138 (void)cudaFreeAsync(p, /*legacy default stream*/ nullptr);
139 } else {
140 (void)cudaFree(p);
141 }
142}
143
144struct CudaFreeDeleter {
145 // When `borrow == true`, the unique_ptr does not free the pointer. Used by
146 // DeviceMatrix::view() to wrap a non-owning device pointer with the same
147 // smart-pointer machinery as owning storage, without changing the type.
148 bool borrow = false;
149 void operator()(void* p) const noexcept {
150 if (p && !borrow) device_free(p);
151 }
152};
153
154struct CudaFreeHostDeleter {
155 void operator()(void* p) const noexcept {
156 if (p) (void)cudaFreeHost(p);
157 }
158};
159
160// RAII CUDA stream; the ownership flag supports borrowed, caller-owned streams.
161struct CudaStreamDeleter {
162 bool owns = true;
163 void operator()(cudaStream_t s) const noexcept {
164 if (owns && s) (void)cudaStreamDestroy(s);
165 }
166};
167using UniqueStream = std::unique_ptr<std::remove_pointer_t<cudaStream_t>, CudaStreamDeleter>;
168
169// Recycles allocations up to kSmallBufferThreshold bytes (e.g. DeviceScalar) to
170// avoid cudaMalloc/cudaFree overhead on devices without memory pools; where
171// cudaMallocAsync exists it is both stream-ordered and cheaper than this pool's
172// release event, so DeviceBuffer bypasses the pool there (bench_overhead:
173// CudaMallocAsyncFree vs PoolAllocFree). Larger allocations always bypass it.
174// Invariant: a block is recycled only after the device has retired every
175// operation enqueued before its release on any blocking stream: deallocate()
176// records an event on the legacy default stream (the ordering device_free
177// relies on), the free list stays in release order, and events on one stream
178// retire in order, so allocate() scans until the first pending entry.
179template <size_t SmallBufferThreshold = 256, size_t MaxPoolSize = 64>
180struct DeviceBufferPool {
181 static constexpr size_t kSmallBufferThreshold = SmallBufferThreshold;
182 static constexpr size_t kMaxPoolSize = MaxPoolSize;
183
184 struct Entry {
185 void* ptr;
186 size_t bytes;
187 cudaEvent_t release_event;
188 };
189
190 // Lifetime marker for the thread-local pool. thread_local destruction runs
191 // in reverse construction order, so a long-lived object holding pooled
192 // buffers (e.g. the thread-local gpu::Context, or a static) can be
193 // destroyed *after* the pool. The marker is trivially destructible — it
194 // stays readable during TLS teardown — letting the deleter fall back to a
195 // direct device_free once the pool is gone instead of touching a destroyed
196 // vector.
197 enum class State : signed char { kNotConstructed = 0, kAlive = 1, kDestroyed = 2 };
198
199 static State& threadState() {
200 thread_local State state = State::kNotConstructed;
201 return state;
202 }
203
204 DeviceBufferPool() { threadState() = State::kAlive; }
205
206 ~DeviceBufferPool() {
207 for (const Entry& entry : free_list_) freeBlock(entry.ptr, entry.release_event);
208 for (cudaEvent_t event : spare_events_) (void)cudaEventDestroy(event);
209 threadState() = State::kDestroyed;
210 }
211
212 // First fit among the retired blocks, oldest release first; the first pending
213 // entry ends the scan because every later one is pending too.
214 void* allocate(size_t bytes) {
215 for (auto it = free_list_.begin(); it != free_list_.end(); ++it) {
216 if (cudaEventQuery(it->release_event) != cudaSuccess) break;
217 if (it->bytes >= bytes) {
218 void* p = it->ptr;
219 spare_events_.push_back(it->release_event);
220 free_list_.erase(it);
221 return p;
222 }
223 }
224 return device_malloc(bytes);
225 }
226
227 // Called from a noexcept deleter: every failure falls back to device_free.
228 void deallocate(void* p, size_t bytes) noexcept {
229 if (free_list_.size() >= kMaxPoolSize) {
230 device_free(p);
231 return;
232 }
233 cudaEvent_t release_event = acquireEvent();
234 if (release_event == nullptr) {
235 device_free(p);
236 return;
237 }
238 if (cudaEventRecord(release_event, /*legacy default stream*/ nullptr) != cudaSuccess) {
239 freeBlock(p, release_event);
240 return;
241 }
242 free_list_.push_back({p, bytes, release_event});
243 }
244
245 static DeviceBufferPool& threadLocal() {
246 thread_local DeviceBufferPool pool;
247 return pool;
248 }
249
250 private:
251 // Returns a spare event, or a newly created one; nullptr if creation fails.
252 cudaEvent_t acquireEvent() noexcept {
253 if (!spare_events_.empty()) {
254 cudaEvent_t event = spare_events_.back();
255 spare_events_.pop_back();
256 return event;
257 }
258 cudaEvent_t event = nullptr;
259 if (cudaEventCreateWithFlags(&event, cudaEventDisableTiming) != cudaSuccess) return nullptr;
260 return event;
261 }
262
263 // Gives up a block and the event tracking its release. Destroying a pending
264 // event is non-blocking; the runtime defers it until the event completes.
265 static void freeBlock(void* p, cudaEvent_t release_event) noexcept {
266 (void)cudaEventDestroy(release_event);
267 device_free(p);
268 }
269
270 std::vector<Entry> free_list_;
271 // Events of recycled entries; free_list_.size() + spare_events_.size() <= kMaxPoolSize.
272 std::vector<cudaEvent_t> spare_events_;
273};
274
275// Stateful deleter that returns pooled buffers to the thread-local pool and
276// device_free's the rest. size==0 means "always device_free" (adopted pointers
277// and allocations that went straight to device_malloc).
278struct PooledCudaFreeDeleter {
279 size_t size = 0;
280
281 void operator()(void* p) const noexcept {
282 if (!p) return;
283 if (size > 0 && size <= DeviceBufferPool<>::kSmallBufferThreshold &&
284 DeviceBufferPool<>::threadState() == DeviceBufferPool<>::State::kAlive) {
285 DeviceBufferPool<>::threadLocal().deallocate(p, size);
286 } else {
287 device_free(p);
288 }
289 }
290};
291
293class DeviceBuffer {
294 public:
295 DeviceBuffer() = default;
296
297 explicit DeviceBuffer(size_t bytes) : bytes_(bytes) {
298 if (bytes > 0) {
299 // The pool serves small blocks only on the cudaMalloc fallback path, and
300 // not once its thread_local has been destroyed (allocation from a
301 // static/TLS destructor). A deleter size of 0 keeps the other
302 // allocations on the direct device_free path.
303 const bool pooled = bytes <= DeviceBufferPool<>::kSmallBufferThreshold && !device_supports_memory_pools() &&
304 DeviceBufferPool<>::threadState() != DeviceBufferPool<>::State::kDestroyed;
305 void* p = pooled ? DeviceBufferPool<>::threadLocal().allocate(bytes) : device_malloc(bytes);
306 ptr_ = std::unique_ptr<void, PooledCudaFreeDeleter>(p, PooledCudaFreeDeleter{pooled ? bytes : 0});
307 }
308 }
309
310 // Explicit moves so a moved-from buffer reports size() == 0 (callers use
311 // size() for grow-only reuse decisions; a stale size on a null buffer would
312 // suppress the reallocation).
313 DeviceBuffer(DeviceBuffer&& o) noexcept : ptr_(std::move(o.ptr_)), bytes_(o.bytes_) { o.bytes_ = 0; }
314 DeviceBuffer& operator=(DeviceBuffer&& o) noexcept {
315 if (this != &o) {
316 ptr_ = std::move(o.ptr_);
317 bytes_ = o.bytes_;
318 o.bytes_ = 0;
319 }
320 return *this;
321 }
322
323 void* get() const noexcept { return ptr_.get(); }
324 void* release() noexcept {
325 bytes_ = 0;
326 return ptr_.release();
327 }
328 explicit operator bool() const noexcept { return static_cast<bool>(ptr_); }
329
331 size_t size() const noexcept { return bytes_; }
332
333 // Adopt an existing device pointer of `bytes` usable bytes. Caller
334 // relinquishes ownership. Adopted buffers bypass the pool on destruction
335 // (deleter size == 0).
336 static DeviceBuffer adopt(void* p, size_t bytes) noexcept {
337 DeviceBuffer b;
338 b.ptr_ = std::unique_ptr<void, PooledCudaFreeDeleter>(p, PooledCudaFreeDeleter{});
339 b.bytes_ = p ? bytes : 0;
340 return b;
341 }
342
343 private:
344 std::unique_ptr<void, PooledCudaFreeDeleter> ptr_;
345 size_t bytes_ = 0;
346};
347
348// cudaMemcpyAsync only overlaps with compute when the host side is pinned, so
349// async D2H staging goes through this buffer.
350class PinnedHostBuffer {
351 public:
352 PinnedHostBuffer() = default;
353
354 explicit PinnedHostBuffer(size_t bytes) {
355 if (bytes > 0) {
356 void* p = nullptr;
357 EIGEN_CUDA_RUNTIME_CHECK(cudaMallocHost(&p, bytes));
358 ptr_.reset(p);
359 }
360 }
361
362 void* get() const noexcept { return ptr_.get(); }
363 explicit operator bool() const noexcept { return static_cast<bool>(ptr_); }
364
365 private:
366 std::unique_ptr<void, CudaFreeHostDeleter> ptr_;
367};
368
369// Upload a column-major host matrix whose strides are in elements. Ref<const
370// PlainMatrix> can bind any outer stride in place. Use a 2D DMA for ordinary
371// padded layouts; copy legal negative or overlapping Eigen strides one
372// contiguous column at a time because CUDA cannot express them as a pitch.
373template <typename Scalar>
374void upload_host_matrix(Scalar* dst, Index dst_outer_stride, const Scalar* src, Index src_outer_stride, Index rows,
375 Index cols, cudaStream_t stream) {
376 if (rows <= 0 || cols <= 0) return;
377 eigen_assert(dst_outer_stride >= rows);
378 const size_t column_bytes = static_cast<size_t>(rows) * sizeof(Scalar);
379 if (src_outer_stride >= rows) {
380 EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpy2DAsync(dst, static_cast<size_t>(dst_outer_stride) * sizeof(Scalar), src,
381 static_cast<size_t>(src_outer_stride) * sizeof(Scalar), column_bytes,
382 static_cast<size_t>(cols), cudaMemcpyHostToDevice, stream));
383 } else {
384 for (Index col = 0; col < cols; ++col) {
385 EIGEN_CUDA_RUNTIME_CHECK(cudaMemcpyAsync(dst + col * dst_outer_stride, src + col * src_outer_stride, column_bytes,
386 cudaMemcpyHostToDevice, stream));
387 }
388 }
389}
390
391// cudaDataType_t lives in library_types.h, pulled in transitively by
392// cuda_runtime.h, so this trait needs no NVIDIA library header of its own.
393template <typename Scalar>
394struct cuda_data_type;
395
396template <>
397struct cuda_data_type<float> {
398 static constexpr cudaDataType_t value = CUDA_R_32F;
399};
400template <>
401struct cuda_data_type<double> {
402 static constexpr cudaDataType_t value = CUDA_R_64F;
403};
404template <>
405struct cuda_data_type<std::complex<float>> {
406 static constexpr cudaDataType_t value = CUDA_C_32F;
407};
408template <>
409struct cuda_data_type<std::complex<double>> {
410 static constexpr cudaDataType_t value = CUDA_C_64F;
411};
412} // namespace internal
413} // namespace gpu
414} // namespace Eigen
415
416#endif // EIGEN_GPU_SUPPORT_H
Internal RAII owner for an untyped GPU device allocation.
Definition GpuSupport.h:293
size_t size() const noexcept
Definition GpuSupport.h:331
Namespace containing all symbols from the Eigen library.