compute-acceleration
GEMM and sum reduction across serial, OpenMP and CUDA
Loading...
Searching...
No Matches
device_buffer.cuh
1// Copyright (c) 2025 yanghuafang
2// SPDX-License-Identifier: MIT
3
4#ifndef ACCEL_CUDA_DEVICE_BUFFER_CUH_
5#define ACCEL_CUDA_DEVICE_BUFFER_CUH_
6
7#include <cstddef>
8#include <stdexcept>
9#include <string>
10#include <utility>
11#include <vector>
12
13#include <cuda_runtime.h>
14
15#include "cuda/cuda_check.cuh"
16
17namespace accel {
18
19// Move-only RAII owner of a cudaMalloc allocation: exactly one owner, released
20// on scope exit however that exit is reached. Every early return between
21// cudaMalloc and cudaFree is otherwise a leak the compiler cannot warn about.
22//
23// Copying is deleted; an implicit one would double-free. Transfer with
24// std::move. Not internally synchronised -- concurrent CopyFromHost calls race
25// exactly as raw cudaMemcpy would.
26//
27// T must be trivially copyable: the transfer helpers are byte copies and run
28// no constructors on either side.
29template <typename T>
30class DeviceBuffer {
31 public:
32 // Constructs an empty buffer that owns nothing.
33 DeviceBuffer() noexcept = default;
34
35 // Allocates room for `count` elements.
36 //
37 // Throws CudaError if `cudaMalloc` fails.
38 //
39 // count Element count; Zero yields an empty buffer without calling into
40 // the driver.
41 explicit DeviceBuffer(std::size_t count) {
42 if (count == 0) {
43 return;
44 }
45 void* raw = nullptr;
46 CUDA_CHECK(cudaMalloc(&raw, count * sizeof(T)));
47 Data_ = static_cast<T*>(raw);
48 size_ = count;
49 }
50
51 // Swallows any `cudaFree` error: destructors must not throw, and a
52 // free failing during context teardown is not actionable.
53 ~DeviceBuffer() { Reset(); }
54
55 DeviceBuffer(const DeviceBuffer&) = delete;
56 DeviceBuffer& operator=(const DeviceBuffer&) = delete;
57
58 DeviceBuffer(DeviceBuffer&& other) noexcept
59 : Data_(std::exchange(other.Data_, nullptr)),
60 size_(std::exchange(other.size_, 0)) {}
61
62 DeviceBuffer& operator=(DeviceBuffer&& other) noexcept {
63 if (this != &other) {
64 Reset();
65 Data_ = std::exchange(other.Data_, nullptr);
66 size_ = std::exchange(other.size_, 0);
67 }
68 return *this;
69 }
70
71 T* get() noexcept { return Data_; }
72 const T* get() const noexcept { return Data_; }
73 std::size_t size() const noexcept { return size_; }
74 std::size_t SizeBytes() const noexcept { return size_ * sizeof(T); }
75 bool empty() const noexcept { return size_ == 0; }
76 explicit operator bool() const noexcept { return Data_ != nullptr; }
77
78 // Releases the allocation and returns the buffer to the empty state.
79 void Reset() noexcept {
80 if (Data_ != nullptr) {
81 static_cast<void>(cudaFree(Data_));
82 Data_ = nullptr;
83 size_ = 0;
84 }
85 }
86
87 // Uploads `count` elements from host memory.
88 //
89 // Stream-ordered and asynchronous. A subsequent kernel launched on the same
90 // stream observes the data, but work on any *other* stream does not until
91 // the caller synchronises.
92 //
93 // Safe to let `host_src` die on return even so: for pageable source memory
94 // the driver stages through its own pinned buffer before the call returns.
95 //
96 // Throws CudaError on transfer failure, or std::out_of_range if `count`
97 // exceeds the allocation — catching the overflow before the driver does
98 // turns a silent heap corruption into a diagnosable error.
99 void CopyFromHost(const T* host_src, std::size_t count,
100 cudaStream_t stream = nullptr) {
101 RequireCapacity(count);
102 if (count == 0) {
103 return;
104 }
105 CUDA_CHECK(cudaMemcpyAsync(Data_, host_src, count * sizeof(T),
106 cudaMemcpyHostToDevice, stream));
107 }
108
109 // Convenience overload uploading an entire host vector.
110 void CopyFromHost(const std::vector<T>& host_src,
111 cudaStream_t stream = nullptr) {
112 CopyFromHost(host_src.data(), host_src.size(), stream);
113 }
114
115 // Downloads `count` elements into host memory.
116 //
117 // Stream-ordered and asynchronous: `host_dst` is **not** readable when this
118 // returns. Synchronise the stream first, or use ToHost(), which does it for
119 // you. This is the one asymmetry with CopyFromHost() — there the driver's
120 // staging buffer hides the asynchrony, here nothing does.
121 //
122 // Throws CudaError on transfer failure, std::out_of_range on overrun.
123 void CopyToHost(T* host_dst, std::size_t count,
124 cudaStream_t stream = nullptr) const {
125 RequireCapacity(count);
126 if (count == 0) {
127 return;
128 }
129 CUDA_CHECK(cudaMemcpyAsync(host_dst, Data_, count * sizeof(T),
130 cudaMemcpyDeviceToHost, stream));
131 }
132
133 // Downloads the whole buffer and synchronises, so the result is readable on
134 // return. Prefer this over CopyToHost() unless the extra sync matters.
135 std::vector<T> ToHost(cudaStream_t stream = nullptr) const {
136 std::vector<T> host(size_);
137 CopyToHost(host.data(), size_, stream);
138 CUDA_CHECK(cudaStreamSynchronize(stream));
139 return host;
140 }
141
142 // Zero-fills the entire allocation.
143 // Throws CudaError if `cudaMemsetAsync` fails.
144 void Zero(cudaStream_t stream = nullptr) {
145 if (size_ == 0) {
146 return;
147 }
148 CUDA_CHECK(cudaMemsetAsync(Data_, 0, SizeBytes(), stream));
149 }
150
151 private:
152 void RequireCapacity(std::size_t count) const {
153 if (count > size_) {
154 throw std::out_of_range(
155 "DeviceBuffer transfer of " + std::to_string(count) +
156 " elements exceeds capacity " + std::to_string(size_));
157 }
158 }
159
160 T* Data_ = nullptr;
161 std::size_t size_ = 0;
162};
163
164} // namespace accel
165
166#endif // ACCEL_CUDA_DEVICE_BUFFER_CUH_