Skip to content

Commit 02e720c

Browse files
authored
Dev/cuda and performance (#4)
* Added MNIST example, applied formattin, and fixed some bugs. * Enabled CUDA compilation * Better mem-access for matmul * Optimized matmul for better cache alignment/lower cache misses * MNIST and gitignore update * Tensor backend for CUDA ready * Minor optimization using memcopy * Laying foundation for easy transpose * Further enabling of resetting dims * Progress on view transpose/lazy transpose * Finished implementing transpose logic * Some more CUDA and compiler hints, clean up values_t, fix leaky-relu * Sigmoid kernel * Fixed issues with new transposition logic * skeleton for softmax * First softmax untested * Skeleton of cuda bce loss * Renaming of cuda namespace * Fixed several compile-time and run-time bugs * Fixing some cuda errors in tensor.cpp * Fix some more compile time bugs in cuda backend * Resolved compile time errors * Resolved compile time and runtime issues of unit tests * Fixed python unit tests * First CUDA unit tests, plus some fixes and checks based on them * More unit tests and some unit test cleanup * Fixing Gaussian interface, using expect_near in unit tests instead of assert_double_eq for better test stability * Minor refactor * Remove cuda branch from printing * Fixed CUDA transpose * Implemented naive matmul in CUDA * Fixed unit test, made threads per block more generic through device properties singleton * Removed bug from matmul * Added unit tests for CUDA modules, set up infrastructure for cuda code for other cuda parts as well * More infra prepared for CUDA * Last pieces of infrastructure * Added warp reduce kernel code to compute softmax. Needs dimension that softmax computes over <= 64 == 2 * warpSize; added kernel code for backward functions of relu, leakyrelu and sigmoid; added unit tests for all new kernels * Added softmax kernel for medium large strides * Fixed indexing bug for crossentropy- and softmax that arises in dimensions >= 3; added unit test for larger softmax kernel; removed unused function stubs * Update readme * Adjusting spacing * Fix GPU softmax * Fixed softmax for good * Softmax kernels for large case; need debugging * Fixed large softmax kernel * Fixed softmax backward indexing to process multidimensional softmax * Softmax backward kernel for small stride in CUDA; fix some indentation * Minor fix in kernel, add unit test, update readme. Kernel still needs fixing * Update unit tests with sharper delta * Fix error in small softmax backward kernel * Kernel for large backward softmax * CUDA version of FfLayer with unit tests * Updated unit tests * Fixed unit tests * Clean up unit tests further, align indentation * backward on broadcast add * Fixed sum over dims on CPU for general case and implemented general case CUDA kernel * Implemented backward loss functions, generalized forward CE loss functions for general case * Fixed compile time issues, added unit tests for backward of basic tensorops * More forward loss kernels, prepared unit tests for losses * Forward crossentropy and RMSE losses * Preparing infrastructure for forward crossentropy softmax kernel * Crossentropy softmax kernel forward, some restructuring of includes * Fix crossentropy forward kernel, simplify CPU version * Unit tests for larger input in loss functions, bugfixes in crossentropy and rmse for larger inputs * Fixed all forward loss functions and eliminated error of uninitialized shared memory * CUDA versions of optimizers * Fix bug in loss * CUDA Python unit tests * Fixing some subtle bugs, updated some kernels with type traits, cleaned code, added FfLayer large backward unit tests * Fixed RMSProp and bug in RMSE backward, added unit tests for large loss inputs * Fix bug in CUDA softmax backward, add unit test to capture gap, fix overfitting unit tests for CUDA * Fix bug and python unit tests
1 parent 32bfd0e commit 02e720c

83 files changed

Lines changed: 6952 additions & 1906 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 0 additions & 9 deletions
This file was deleted.

CMakeLists.txt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,22 @@ endif()
2727
add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>")
2828
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
2929

30-
# TODO: add flag for double precision?
30+
option(CUDA "Enable CUDA execution for some faster data structures" ON)
31+
32+
if (CUDA)
33+
include(CheckLanguage)
34+
check_language(CUDA)
35+
36+
if(CMAKE_CUDA_COMPILER)
37+
add_definitions(-D__CUDA)
38+
enable_language(CUDA)
39+
40+
set(CMAKE_CUDA_STANDARD 20)
41+
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
42+
else()
43+
message(WARNING "Could not find CUDA on system. Compiling without CUDA enabled")
44+
endif()
45+
endif()
3146

3247
# include python libs
3348
if(APPLE)

examples/mnist.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ def evaluate(net, x, y_int, batch_size=256):
125125
# setup
126126
net = make_net()
127127
loss_fn = CrossEntropyWithSoftmax()
128-
optim = RmsProp(net.parameters(), 0.00001, 0.95) # lr and decay
128+
optim = RmsProp(net.parameters(), 0.000001, 0.999) # lr and decay
129129

130130
# training loop
131-
n_epochs = 10
131+
n_epochs = 5
132132
for epoch in range(n_epochs):
133133
train_loss = train_epoch(net, loss_fn, optim, x_train, y_train)
134134
val_acc = evaluate(net, x_val, y_val)

readme.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ For some examples on Python interface, see tests/python.
2020
- Training framework (optimizers, loss functions, layers, and networks)
2121
- **Example code**: Full MNIST dataset training example
2222
- **Python Interface**: Seamless integration via Boost.Python
23-
- **Clean Architecture**: Modular design, ~4K LOC
23+
- **Clean Architecture**: Modular design, maintainable and extensible
2424
- **CI/CD**: Automated testing with GTest and GitHub Actions
2525

2626
## Tech Stack
2727

28-
- C++17/20
28+
- C++17/20/23
2929
- CMake build system
3030
- Boost.Python for Python bindings
3131
- Python 3 for library interface and examples
3232
- Google Test (GTest) and PyTest for unit testing
3333
- GitHub Actions for CI/CD
34+
- CUDA
3435

3536
## Current Status
3637

@@ -40,7 +41,7 @@ Roadmap:
4041
- [x] Python Binding Unit Tests
4142
- [x] Optimizers and training framework
4243
- [x] MNIST example
43-
- [ ] CUDA mode for operations
44+
- [x] CUDA mode for operations
4445
- [ ] Additional layer types (Conv2D, Dropout, etc.)
4546
- [ ] AlexNet reference implementation
4647
- [ ] Docker deployment example
@@ -50,9 +51,18 @@ Roadmap:
5051
mkdir build && cd build
5152
cmake ..
5253
make
53-
ctest
5454
```
5555

56+
### Building with CUDA
57+
58+
Project automatically detects whether CUDA is installed, and compiles with it.
59+
If CUDA compilation not desired you can switch it off via
60+
61+
```bash
62+
cmake --DCUDA=Off ..
63+
```
64+
65+
5666
## Running Unit Tests
5767

5868
Compile with building tests enabled:
@@ -61,7 +71,7 @@ Compile with building tests enabled:
6171
mkdir build && cd build
6272
cmake -DBUILD_TESTS=On ..
6373
make
64-
ctest
74+
ctest .
6575
```
6676

6777
## Required
@@ -73,6 +83,7 @@ ctest
7383
- numpy 1.26.4
7484
- pytest and GTest for unit tests (we use pytest=9.0.2)
7585
- Google Benchmark for benchmarking
86+
- CUDA (we use CUDA 13.1 on an RTX-5050)
7687

7788
## Troubleshooting
7889

src/backend/CMakeLists.txt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ file(GLOB_RECURSE CORE_SOURCES
77
utility/*.cpp
88
)
99

10+
if(CMAKE_CUDA_COMPILER)
11+
file(GLOB_RECURSE CUDA_SOURCES
12+
computational_graph/*.cu
13+
data_modeling/*.cu
14+
module/*.cu
15+
system/*.cu
16+
training/*.cu
17+
utility/*.cu
18+
)
19+
list(APPEND CORE_SOURCES ${CUDA_SOURCES})
20+
endif()
21+
1022
add_library(BackendCore SHARED ${CORE_SOURCES})
1123

1224
target_include_directories(BackendCore PUBLIC
@@ -15,4 +27,23 @@ target_include_directories(BackendCore PUBLIC
1527

1628
set_target_properties(BackendCore PROPERTIES
1729
LIBRARY_OUTPUT_DIRECTORY "${PYTHON_MODULE_DIR}" # make sure Python-modules see backend
18-
)
30+
)
31+
32+
if(CMAKE_CUDA_COMPILER)
33+
set_target_properties(BackendCore PROPERTIES
34+
CUDA_SEPARABLE_COMPILATION ON
35+
# nvidia-smi --query-gpu=compute_cap --format=csv,noheader
36+
# I get 12.0, hence 120
37+
#set(CMAKE_CUDA_ARCHITECTURES "75;86;89;100;120")
38+
CMAKE_CUDA_ARCHITECTURES native
39+
)
40+
41+
find_package(CUDAToolkit REQUIRED)
42+
target_include_directories(BackendCore PRIVATE
43+
${CUDAToolkit_INCLUDE_DIRS}
44+
)
45+
target_link_libraries(BackendCore
46+
CUDA::cudart
47+
)
48+
endif()
49+
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* @file activation_nodes.cu
3+
* @author Robert Baumgartner (r.baumgartner-1@tudelft.nl)
4+
* @brief
5+
* @version 0.1
6+
* @date 2026-03-23
7+
*
8+
* @copyright Copyright (c) 2026
9+
*
10+
*/
11+
12+
#ifndef __CUDA
13+
static_assert(false, "File should not be compiled without CUDA enabled");
14+
#endif // __CUDA
15+
16+
#include "activation_nodes.cuh"
17+
#include "utility/cuda/cuda_common.cuh"
18+
19+
using namespace std;
20+
21+
namespace {
22+
/**
23+
* @brief Relu backward kernel.
24+
*/
25+
__global__ void reluBackwardKernel(ftype* const res, const ftype* const upstreamGrad, const ftype* const parent, const tensorSize_t size) {
26+
const int gid = blockIdx.x * blockDim.x + threadIdx.x;
27+
if(gid >= size) {
28+
return;
29+
}
30+
31+
res[gid] = parent[gid] > 0 ? upstreamGrad[gid] : 0;
32+
}
33+
34+
/**
35+
* @brief Leaky relu backward kernel.
36+
*/
37+
__global__ void leakyReluBackwardKernel(ftype* const res, const ftype* const upstreamGrad, const ftype* const parent, const ftype eps, const tensorSize_t size) {
38+
const int gid = blockIdx.x * blockDim.x + threadIdx.x;
39+
if(gid >= size) {
40+
return;
41+
}
42+
43+
res[gid] = parent[gid] > 0 ? upstreamGrad[gid] : eps * upstreamGrad[gid];
44+
}
45+
46+
/**
47+
* @brief Sigmoid backward kernel, optimized by using the forward sigmoid.
48+
*/
49+
__global__ void sigmoidBackwardKernel(ftype* const res, const ftype* const upstreamGrad, const ftype* const sigmoid, const tensorSize_t size) {
50+
const int gid = blockIdx.x * blockDim.x + threadIdx.x;
51+
if(gid >= size) {
52+
return;
53+
}
54+
55+
ftype si = sigmoid[gid];
56+
res[gid] = si * (1 - si) * upstreamGrad[gid];
57+
}
58+
59+
/**
60+
* @brief Softmax backward kernel. This kernel is different than others since it is warp aligned. The inner loop avoids shared memory bank
61+
* conflicts by broadcasting.
62+
*
63+
* stridesWidthPerBlock is an awkward name. It is the product of number of strides per block (times) stride. We pre-compute it on host.
64+
*/
65+
__global__ void softmaxBackwardKernelOneBlock(ftype* const res, const ftype* const upstreamGrad, const ftype* const softmax,
66+
const tensorSize_t stride, const int stridesWidthPerBlock, const int threadsPerStride, tensorSize_t size) {
67+
const int tid = threadIdx.x;
68+
69+
const int withinStrideOffset = tid % threadsPerStride;
70+
const int strideOffset = (tid / threadsPerStride) * stride;
71+
72+
const int gid = blockIdx.x * stridesWidthPerBlock + strideOffset + withinStrideOffset;
73+
const bool isPadded = (withinStrideOffset >= stride) || (gid >= size); // padded threads only exists to align warps with strides
74+
75+
ftype yi = 0;
76+
const int smemOffset = strideOffset + withinStrideOffset;
77+
78+
extern __shared__ ftype smem[];
79+
if(!isPadded) {
80+
yi = softmax[gid];
81+
smem[smemOffset] = yi;
82+
smem[smemOffset + stridesWidthPerBlock] = upstreamGrad[gid];
83+
}
84+
__syncthreads();
85+
86+
if(isPadded) {
87+
return;
88+
}
89+
90+
ftype grad = 0;
91+
for(int j = 0; j < stride; j++) {
92+
// warp alignment -> smem-reads are broadcasted per warp -> no bank conflicts
93+
ftype yj = smem[strideOffset + j];
94+
ftype gj = smem[strideOffset + j + stridesWidthPerBlock];
95+
96+
auto jacobian = (withinStrideOffset == j) ? yi * (1 - yj) : -yi * yj;
97+
grad += gj * jacobian;
98+
}
99+
100+
res[gid] = grad;
101+
}
102+
103+
/**
104+
* @brief Large softmax pass. Because the stride now does not fit into one block anymore we do a grid-stride loop.
105+
*/
106+
__global__ void softmaxBackwardKernelLargePass(ftype* const res, const ftype* const upstreamGrad, const ftype* const softmax, const int blocksPerStride, const tensorSize_t stride) {
107+
const int strideNumber = blockIdx.x / blocksPerStride;
108+
const int strideOffset = strideNumber * stride;
109+
const int i = (blockIdx.x % blocksPerStride) * blockDim.x + threadIdx.x;
110+
// blockIdx.x % blocksPerStride = block number within this stride
111+
112+
const int tid = threadIdx.x;
113+
const int gid = strideOffset + i;
114+
115+
extern __shared__ ftype smem[];
116+
117+
const bool isNotPadded = i < stride;
118+
const ftype yi = isNotPadded ? softmax[gid] : 0;
119+
120+
ftype grad = 0;
121+
for(int offset = 0; offset < stride; offset += blockDim.x) {
122+
// load into smem
123+
{
124+
const int j = offset + tid;
125+
if(j < stride) {
126+
smem[tid] = softmax[strideOffset + j];
127+
smem[tid + blockDim.x] = upstreamGrad[strideOffset + j];
128+
}
129+
__syncthreads();
130+
}
131+
132+
133+
for(int k = 0; k < blockDim.x; k++) {
134+
const int j = offset + k;
135+
if(j < stride) {
136+
ftype yj = smem[k];
137+
ftype gj = smem[k + blockDim.x];
138+
139+
auto jacobian = (i == j) ? yi * (1 - yj) : -yi * yj;
140+
grad += gj * jacobian;
141+
}
142+
}
143+
__syncthreads();
144+
}
145+
146+
if(isNotPadded) {
147+
res[gid] = grad;
148+
}
149+
}
150+
}
151+
152+
namespace cuda_impl {
153+
void reluBackward(Tensor& res, const Tensor& upstreamGrad, const Tensor& parent) {
154+
constexpr int threadsPerBlock = 256;
155+
const int blocks = (upstreamGrad.getSize() + threadsPerBlock - 1) / threadsPerBlock;
156+
157+
reluBackwardKernel<<<blocks, threadsPerBlock>>>(res.getData(), upstreamGrad.getData(), parent.getData(), res.getSize());
158+
cudaErrchk(cudaDeviceSynchronize());
159+
}
160+
161+
void leakyReluBackward(Tensor& res, const Tensor& upstreamGrad, const Tensor& parent, ftype eps) {
162+
constexpr int threadsPerBlock = 256;
163+
const int blocks = (upstreamGrad.getSize() + threadsPerBlock - 1) / threadsPerBlock;
164+
165+
leakyReluBackwardKernel<<<blocks, threadsPerBlock>>>(res.getData(), upstreamGrad.getData(), parent.getData(), eps, res.getSize());
166+
cudaErrchk(cudaDeviceSynchronize());
167+
}
168+
169+
void sigmoidBackward(Tensor& res, const Tensor& upstreamGrad, const Tensor& sigmoid) {
170+
constexpr int threadsPerBlock = 256;
171+
const int blocks = (upstreamGrad.getSize() + threadsPerBlock - 1) / threadsPerBlock;
172+
173+
sigmoidBackwardKernel<<<blocks, threadsPerBlock>>>(res.getData(), upstreamGrad.getData(), sigmoid.getData(), res.getSize());
174+
cudaErrchk(cudaDeviceSynchronize());
175+
}
176+
177+
/**
178+
* @brief The backward of the softmax. Due to optimization this function distinguishes three cases of stride size, where stride
179+
* is the size of the dimension the softmax operation is applied to. The two cases are a stride either fitting into one block or not.
180+
*/
181+
void softmaxBackward(Tensor& res, const Tensor& upstreamGrad, const Tensor& softmax) {
182+
assert(upstreamGrad.getSize() == softmax.getSize());
183+
184+
constexpr int maxThreadsPerBlock = 256;
185+
const int stride = softmax.getDims()[-1];
186+
187+
if(stride < maxThreadsPerBlock) {
188+
const int threadsPerStride = max(1, ((stride + 31) / 32)) * 32; // == warps per stride * 32
189+
190+
// min over maximum possible strides per block and actual number of strides
191+
const int stridesPerBlock = min(maxThreadsPerBlock / threadsPerStride, softmax.getSize() / stride);
192+
const int strideWidthPerBlock = stridesPerBlock * stride; // for smem idx computation
193+
194+
int threadsPerBlock = 1;
195+
while(threadsPerBlock < threadsPerStride * stridesPerBlock) threadsPerBlock <<= 1;
196+
// threadsPerBlock now larger than threadsPerStride * stridesPerBlock
197+
const int blocks = (upstreamGrad.getSize() + threadsPerBlock - 1) / threadsPerBlock;
198+
199+
softmaxBackwardKernelOneBlock<<<blocks, threadsPerBlock, 2 * strideWidthPerBlock * sizeof(ftype)>>>(
200+
res.getData(), upstreamGrad.getData(), softmax.getData(), stride, strideWidthPerBlock, threadsPerStride, softmax.getSize());
201+
}
202+
else {
203+
constexpr int maxThreadsPerBlock = 256;
204+
205+
const int nStrides = softmax.getSize() / stride;
206+
const int threadsPerBlock = maxThreadsPerBlock; // TODO: do that one better, this can result in gross imbalance; also for normal softmax
207+
const int blocksPerStride = (stride + threadsPerBlock - 1) / threadsPerBlock;
208+
209+
softmaxBackwardKernelLargePass<<<blocksPerStride * nStrides, threadsPerBlock, 2 * threadsPerBlock * sizeof(ftype)>>>(
210+
res.getData(), upstreamGrad.getData(), softmax.getData(), blocksPerStride, stride);
211+
}
212+
cudaErrchk(cudaDeviceSynchronize());
213+
}
214+
}

0 commit comments

Comments
 (0)