From 63f34b1606bad1ad131e6389186046ac36f39d25 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Wed, 11 Sep 2019 00:12:54 -0400 Subject: [PATCH 01/20] up to naive scan - bug still in code --- .../stream_compaction/CMakeLists.txt | 2 +- .../stream_compaction/cpu.cu | 32 +++++++++++- .../stream_compaction/naive.cu | 49 ++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt index cdbef77..272a7e8 100644 --- a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt +++ b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt @@ -13,5 +13,5 @@ set(SOURCE_FILES cuda_add_library(stream_compaction ${SOURCE_FILES} - OPTIONS -arch=sm_20 + OPTIONS -arch=sm_70 ) diff --git a/Project2-Stream-Compaction/stream_compaction/cpu.cu b/Project2-Stream-Compaction/stream_compaction/cpu.cu index a2d3e6c..76b306d 100644 --- a/Project2-Stream-Compaction/stream_compaction/cpu.cu +++ b/Project2-Stream-Compaction/stream_compaction/cpu.cu @@ -20,6 +20,10 @@ namespace StreamCompaction { void scan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + odata[0] = 0; + for (int i = 1; i < n; i++) { + odata[i] = odata[i - 1] + idata[i - 1]; + } timer().endCpuTimer(); } @@ -31,8 +35,14 @@ namespace StreamCompaction { int compactWithoutScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + int k = 0; + for (int i = 0; i < n; i++) { + if (idata[i] != 0) { + odata[k++] = idata[i]; + } + } timer().endCpuTimer(); - return -1; + return k; } /** @@ -43,8 +53,26 @@ namespace StreamCompaction { int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + int *map_array = new int[n]; + int *scanned = new int[n]; + for (int i = 0; i < n; i++) { + map_array[i] = idata[i] != 0 ? 1 : 0; + } + scanned[0] = 0; + for (int i = 1; i < n; i++) { + scanned[i] = scanned[i - 1] + map_array[i - 1]; + } + int k = 0; + for (int i = 0; i < n; i++) { + if (map_array[i]) { + k++; + odata[scanned[i]] = idata[i]; + } + } + delete[] map_array; + delete[] scanned; timer().endCpuTimer(); - return -1; + return k; } } } diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index 4308876..8055ec3 100644 --- a/Project2-Stream-Compaction/stream_compaction/naive.cu +++ b/Project2-Stream-Compaction/stream_compaction/naive.cu @@ -3,6 +3,9 @@ #include "common.h" #include "naive.h" +/*! Block size used for CUDA kernel launch. */ +#define blockSize 128 + namespace StreamCompaction { namespace Naive { using StreamCompaction::Common::PerformanceTimer; @@ -11,15 +14,57 @@ namespace StreamCompaction { static PerformanceTimer timer; return timer; } - // TODO: __global__ + __global__ void kernScan(int N, int p, int *odata, int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= N) { + return; + } + if (index >= p) { + odata[index] = idata[index - p] + idata[index]; + } + else { + odata[index] = idata[index]; + } + + } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + int *dev_idata; + int *dev_odata; + int *temp; + + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + //checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMalloc((void**)&dev_odata, n * sizeof(int)); + //checkCUDAErrorWithLine("cudaMalloc dev_odata failed!"); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + timer().startGpuTimer(); // TODO + for (int d = 1; d <= ilog2ceil(n); d++) { + //int p = 1 << (d - 1); + int p = pow(2,d-1); + kernScan << > > (n, p, dev_odata, dev_idata); + cudaThreadSynchronize(); + temp = dev_idata; + dev_idata = dev_odata; + dev_odata = temp; + } timer().endGpuTimer(); + + cudaMemcpy(odata + 1, dev_idata, sizeof(int) * n - 1, cudaMemcpyDeviceToHost); + odata[0] = 0; + + cudaFree(dev_idata); + cudaFree(dev_odata); } } } From 1a2fa26784311436701272e5d3dff32f86239984 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Wed, 11 Sep 2019 22:51:19 -0400 Subject: [PATCH 02/20] fixed naive = now debuggin down-sweep on efficient --- .../stream_compaction/CMakeLists.txt | 2 +- .../stream_compaction/efficient.cu | 62 +++++++++++++++++++ .../stream_compaction/naive.cu | 42 +++++++++---- 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt index 272a7e8..4bb0dc2 100644 --- a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt +++ b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt @@ -13,5 +13,5 @@ set(SOURCE_FILES cuda_add_library(stream_compaction ${SOURCE_FILES} - OPTIONS -arch=sm_70 + OPTIONS -arch=sm_61 ) diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.cu b/Project2-Stream-Compaction/stream_compaction/efficient.cu index 2db346e..fb7be80 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.cu +++ b/Project2-Stream-Compaction/stream_compaction/efficient.cu @@ -3,6 +3,11 @@ #include "common.h" #include "efficient.h" + +#define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) +/*! Block size used for CUDA kernel launch. */ +#define blockSize 128 + namespace StreamCompaction { namespace Efficient { using StreamCompaction::Common::PerformanceTimer; @@ -11,14 +16,71 @@ namespace StreamCompaction { static PerformanceTimer timer; return timer; } + + __global__ void kernUpSweep(int N, int p, int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= N) { + return; + } + if (index % 2 * p == 0) { + idata[index + 2 * p - 1] += idata[index + p - 1]; + } + + } + + __global__ void kernDownSweep(int N, int p, int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= N) { + return; + } + if (index == N - 1) { + idata[N - 1] = 0; + } + + if (index % 2 * p == 0) { + int t = idata[index + p - 1]; + idata[index + p - 1] = idata[index + (2 * p) - 1]; + idata[index + (2 * p) - 1] += t; + } + } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + int *dev_idata; + + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_idata failed!"); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy idata failed!"); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + timer().startGpuTimer(); // TODO + for (int d = 0; d < ilog2ceil(n) - 1; d++) { + int p = 1 << d; + //int p = pow(2, d); + kernUpSweep << > > (n, p, dev_idata); + checkCUDAError("kernel kernUpSweep failed!"); + } + + for (int d = ilog2ceil(n) - 1; d > 0; d--) { + int p = 1 << d; + //int p = pow(2, d); + kernDownSweep << > > (n, p, dev_idata); + checkCUDAError("kernel kernDownSweep failed!"); + } + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, sizeof(int) * n, cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy odata failed!"); + + cudaFree(dev_idata); } /** diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index 8055ec3..c9627e0 100644 --- a/Project2-Stream-Compaction/stream_compaction/naive.cu +++ b/Project2-Stream-Compaction/stream_compaction/naive.cu @@ -3,6 +3,8 @@ #include "common.h" #include "naive.h" + +#define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) /*! Block size used for CUDA kernel launch. */ #define blockSize 128 @@ -28,40 +30,54 @@ namespace StreamCompaction { } + __global__ void kernRightShift(int N, int *odata, int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= N) { + return; + } + if (index == 0) { + odata[index] = 0; + } + odata[index + 1] = idata[index]; + + } + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { int *dev_idata; int *dev_odata; - int *temp; cudaMalloc((void**)&dev_idata, n * sizeof(int)); - //checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + checkCUDAError("cudaMalloc dev_idata failed!"); cudaMalloc((void**)&dev_odata, n * sizeof(int)); - //checkCUDAErrorWithLine("cudaMalloc dev_odata failed!"); + checkCUDAError("cudaMalloc dev_odata failed!"); cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy idata failed!"); dim3 threadsPerBlock(blockSize); dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); timer().startGpuTimer(); - // TODO + // TODO ilog2ceil(n) for (int d = 1; d <= ilog2ceil(n); d++) { - //int p = 1 << (d - 1); - int p = pow(2,d-1); - kernScan << > > (n, p, dev_odata, dev_idata); - cudaThreadSynchronize(); - temp = dev_idata; - dev_idata = dev_odata; - dev_odata = temp; + int p = 1 << (d - 1); + //int p = pow(2,d-1); + kernScan<<>>(n, p, dev_odata, dev_idata); + checkCUDAError("kernel kernScan failed!"); + + std::swap(dev_idata, dev_odata); } + + kernRightShift << > > (n, dev_odata, dev_idata); + checkCUDAError("kernel kernRightShift failed!"); timer().endGpuTimer(); - cudaMemcpy(odata + 1, dev_idata, sizeof(int) * n - 1, cudaMemcpyDeviceToHost); - odata[0] = 0; + cudaMemcpy(odata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy odata failed!"); cudaFree(dev_idata); cudaFree(dev_odata); From dfd81e775be7f53acbe9830a0e07bad3e880b012 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Thu, 12 Sep 2019 12:46:07 -0400 Subject: [PATCH 03/20] need power of two and fix bugs on efficient algorithm --- .../character_recognition/CMakeLists.txt | 2 +- .../stream_compaction/common.cu | 12 +++ .../stream_compaction/efficient.cu | 86 ++++++++++++++----- .../stream_compaction/naive.cu | 10 +-- .../stream_compaction/thrust.cu | 2 + 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/CMakeLists.txt b/Project2-Character-Recognition/character_recognition/CMakeLists.txt index 7446175..9e834c1 100644 --- a/Project2-Character-Recognition/character_recognition/CMakeLists.txt +++ b/Project2-Character-Recognition/character_recognition/CMakeLists.txt @@ -7,5 +7,5 @@ set(SOURCE_FILES cuda_add_library(character_recognition ${SOURCE_FILES} - OPTIONS -arch=sm_20 + OPTIONS -arch=sm_61 ) diff --git a/Project2-Stream-Compaction/stream_compaction/common.cu b/Project2-Stream-Compaction/stream_compaction/common.cu index 2ed6d63..a5fb59b 100644 --- a/Project2-Stream-Compaction/stream_compaction/common.cu +++ b/Project2-Stream-Compaction/stream_compaction/common.cu @@ -24,6 +24,11 @@ namespace StreamCompaction { */ __global__ void kernMapToBoolean(int n, int *bools, const int *idata) { // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + bools[index] = idata[index] != 0 ? 1 : 0; } /** @@ -33,6 +38,13 @@ namespace StreamCompaction { __global__ void kernScatter(int n, int *odata, const int *idata, const int *bools, const int *indices) { // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + if (bools[index]) { + odata[indices[index]] = idata[index]; + } } } diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.cu b/Project2-Stream-Compaction/stream_compaction/efficient.cu index fb7be80..4473c75 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.cu +++ b/Project2-Stream-Compaction/stream_compaction/efficient.cu @@ -4,7 +4,7 @@ #include "efficient.h" -#define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) + /*! Block size used for CUDA kernel launch. */ #define blockSize 128 @@ -17,9 +17,9 @@ namespace StreamCompaction { return timer; } - __global__ void kernUpSweep(int N, int p, int *idata) { + __global__ void kernUpSweep(int n, int p, int *idata) { int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= N) { + if (index >= n) { return; } if (index % 2 * p == 0) { @@ -28,13 +28,13 @@ namespace StreamCompaction { } - __global__ void kernDownSweep(int N, int p, int *idata) { + __global__ void kernDownSweep(int n, int p, int *idata) { int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= N) { + if (index >= n) { return; } - if (index == N - 1) { - idata[N - 1] = 0; + if (index == n - 1) { + idata[n - 1] = 0; } if (index % 2 * p == 0) { @@ -44,6 +44,23 @@ namespace StreamCompaction { } } + void workEfficientScan(int n, int *dev_idata, dim3 &threadsPerBlock, dim3 &fullBlocksPerGrid) { + for (int d = 0; d < ilog2ceil(n) - 1; d++) { + int p = 1 << d; + //int p = pow(2, d); + kernUpSweep << > > (n, p, dev_idata); + checkCUDAError("kernel kernUpSweep failed!"); + } + + for (int d = ilog2ceil(n) - 1; d > 0; d--) { + int p = 1 << d; + //int p = pow(2, d); + kernDownSweep << > > (n, p, dev_idata); + checkCUDAError("kernel kernDownSweep failed!"); + } + + } + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ @@ -61,19 +78,7 @@ namespace StreamCompaction { timer().startGpuTimer(); // TODO - for (int d = 0; d < ilog2ceil(n) - 1; d++) { - int p = 1 << d; - //int p = pow(2, d); - kernUpSweep << > > (n, p, dev_idata); - checkCUDAError("kernel kernUpSweep failed!"); - } - - for (int d = ilog2ceil(n) - 1; d > 0; d--) { - int p = 1 << d; - //int p = pow(2, d); - kernDownSweep << > > (n, p, dev_idata); - checkCUDAError("kernel kernDownSweep failed!"); - } + workEfficientScan(n, dev_idata, threadsPerBlock, fullBlocksPerGrid); timer().endGpuTimer(); @@ -93,9 +98,50 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { + int *dev_idata; + int *dev_indices; + int *dev_odata; + int *dev_bools; + + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_idata failed!"); + + cudaMalloc((void**)&dev_odata, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_odata failed!"); + + cudaMalloc((void**)&dev_indices, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_indices failed!"); + + cudaMalloc((void**)&dev_bools, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_bools failed!"); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy idata failed!"); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + timer().startGpuTimer(); // TODO + StreamCompaction::Common::kernMapToBoolean<< > > (n, dev_bools, dev_idata); + + cudaMemcpy(dev_indices, dev_bools, sizeof(int) * n, cudaMemcpyDeviceToDevice); + checkCUDAError("Memcpy odata failed!"); + + workEfficientScan(n, dev_indices, threadsPerBlock, fullBlocksPerGrid); + + StreamCompaction::Common::kernScatter << > > (n, dev_odata, dev_idata, dev_bools, dev_indices); + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy odata failed!"); + + cudaFree(dev_idata); + cudaFree(dev_indices); + cudaFree(dev_odata); + cudaFree(dev_bools); + return -1; } } diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index c9627e0..4a03548 100644 --- a/Project2-Stream-Compaction/stream_compaction/naive.cu +++ b/Project2-Stream-Compaction/stream_compaction/naive.cu @@ -3,8 +3,6 @@ #include "common.h" #include "naive.h" - -#define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) /*! Block size used for CUDA kernel launch. */ #define blockSize 128 @@ -16,9 +14,9 @@ namespace StreamCompaction { static PerformanceTimer timer; return timer; } - __global__ void kernScan(int N, int p, int *odata, int *idata) { + __global__ void kernScan(int n, int p, int *odata, int *idata) { int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= N) { + if (index >= n) { return; } if (index >= p) { @@ -30,9 +28,9 @@ namespace StreamCompaction { } - __global__ void kernRightShift(int N, int *odata, int *idata) { + __global__ void kernRightShift(int n, int *odata, int *idata) { int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= N) { + if (index >= n) { return; } if (index == 0) { diff --git a/Project2-Stream-Compaction/stream_compaction/thrust.cu b/Project2-Stream-Compaction/stream_compaction/thrust.cu index 1def45e..d6835c8 100644 --- a/Project2-Stream-Compaction/stream_compaction/thrust.cu +++ b/Project2-Stream-Compaction/stream_compaction/thrust.cu @@ -22,6 +22,8 @@ namespace StreamCompaction { // TODO use `thrust::exclusive_scan` // example: for device_vectors dv_in and dv_out: // thrust::exclusive_scan(dv_in.begin(), dv_in.end(), dv_out.begin()); + thrust::exclusive_scan(idata, idata + n, odata); + timer().endGpuTimer(); } } From 579b292497355bcab1aad9c20cf5b960835c8014 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Thu, 12 Sep 2019 17:13:54 -0400 Subject: [PATCH 04/20] added non power of 2 capability and fixed bug in downsweep with setting root to 0 --- .../character_recognition/mlp.cu | 4 + .../character_recognition/mlp.h | 1 + Project2-Character-Recognition/src/main.cpp | 124 ++---------------- Project2-Stream-Compaction/src/main.cpp | 2 +- .../stream_compaction/common.cu | 2 - .../stream_compaction/cpu.cu | 29 ++-- .../stream_compaction/efficient.cu | 66 ++++++---- .../stream_compaction/naive.cu | 11 +- 8 files changed, 82 insertions(+), 157 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index 5a3ed7f..e5b5835 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -24,4 +24,8 @@ namespace CharacterRecognition { */ // TODO: implement required elements for MLP sections 1 and 2 here + + void train() { + + } } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index 2096228..1e6ac07 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -6,4 +6,5 @@ namespace CharacterRecognition { Common::PerformanceTimer& timer(); // TODO: implement required elements for MLP sections 1 and 2 here + void train(); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index 11dd534..429e559 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -22,128 +22,26 @@ int main(int argc, char* argv[]) { printf("\n"); printf("****************\n"); - printf("** SCAN TESTS **\n"); + printf("** MLP TESTS **\n"); printf("****************\n"); genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; printArray(SIZE, a, true); - // initialize b using StreamCompaction::CPU::scan you implement - // We use b for further comparison. Make sure your StreamCompaction::CPU::scan is correct. - // At first all cases passed because b && c are all zeroes. - zeroArray(SIZE, b); - printDesc("cpu scan, power-of-two"); - StreamCompaction::CPU::scan(SIZE, b, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(SIZE, b, true); + printDesc("train"); - zeroArray(SIZE, c); - printDesc("cpu scan, non-power-of-two"); - StreamCompaction::CPU::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(NPOT, b, true); - printCmpResult(NPOT, b, c); + CharacterRecognition::train(); - zeroArray(SIZE, c); - printDesc("naive scan, power-of-two"); - StreamCompaction::Naive::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(SIZE, b, c); + printDesc("test"); + - /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan - onesArray(SIZE, c); - printDesc("1s array for finding bugs"); - StreamCompaction::Naive::scan(SIZE, c, a); - printArray(SIZE, c, true); */ - - zeroArray(SIZE, c); - printDesc("naive scan, non-power-of-two"); - StreamCompaction::Naive::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient scan, power-of-two"); - StreamCompaction::Efficient::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(SIZE, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient scan, non-power-of-two"); - StreamCompaction::Efficient::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(NPOT, c, true); - printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("thrust scan, power-of-two"); - StreamCompaction::Thrust::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(SIZE, b, c); - - zeroArray(SIZE, c); - printDesc("thrust scan, non-power-of-two"); - StreamCompaction::Thrust::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(NPOT, c, true); - printCmpResult(NPOT, b, c); - - printf("\n"); - printf("*****************************\n"); - printf("** STREAM COMPACTION TESTS **\n"); - printf("*****************************\n"); - - // Compaction tests - - genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case - a[SIZE - 1] = 0; - printArray(SIZE, a, true); - - int count, expectedCount, expectedNPOT; - - // initialize b using StreamCompaction::CPU::compactWithoutScan you implement - // We use b for further comparison. Make sure your StreamCompaction::CPU::compactWithoutScan is correct. - zeroArray(SIZE, b); - printDesc("cpu compact without scan, power-of-two"); - count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - expectedCount = count; - printArray(count, b, true); - printCmpLenResult(count, expectedCount, b, b); - - zeroArray(SIZE, c); - printDesc("cpu compact without scan, non-power-of-two"); - count = StreamCompaction::CPU::compactWithoutScan(NPOT, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - expectedNPOT = count; - printArray(count, c, true); - printCmpLenResult(count, expectedNPOT, b, c); - - zeroArray(SIZE, c); - printDesc("cpu compact with scan"); - count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(count, c, true); - printCmpLenResult(count, expectedCount, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient compact, power-of-two"); - count = StreamCompaction::Efficient::compact(SIZE, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); - printCmpLenResult(count, expectedCount, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient compact, non-power-of-two"); - count = StreamCompaction::Efficient::compact(NPOT, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); - printCmpLenResult(count, expectedNPOT, b, c); + //zeroArray(SIZE, c); + //printDesc("work-efficient compact, non-power-of-two"); + //count = StreamCompaction::Efficient::compact(NPOT, c, a); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(count, c, true); + //printCmpLenResult(count, expectedNPOT, b, c); system("pause"); // stop Win32 console from closing on exit delete[] a; diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index d016553..a0493db 100644 --- a/Project2-Stream-Compaction/src/main.cpp +++ b/Project2-Stream-Compaction/src/main.cpp @@ -13,7 +13,7 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 8; // feel free to change the size of array +const int SIZE = 1 << 8; //1 << 8; // feel free to change the size of array const int NPOT = SIZE - 3; // Non-Power-Of-Two int *a = new int[SIZE]; int *b = new int[SIZE]; diff --git a/Project2-Stream-Compaction/stream_compaction/common.cu b/Project2-Stream-Compaction/stream_compaction/common.cu index a5fb59b..ccffc8c 100644 --- a/Project2-Stream-Compaction/stream_compaction/common.cu +++ b/Project2-Stream-Compaction/stream_compaction/common.cu @@ -23,7 +23,6 @@ namespace StreamCompaction { * which map to 0 will be removed, and elements which map to 1 will be kept. */ __global__ void kernMapToBoolean(int n, int *bools, const int *idata) { - // TODO int index = threadIdx.x + (blockIdx.x * blockDim.x); if (index >= n) { return; @@ -37,7 +36,6 @@ namespace StreamCompaction { */ __global__ void kernScatter(int n, int *odata, const int *idata, const int *bools, const int *indices) { - // TODO int index = threadIdx.x + (blockIdx.x * blockDim.x); if (index >= n) { return; diff --git a/Project2-Stream-Compaction/stream_compaction/cpu.cu b/Project2-Stream-Compaction/stream_compaction/cpu.cu index 76b306d..4079764 100644 --- a/Project2-Stream-Compaction/stream_compaction/cpu.cu +++ b/Project2-Stream-Compaction/stream_compaction/cpu.cu @@ -12,6 +12,13 @@ namespace StreamCompaction { return timer; } + void scanHelper(int n, int *odata, const int *idata) { + odata[0] = 0; + for (int i = 1; i < n; i++) { + odata[i] = odata[i - 1] + idata[i - 1]; + } + } + /** * CPU scan (prefix sum). * For performance analysis, this is supposed to be a simple for loop. @@ -19,11 +26,7 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO - odata[0] = 0; - for (int i = 1; i < n; i++) { - odata[i] = odata[i - 1] + idata[i - 1]; - } + scanHelper(n, odata, idata); timer().endCpuTimer(); } @@ -34,7 +37,6 @@ namespace StreamCompaction { */ int compactWithoutScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO int k = 0; for (int i = 0; i < n; i++) { if (idata[i] != 0) { @@ -52,16 +54,17 @@ namespace StreamCompaction { */ int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO int *map_array = new int[n]; int *scanned = new int[n]; + + // mapping to boolean for (int i = 0; i < n; i++) { map_array[i] = idata[i] != 0 ? 1 : 0; } - scanned[0] = 0; - for (int i = 1; i < n; i++) { - scanned[i] = scanned[i - 1] + map_array[i - 1]; - } + // scanning exclusively + scanHelper(n, scanned, map_array); + + //scatter results int k = 0; for (int i = 0; i < n; i++) { if (map_array[i]) { @@ -69,8 +72,8 @@ namespace StreamCompaction { odata[scanned[i]] = idata[i]; } } - delete[] map_array; - delete[] scanned; + delete[n] map_array; + delete[n] scanned; timer().endCpuTimer(); return k; } diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.cu b/Project2-Stream-Compaction/stream_compaction/efficient.cu index 4473c75..2d6875b 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.cu +++ b/Project2-Stream-Compaction/stream_compaction/efficient.cu @@ -22,7 +22,8 @@ namespace StreamCompaction { if (index >= n) { return; } - if (index % 2 * p == 0) { + + if (index % (2 * p) == 0) { idata[index + 2 * p - 1] += idata[index + p - 1]; } @@ -33,11 +34,8 @@ namespace StreamCompaction { if (index >= n) { return; } - if (index == n - 1) { - idata[n - 1] = 0; - } - if (index % 2 * p == 0) { + if (index % (2 * p) == 0) { int t = idata[index + p - 1]; idata[index + p - 1] = idata[index + (2 * p) - 1]; idata[index + (2 * p) - 1] += t; @@ -45,16 +43,19 @@ namespace StreamCompaction { } void workEfficientScan(int n, int *dev_idata, dim3 &threadsPerBlock, dim3 &fullBlocksPerGrid) { - for (int d = 0; d < ilog2ceil(n) - 1; d++) { + //perform upsweep parallel reduction + for (int d = 0; d < ilog2ceil(n); d++) { int p = 1 << d; - //int p = pow(2, d); kernUpSweep << > > (n, p, dev_idata); checkCUDAError("kernel kernUpSweep failed!"); } - for (int d = ilog2ceil(n) - 1; d > 0; d--) { + //set root to 0 + cudaMemset(dev_idata + n - 1, 0, sizeof(int)); + + //perform down sweep as binary tree + for (int d = ilog2ceil(n)-1 ; d >= 0; d--) { int p = 1 << d; - //int p = pow(2, d); kernDownSweep << > > (n, p, dev_idata); checkCUDAError("kernel kernDownSweep failed!"); } @@ -65,20 +66,23 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + //set up variable, allocate space on gpu, and copy over data + int npad = 1 << ilog2ceil(n); //pads adds padding if needed for arrays of not power of 2 length int *dev_idata; - cudaMalloc((void**)&dev_idata, n * sizeof(int)); + cudaMalloc((void**)&dev_idata, npad * sizeof(int)); checkCUDAError("cudaMalloc dev_idata failed!"); cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); checkCUDAError("Memcpy idata failed!"); dim3 threadsPerBlock(blockSize); - dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + dim3 fullBlocksPerGrid((npad + blockSize - 1) / blockSize); timer().startGpuTimer(); - // TODO - workEfficientScan(n, dev_idata, threadsPerBlock, fullBlocksPerGrid); + + //call work efficient scan helper + workEfficientScan(npad, dev_idata, threadsPerBlock, fullBlocksPerGrid); timer().endGpuTimer(); @@ -98,51 +102,65 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { + //set up variable, allocate space on gpu, and copy over data + int npad = 1 << ilog2ceil(n); int *dev_idata; int *dev_indices; int *dev_odata; int *dev_bools; - cudaMalloc((void**)&dev_idata, n * sizeof(int)); + cudaMalloc((void**)&dev_idata, npad * sizeof(int)); checkCUDAError("cudaMalloc dev_idata failed!"); - cudaMalloc((void**)&dev_odata, n * sizeof(int)); + cudaMalloc((void**)&dev_odata, npad * sizeof(int)); checkCUDAError("cudaMalloc dev_odata failed!"); - cudaMalloc((void**)&dev_indices, n * sizeof(int)); + cudaMalloc((void**)&dev_indices, npad * sizeof(int)); checkCUDAError("cudaMalloc dev_indices failed!"); - cudaMalloc((void**)&dev_bools, n * sizeof(int)); + cudaMalloc((void**)&dev_bools, npad * sizeof(int)); checkCUDAError("cudaMalloc dev_bools failed!"); cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); checkCUDAError("Memcpy idata failed!"); dim3 threadsPerBlock(blockSize); - dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + dim3 fullBlocksPerGrid((npad + blockSize - 1) / blockSize); timer().startGpuTimer(); - // TODO - StreamCompaction::Common::kernMapToBoolean<< > > (n, dev_bools, dev_idata); - cudaMemcpy(dev_indices, dev_bools, sizeof(int) * n, cudaMemcpyDeviceToDevice); + // map data to boolean + StreamCompaction::Common::kernMapToBoolean<< > > (npad, dev_bools, dev_idata); + checkCUDAError("Memcpy kernMapToBoolean failed!"); + + //copy to indices to call on workEfficientScan inplace + cudaMemcpy(dev_indices, dev_bools, sizeof(int) * npad, cudaMemcpyDeviceToDevice); checkCUDAError("Memcpy odata failed!"); - workEfficientScan(n, dev_indices, threadsPerBlock, fullBlocksPerGrid); + //call work efficient exclusive scan helper to edit dev_indices inplace + workEfficientScan(npad, dev_indices, threadsPerBlock, fullBlocksPerGrid); - StreamCompaction::Common::kernScatter << > > (n, dev_odata, dev_idata, dev_bools, dev_indices); + //scatter results from scan + StreamCompaction::Common::kernScatter << > > (npad, dev_odata, dev_idata, dev_bools, dev_indices); + checkCUDAError("Memcpy kernScatter failed!"); timer().endGpuTimer(); + //copy back output cudaMemcpy(odata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); checkCUDAError("Memcpy odata failed!"); + // find length of output array as final element of indices array + int *k = new int; + cudaMemcpy(k, dev_indices + npad - 1, sizeof(int), cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy dev_indices failed!"); + cudaFree(dev_idata); cudaFree(dev_indices); cudaFree(dev_odata); cudaFree(dev_bools); - return -1; + return *k; } } } diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index 4a03548..0a146bc 100644 --- a/Project2-Stream-Compaction/stream_compaction/naive.cu +++ b/Project2-Stream-Compaction/stream_compaction/naive.cu @@ -44,6 +44,7 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + //set up variable, allocate space on gpu, and copy over data int *dev_idata; int *dev_odata; @@ -60,18 +61,20 @@ namespace StreamCompaction { dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); timer().startGpuTimer(); - // TODO ilog2ceil(n) - for (int d = 1; d <= ilog2ceil(n); d++) { - int p = 1 << (d - 1); - //int p = pow(2,d-1); + + //inclusive scanning + for (int d = 0; d < ilog2ceil(n); d++) { + int p = 1 << d; kernScan<<>>(n, p, dev_odata, dev_idata); checkCUDAError("kernel kernScan failed!"); std::swap(dev_idata, dev_odata); } + //right shift to convert to exclusive scan kernRightShift << > > (n, dev_odata, dev_idata); checkCUDAError("kernel kernRightShift failed!"); + timer().endGpuTimer(); cudaMemcpy(odata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); From 2d618bd838bd47aa20e22b57e72ef8677348bbbf Mon Sep 17 00:00:00 2001 From: klaywittler Date: Fri, 13 Sep 2019 10:27:27 -0400 Subject: [PATCH 05/20] some potential helper functions --- .../character_recognition/mlp.cu | 59 ++++++++++++++++++- .../character_recognition/mlp.h | 2 + 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index e5b5835..bd845bc 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -3,6 +3,9 @@ #include "common.h" #include "mlp.h" +/*! Block size used for CUDA kernel launch. */ +#define blockSize 128 + namespace CharacterRecognition { using Common::PerformanceTimer; PerformanceTimer& timer() @@ -24,8 +27,62 @@ namespace CharacterRecognition { */ // TODO: implement required elements for MLP sections 1 and 2 here + __global__ void addVector(int n, float* vec1, float* vec2, float* result) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + result[index] = vec1[index] + vec2[index]; + } + + __global__ void elementMultVect(int n, float *input, float *output, float *weight) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + output[index] = input[index]*weight[index]; + } + + float sigmoid(float x) { + return 1.0f / (1 + exp(-x)); + } - void train() { + float mlp(float *input, float *output, float *weight) { + + + //thrust::inclusive_scan(idata, idata + n, odata); + } + + void network() { } + + void partition() { + + } + void train(int n, float *input, float* output) { + int *dev_input; + int *dev_output; + int *dev_hidden; + + cudaMalloc((void**)&dev_input, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_input failed!"); + + cudaMalloc((void**)&dev_hidden, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_output failed!"); + + cudaMalloc((void**)&dev_output, n * sizeof(int)); + checkCUDAError("cudaMalloc dev_output failed!"); + + cudaMemcpy(dev_input, input, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy input failed!"); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + } + void test() { + + } + } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index 1e6ac07..e573ede 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -6,5 +6,7 @@ namespace CharacterRecognition { Common::PerformanceTimer& timer(); // TODO: implement required elements for MLP sections 1 and 2 here + void partition(); void train(); + void test(); } From 2d862bee5e736640347b3c765f15a9006838d122 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Sat, 14 Sep 2019 22:16:45 -0400 Subject: [PATCH 06/20] an attempt at matrix multiply --- Project2-Character-Recognition/CMakeLists.txt | 3 + .../character_recognition/CMakeLists.txt | 3 + .../character_recognition/mlp.cu | 298 +++++++++++++++--- .../character_recognition/mlp.h | 9 +- Project2-Character-Recognition/src/main.cpp | 63 ++-- 5 files changed, 306 insertions(+), 70 deletions(-) diff --git a/Project2-Character-Recognition/CMakeLists.txt b/Project2-Character-Recognition/CMakeLists.txt index 09e9198..3301b19 100644 --- a/Project2-Character-Recognition/CMakeLists.txt +++ b/Project2-Character-Recognition/CMakeLists.txt @@ -22,6 +22,7 @@ if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") endif() include_directories(.) +link_directories(${CUDA_TOOLKIT_ROOT_DIR}/lib/x64) add_subdirectory(character_recognition) cuda_add_executable(${CMAKE_PROJECT_NAME} @@ -32,4 +33,6 @@ cuda_add_executable(${CMAKE_PROJECT_NAME} target_link_libraries(${CMAKE_PROJECT_NAME} character_recognition ${CORELIBS} + cublas ) + diff --git a/Project2-Character-Recognition/character_recognition/CMakeLists.txt b/Project2-Character-Recognition/character_recognition/CMakeLists.txt index 9e834c1..cbe0e75 100644 --- a/Project2-Character-Recognition/character_recognition/CMakeLists.txt +++ b/Project2-Character-Recognition/character_recognition/CMakeLists.txt @@ -9,3 +9,6 @@ cuda_add_library(character_recognition ${SOURCE_FILES} OPTIONS -arch=sm_61 ) + +target_link_libraries(character_recognition ${CUDA_LIBRARIES}) +target_link_libraries(character_recognition ${CUDA_CUBLAS_LIBRARIES}) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index bd845bc..e7d2f2d 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -2,9 +2,13 @@ #include #include "common.h" #include "mlp.h" +#include /*! Block size used for CUDA kernel launch. */ -#define blockSize 128 +#define blockSize 32 +#define index(i,j,ld) (((j)*(ld))+(i)) + + namespace CharacterRecognition { using Common::PerformanceTimer; @@ -13,76 +17,280 @@ namespace CharacterRecognition { static PerformanceTimer timer; return timer; } - - // TODO: __global__ - - /** - * Example of use case (follow how you did it in stream compaction) - */ - /*void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); - // TODO - timer().endGpuTimer(); - } - */ - // TODO: implement required elements for MLP sections 1 and 2 here - __global__ void addVector(int n, float* vec1, float* vec2, float* result) { - int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= n) { - return; + typedef struct _matrixSize // Optional Command-line multiplier for matrix sizes + { + unsigned int WA, HA, WB, HB, WC, HC; + _matrixSize(unsigned int WA=1, unsigned int HA=1, unsigned int WB=1, unsigned int HB=1, unsigned int WC=1, unsigned int HC=1){} + } sMatrixSize; + + void printMat(float*P, int uWP, int uHP) { + int i, j; + for (i = 0; i < uHP; i++) { + printf("\n"); + for (j = 0; j < uWP; j++) + printf("%f ", P[index(i, j, uHP)]); } - result[index] = vec1[index] + vec2[index]; } - __global__ void elementMultVect(int n, float *input, float *output, float *weight) { + void randomInit(float *data, int size) + { + for (int i = 0; i < size; ++i) + data[i] = rand() / (float)RAND_MAX; + } + + void indexInit(float *data, int size) + { + for (int i = 0; i < size; ++i) + data[i] = (float)i; + } + + void initializeCUDA(sMatrixSize &matrix_size) + { + matrix_size.WA = 3; + matrix_size.HA = 4; + matrix_size.WB = 2; + matrix_size.HB = 3; + matrix_size.WC = 2; + matrix_size.HC = 4; + + printf("MatrixA(%u,%u), MatrixB(%u,%u), MatrixC(%u,%u)\n", + matrix_size.HA, matrix_size.WA, + matrix_size.HB, matrix_size.WB, + matrix_size.HC, matrix_size.WC); + + } + + int getNum(int &n, float *v) { + // Generate a random number + srand(time(NULL)); + // Make sure the number is within the index range + int index = rand() % n; + // Get random number from the vector + int num = v[index]; + // Remove the number from the vector + std::swap(v[index], v[n - 1]); + n--; + // Return the removed number + return num; + } + + void generateRandom(int n, float *perm) { + float *v = (float *)malloc(n); + // Fill the vector with the values 1, 2, 3, ..., n + for (int i = 0; i < n; i++) { + v[i] = i; + } + // While vector has elements get a random number from the vector and print it + int i = 0; + while (n > 0) { + perm[i] = getNum(n,v); + i++; + } + } + + //////////////////////////////////////////////////////////////////////////////// + //! Run a simple test matrix multiply using CUBLAS + //////////////////////////////////////////////////////////////////////////////// + void matrixMultiply(cublasHandle_t* handle, sMatrixSize &matrix_size, float *d_A, float *d_B, float *d_C){ + const float alpha = 1.0f; + const float beta = 0.0f; + + cublasSgemm(*handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.WB, matrix_size.HA, matrix_size.WA, &alpha, d_B, matrix_size.WB, d_A, matrix_size.WA, &beta, d_C, matrix_size.WB); + checkCUDAError("matrix multiply"); + } + + // TODO: implement required elements for MLP sections 1 and 2 here + __global__ void kernSigmoid(int n, float *input) { int index = threadIdx.x + (blockIdx.x * blockDim.x); if (index >= n) { return; } - output[index] = input[index]*weight[index]; + input[index] = 1.0f / (1 + exp(-input[index])); } + void backward(){} - float sigmoid(float x) { - return 1.0f / (1 + exp(-x)); - } + float *forward(float *Xi, float *yi, float *wI, float *wO, sMatrixSize &hidden_matrix_size, sMatrixSize &output_matrix_size) { + // allocate device memory + float *dev_X, *dev_wI, *dev_wO, *dev_h1, *dev_pred; + unsigned int size_X = hidden_matrix_size.WB * hidden_matrix_size.HB; + unsigned int mem_size_X = sizeof(float) * size_X; + unsigned int size_wI = hidden_matrix_size.WA * hidden_matrix_size.HA; + unsigned int mem_size_wI = sizeof(float) * size_wI; + unsigned int size_wO = output_matrix_size.WA * output_matrix_size.HA; + unsigned int mem_size_wO = sizeof(float) * size_wO; + unsigned int size_h1 = hidden_matrix_size.WC * hidden_matrix_size.HC; + unsigned int mem_size_h1 = sizeof(float) * size_h1; + unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; + unsigned int mem_size_pred = sizeof(float) * size_pred; - float mlp(float *input, float *output, float *weight) { + // allocate host memory for the result + float *pred = (float *)malloc(mem_size_pred); + cudaMalloc((void **)&dev_X, mem_size_X); + checkCUDAError("cudaMalloc dev_X"); + cudaMalloc((void **)&dev_wI, mem_size_wI); + checkCUDAError("cudaMalloc dev_wI"); + cudaMalloc((void **)&dev_wO, mem_size_wO); + checkCUDAError("cudaMalloc dev_wO"); + cudaMemcpy(dev_X, Xi, mem_size_X, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_X"); + cudaMemcpy(dev_wI, wI, mem_size_wI, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_wI"); + cudaMemcpy(dev_wO, wO, mem_size_wO, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_wO"); + cudaMalloc((void **)&dev_h1, mem_size_h1); + checkCUDAError("cudaMalloc dev_h1"); + cudaMalloc((void **)&dev_pred, mem_size_pred); + checkCUDAError("cudaMalloc dev_pred"); - //thrust::inclusive_scan(idata, idata + n, odata); - } + dim3 threads(blockSize, blockSize); + dim3 grid(hidden_matrix_size.WC / threads.x, hidden_matrix_size.HC / threads.y); - void network() { + cublasHandle_t handle; + cublasCreate(&handle); - } + //hidden layer + + matrixMultiply(&handle, hidden_matrix_size, dev_wI, dev_X, dev_h1); + kernSigmoid <<> > (hidden_matrix_size.HC, dev_h1); + checkCUDAError("kernSigmoid"); + + + dim3 grid1(output_matrix_size.WC / threads.x, output_matrix_size.HC / threads.y); + //output layer + matrixMultiply(&handle, output_matrix_size, dev_wO, dev_h1, dev_pred); + kernSigmoid << > > (output_matrix_size.HC, dev_pred); + checkCUDAError("kernSigmoid"); + + cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); + checkCUDAError("cudaMemcpy pred"); + + cublasDestroy(handle); + checkCUDAError("handle"); - void partition() { + cudaFree(dev_X); + cudaFree(dev_wI); + cudaFree(dev_wO); + cudaFree(dev_h1); + cudaFree(dev_pred); + return pred; } - void train(int n, float *input, float* output) { - int *dev_input; - int *dev_output; - int *dev_hidden; - cudaMalloc((void**)&dev_input, n * sizeof(int)); - checkCUDAError("cudaMalloc dev_input failed!"); + void train(float *X, float *y, int sizeData, const int hiddenNodes, const int numLabels, const int numData) { + sMatrixSize hidden_matrix_size(1, sizeData, hiddenNodes, sizeData, 1, hiddenNodes); + sMatrixSize output_matrix_size(1, hiddenNodes, numLabels, hiddenNodes, 1, numLabels); - cudaMalloc((void**)&dev_hidden, n * sizeof(int)); - checkCUDAError("cudaMalloc dev_output failed!"); + srand(2006); - cudaMalloc((void**)&dev_output, n * sizeof(int)); - checkCUDAError("cudaMalloc dev_output failed!"); + unsigned int size_wI = sizeData * hiddenNodes; + unsigned int mem_size_wI = sizeof(float) * size_wI; + float *wI = (float *)malloc(mem_size_wI); - cudaMemcpy(dev_input, input, sizeof(int) * n, cudaMemcpyHostToDevice); - checkCUDAError("Memcpy input failed!"); + unsigned int size_wO = numLabels * hiddenNodes; + unsigned int mem_size_wO = sizeof(float) * size_wO; + float *wO = (float *)malloc(mem_size_wO); - dim3 threadsPerBlock(blockSize); - dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + randomInit(wI, size_wI); + randomInit(wO, size_wO); + float *perm = (float *)malloc(numData); + float *Xi = (float *)malloc(sizeData); + float *yi = (float *)malloc(numLabels); + + unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; + unsigned int mem_size_pred = sizeof(float) * size_pred; + float *pred = (float *)malloc(mem_size_pred); + for (int iter = 0; iter < 1000; iter++) { + generateRandom(numData, perm); + printf("here"); + for (int i = 0; i < numData; i++) { + int index = perm[i]; + printf("%i \n", index); + memcpy(Xi, (void **)&X[sizeData*i], sizeData * sizeof(float)); + memcpy(yi, (void **)&y[numLabels*i], numLabels * sizeof(float)); + pred = forward(Xi, yi, wI, wO, hidden_matrix_size, output_matrix_size); + for (int j = 0; j < numLabels; j++) { + printf("%f \n", pred[j]); + } + } + } + + free(Xi); + free(yi); + free(perm); } - void test() { + void testMatrixMultiply() { + sMatrixSize matrix_size; + + initializeCUDA(matrix_size); + + // allocate host memory for matrices A and B + unsigned int size_A = matrix_size.WA * matrix_size.HA; + unsigned int mem_size_A = sizeof(float) * size_A; + float *h_A = (float *)malloc(mem_size_A); + unsigned int size_B = matrix_size.WB * matrix_size.HB; + unsigned int mem_size_B = sizeof(float) * size_B; + float *h_B = (float *)malloc(mem_size_B); + + // set seed for rand() + srand(2006); + + // initialize host memory + indexInit(h_A, size_A); + indexInit(h_B, size_B); + + // allocate device memory + float *d_A, *d_B, *d_C; + unsigned int size_C = matrix_size.WC * matrix_size.HC; + unsigned int mem_size_C = sizeof(float) * size_C; + + // allocate host memory for the result + float *h_C = (float *)malloc(mem_size_C); + + cudaMalloc((void **)&d_A, mem_size_A); + cudaMalloc((void **)&d_B, mem_size_B); + cudaMemcpy(d_A, h_A, mem_size_A, cudaMemcpyHostToDevice); + cudaMemcpy(d_B, h_B, mem_size_B, cudaMemcpyHostToDevice); + cudaMalloc((void **)&d_C, mem_size_C); + + // setup execution parameters + dim3 threads(blockSize, blockSize); + dim3 grid(matrix_size.WC / threads.x, matrix_size.HC / threads.y); + + // create and start timer + printf("Computing result using CUBLAS..."); + + cublasHandle_t handle; + cublasCreate(&handle); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + matrixMultiply(&handle, matrix_size, d_A, d_B, d_C); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // copy result from device to host + cudaMemcpy(h_C, d_C, mem_size_C, cudaMemcpyDeviceToHost); + + // Destroy the handle + cublasDestroy(handle); + + printf("\nMatriz A:\n"); + printMat(h_A, matrix_size.WA, matrix_size.HA); + printf("\nMatriz B:\n"); + printMat(h_B, matrix_size.WB, matrix_size.HB); + printf("\nMatriz C:\n"); + printMat(h_C, matrix_size.WC, matrix_size.HC); + + // clean up memory + free(h_A); + free(h_B); + free(h_C); + cudaFree(d_A); + cudaFree(d_B); + cudaFree(d_C); } + } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index e573ede..972ff14 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -1,12 +1,17 @@ #pragma once #include "common.h" +#include namespace CharacterRecognition { Common::PerformanceTimer& timer(); // TODO: implement required elements for MLP sections 1 and 2 here - void partition(); + /*void forward(); + void backward(); void train(); - void test(); + void test();*/ + void testMatrixMultiply(); + void train(float *X, float *y, int sizeData = 10205, const int hiddenNodes = 256, const int numLabels = 52, const int numData = 52); + //void mult(cublasHandle_t* handle, const float* A, const float* B, float* C, int m, int k, int n); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index 429e559..a545785 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -10,41 +10,58 @@ #include #include #include "testing_helpers.hpp" +#include -const int SIZE = 1 << 8; // feel free to change the size of array -const int NPOT = SIZE - 3; // Non-Power-Of-Two -int *a = new int[SIZE]; -int *b = new int[SIZE]; -int *c = new int[SIZE]; +#define sizeData 10205 +#define numLabels 52 -int main(int argc, char* argv[]) { - // Scan tests +void readData(float *X, float *y) { + int c = 0; + for (int i = 1; i <= numLabels; i++) { + float ascii = 65 + c; + if (i % 2 == 0) { c++; } + float yi = i % 2 == 0 ? ascii+32 : ascii; + y[(i-1)*(numLabels+1)] = yi; + + float xi; + std::string n = i < 10 ? "0" + std::to_string(i) : std::to_string(i); + std::string filePath = "../data-set/" + n + "info.txt"; + std::ifstream dataFile(filePath); + int k = 0; + while (!dataFile.fail() && !dataFile.eof()) + { + dataFile >> xi; + X[sizeData*(i-1) + k++] = xi; + }; + } +} +int main(int argc, char* argv[]) { printf("\n"); printf("****************\n"); printf("** MLP TESTS **\n"); printf("****************\n"); - genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case - a[SIZE - 1] = 0; - printArray(SIZE, a, true); + unsigned int size_X = sizeData*numLabels; + unsigned int mem_size_X = sizeof(float) * size_X; + float *X = (float *)malloc(mem_size_X); + + unsigned int size_y = numLabels*numLabels; + unsigned int mem_size_y = sizeof(float) * size_y; + float *y = (float *)malloc(mem_size_y); - printDesc("train"); + printDesc("reading data"); + readData(X,y); - CharacterRecognition::train(); + printDesc("test multiply"); + CharacterRecognition::testMatrixMultiply(); - printDesc("test"); - + printDesc("training"); + CharacterRecognition::train(X, y, sizeData, 10, numLabels); + - //zeroArray(SIZE, c); - //printDesc("work-efficient compact, non-power-of-two"); - //count = StreamCompaction::Efficient::compact(NPOT, c, a); - //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - ////printArray(count, c, true); - //printCmpLenResult(count, expectedNPOT, b, c); + free(X); + free(y); system("pause"); // stop Win32 console from closing on exit - delete[] a; - delete[] b; - delete[] c; } From ab06dcb1cfb2708530af14a7b3b7fcbb83a1b71c Mon Sep 17 00:00:00 2001 From: klaywittler Date: Sun, 15 Sep 2019 14:52:22 -0400 Subject: [PATCH 07/20] readme --- Project2-Character-Recognition/README.md | 14 ++++++++------ Project2-Stream-Compaction/README.md | 14 ++++++++------ README.md | 13 +++++-------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index 4503fac..f252333 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -3,12 +3,14 @@ CUDA Character Recognition **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** -* (TODO) YOUR NAME HERE - * (TODO) [LinkedIn](), [personal website](), [twitter](), etc. -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +* Klayton Wittler + * [LinkedIn](https://www.linkedin.com/in/klayton-wittler/) +* Tested on: Windows 10 Pro, i7-7700K @ 4.20GHz 16.0GB, GTX 1070 8.192GB (my PC) -### (TODO: Your README) +## Sections -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +* [Introduction](#introduction) +* [Performance Analaysis](#performance-analysis) + * [Questions](#questions) +* [Addition Optimization](#additional-optimization) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 0e38ddb..3d5ef16 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -3,12 +3,14 @@ CUDA Stream Compaction **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** -* (TODO) YOUR NAME HERE - * (TODO) [LinkedIn](), [personal website](), [twitter](), etc. -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +* Klayton Wittler + * [LinkedIn](https://www.linkedin.com/in/klayton-wittler/) +* Tested on: Windows 10 Pro, i7-7700K @ 4.20GHz 16.0GB, GTX 1070 8.192GB (my PC) -### (TODO: Your README) +## Sections -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +* [Introduction](#introduction) +* [Performance Analaysis](#performance-analysis) + * [Questions](#questions) +* [Addition Optimization](#additional-optimization) diff --git a/README.md b/README.md index 3a0b2fe..3e17855 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,11 @@ CUDA Number Algorithms **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** -* (TODO) YOUR NAME HERE - * (TODO) [LinkedIn](), [personal website](), [twitter](), etc. -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +* Klayton Wittler + * [LinkedIn](https://www.linkedin.com/in/klayton-wittler/) +* Tested on: Windows 10 Pro, i7-7700K @ 4.20GHz 16.0GB, GTX 1070 8.192GB (my PC) -### (TODO: Your README) +## [Stream Compaction](/Project2-Stream-Compaction) -Link to the readmes of the other two subprojects. - -Add anything else you think is relevant up to this point. -(Remember, this is public, so don't put anything here that you don't want to share with the world.) +## [Character Recognition](/Project2-Character-Recognition) From daf641efb6d5550090c2b76693b6e7fe1e9de423 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Sun, 15 Sep 2019 20:49:36 -0400 Subject: [PATCH 08/20] debugging xor --- .../character_recognition/mlp.cu | 171 ++++++++++-------- Project2-Character-Recognition/src/main.cpp | 43 ++--- 2 files changed, 120 insertions(+), 94 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index e7d2f2d..618389a 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -5,7 +5,7 @@ #include /*! Block size used for CUDA kernel launch. */ -#define blockSize 32 +#define blockSize 128 #define index(i,j,ld) (((j)*(ld))+(i)) @@ -18,10 +18,8 @@ namespace CharacterRecognition { return timer; } - typedef struct _matrixSize // Optional Command-line multiplier for matrix sizes - { - unsigned int WA, HA, WB, HB, WC, HC; - _matrixSize(unsigned int WA=1, unsigned int HA=1, unsigned int WB=1, unsigned int HB=1, unsigned int WC=1, unsigned int HC=1){} + typedef struct _matrixSize { + int WA, HA, WB, HB, WC, HC; } sMatrixSize; void printMat(float*P, int uWP, int uHP) { @@ -33,35 +31,32 @@ namespace CharacterRecognition { } } - void randomInit(float *data, int size) - { + void randomInit(float *data, int size) { for (int i = 0; i < size; ++i) data[i] = rand() / (float)RAND_MAX; } - void indexInit(float *data, int size) - { - for (int i = 0; i < size; ++i) - data[i] = (float)i; + void fixedInit(float *data, int size) { + if (size == 4) { + data[0] = 10.1f; + data[1] = 0.9f; + data[2] = 20.0f; + data[3] = 0.87f; + } + else if (size == 2) { + data[0] = 41.0f; + data[1] = -54.0f; + } + printf("checking weight 0: %f \n", data[0]); + printf("checking weight 1: %f \n", data[1]); } - void initializeCUDA(sMatrixSize &matrix_size) - { - matrix_size.WA = 3; - matrix_size.HA = 4; - matrix_size.WB = 2; - matrix_size.HB = 3; - matrix_size.WC = 2; - matrix_size.HC = 4; - - printf("MatrixA(%u,%u), MatrixB(%u,%u), MatrixC(%u,%u)\n", - matrix_size.HA, matrix_size.WA, - matrix_size.HB, matrix_size.WB, - matrix_size.HC, matrix_size.WC); - + void indexInit(float *data, int size) { + for (int i = 0; i < size; ++i) + data[i] = (float)i; } - int getNum(int &n, float *v) { + int getNum(int &n, int *v) { // Generate a random number srand(time(NULL)); // Make sure the number is within the index range @@ -75,8 +70,8 @@ namespace CharacterRecognition { return num; } - void generateRandom(int n, float *perm) { - float *v = (float *)malloc(n); + void generateRandom(int n, int *permuteData) { + int *v = (int *)malloc(n); // Fill the vector with the values 1, 2, 3, ..., n for (int i = 0; i < n; i++) { v[i] = i; @@ -84,18 +79,50 @@ namespace CharacterRecognition { // While vector has elements get a random number from the vector and print it int i = 0; while (n > 0) { - perm[i] = getNum(n,v); + permuteData[i] = getNum(n,v); i++; } } + /*void deviceMemory(bool create = false, float *Xi = NULL, float *wI = NULL, float *wO = NULL, sMatrixSize &hidden_matrix_size = {}, sMatrixSize &output_matrix_size = {}, float *dev_X = NULL, float *dev_wI = NULL, float *dev_wO = NULL, float *dev_h1 = NULL, float *dev_pred = NULL) { + if (create) { + unsigned int size_X = hidden_matrix_size.WB * hidden_matrix_size.HB; + unsigned int mem_size_X = sizeof(float) * size_X; + unsigned int size_wI = hidden_matrix_size.WA * hidden_matrix_size.HA; + unsigned int mem_size_wI = sizeof(float) * size_wI; + unsigned int size_wO = output_matrix_size.WA * output_matrix_size.HA; + unsigned int mem_size_wO = sizeof(float) * size_wO; + unsigned int size_h1 = hidden_matrix_size.WC * hidden_matrix_size.HC; + unsigned int mem_size_h1 = sizeof(float) * size_h1; + unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; + unsigned int mem_size_pred = sizeof(float) * size_pred; + + cudaMalloc((void **)&dev_X, mem_size_X); + checkCUDAError("cudaMalloc dev_X"); + cudaMalloc((void **)&dev_wI, mem_size_wI); + checkCUDAError("cudaMalloc dev_wI"); + cudaMalloc((void **)&dev_wO, mem_size_wO); + checkCUDAError("cudaMalloc dev_wO"); + cudaMalloc((void **)&dev_h1, mem_size_h1); + checkCUDAError("cudaMalloc dev_h1"); + cudaMalloc((void **)&dev_pred, mem_size_pred); + checkCUDAError("cudaMalloc dev_pred"); + } + else { + cudaFree(dev_X); + cudaFree(dev_wI); + cudaFree(dev_wO); + cudaFree(dev_h1); + cudaFree(dev_pred); + } + }*/ + //////////////////////////////////////////////////////////////////////////////// //! Run a simple test matrix multiply using CUBLAS //////////////////////////////////////////////////////////////////////////////// void matrixMultiply(cublasHandle_t* handle, sMatrixSize &matrix_size, float *d_A, float *d_B, float *d_C){ const float alpha = 1.0f; const float beta = 0.0f; - cublasSgemm(*handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.WB, matrix_size.HA, matrix_size.WA, &alpha, d_B, matrix_size.WB, d_A, matrix_size.WA, &beta, d_C, matrix_size.WB); checkCUDAError("matrix multiply"); } @@ -113,6 +140,7 @@ namespace CharacterRecognition { float *forward(float *Xi, float *yi, float *wI, float *wO, sMatrixSize &hidden_matrix_size, sMatrixSize &output_matrix_size) { // allocate device memory float *dev_X, *dev_wI, *dev_wO, *dev_h1, *dev_pred; + //deviceMemory(true, Xi, wI, wO, hidden_matrix_size, output_matrix_size, dev_X, dev_wI, dev_wO, dev_h1, dev_pred); unsigned int size_X = hidden_matrix_size.WB * hidden_matrix_size.HB; unsigned int mem_size_X = sizeof(float) * size_X; unsigned int size_wI = hidden_matrix_size.WA * hidden_matrix_size.HA; @@ -124,43 +152,38 @@ namespace CharacterRecognition { unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; unsigned int mem_size_pred = sizeof(float) * size_pred; - // allocate host memory for the result - float *pred = (float *)malloc(mem_size_pred); - cudaMalloc((void **)&dev_X, mem_size_X); checkCUDAError("cudaMalloc dev_X"); cudaMalloc((void **)&dev_wI, mem_size_wI); checkCUDAError("cudaMalloc dev_wI"); cudaMalloc((void **)&dev_wO, mem_size_wO); checkCUDAError("cudaMalloc dev_wO"); - cudaMemcpy(dev_X, Xi, mem_size_X, cudaMemcpyHostToDevice); - checkCUDAError("cudaMemcpy dev_X"); - cudaMemcpy(dev_wI, wI, mem_size_wI, cudaMemcpyHostToDevice); - checkCUDAError("cudaMemcpy dev_wI"); - cudaMemcpy(dev_wO, wO, mem_size_wO, cudaMemcpyHostToDevice); - checkCUDAError("cudaMemcpy dev_wO"); cudaMalloc((void **)&dev_h1, mem_size_h1); checkCUDAError("cudaMalloc dev_h1"); cudaMalloc((void **)&dev_pred, mem_size_pred); checkCUDAError("cudaMalloc dev_pred"); - - dim3 threads(blockSize, blockSize); - dim3 grid(hidden_matrix_size.WC / threads.x, hidden_matrix_size.HC / threads.y); + // allocate host memory for result + //unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; + //unsigned int mem_size_pred = sizeof(float) * size_pred; + float *pred = (float *)malloc(mem_size_pred); cublasHandle_t handle; cublasCreate(&handle); //hidden layer + dim3 threads(blockSize); + dim3 grid((hidden_matrix_size.WC*hidden_matrix_size.HC + blockSize - 1) / blockSize); matrixMultiply(&handle, hidden_matrix_size, dev_wI, dev_X, dev_h1); - kernSigmoid <<> > (hidden_matrix_size.HC, dev_h1); + kernSigmoid <<> > (hidden_matrix_size.HC*hidden_matrix_size.WC, dev_h1); checkCUDAError("kernSigmoid"); - dim3 grid1(output_matrix_size.WC / threads.x, output_matrix_size.HC / threads.y); + //dim3 grid1(output_matrix_size.WC / threads.x, output_matrix_size.HC / threads.y); + dim3 grid1((output_matrix_size.WC*output_matrix_size.HC + blockSize - 1) / blockSize); //output layer matrixMultiply(&handle, output_matrix_size, dev_wO, dev_h1, dev_pred); - kernSigmoid << > > (output_matrix_size.HC, dev_pred); + kernSigmoid << > > (output_matrix_size.HC*output_matrix_size.WC, dev_pred); checkCUDAError("kernSigmoid"); cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); @@ -169,6 +192,7 @@ namespace CharacterRecognition { cublasDestroy(handle); checkCUDAError("handle"); + //deviceMemory(); cudaFree(dev_X); cudaFree(dev_wI); cudaFree(dev_wO); @@ -179,53 +203,58 @@ namespace CharacterRecognition { } void train(float *X, float *y, int sizeData, const int hiddenNodes, const int numLabels, const int numData) { - sMatrixSize hidden_matrix_size(1, sizeData, hiddenNodes, sizeData, 1, hiddenNodes); - sMatrixSize output_matrix_size(1, hiddenNodes, numLabels, hiddenNodes, 1, numLabels); + sMatrixSize hidden_matrix_size = {hiddenNodes, sizeData, 1, sizeData, 1, hiddenNodes }; + sMatrixSize output_matrix_size = {numLabels, hiddenNodes, 1, hiddenNodes, 1, numLabels }; - srand(2006); - - unsigned int size_wI = sizeData * hiddenNodes; + unsigned int size_wI = hidden_matrix_size.WA * hidden_matrix_size.WA; unsigned int mem_size_wI = sizeof(float) * size_wI; float *wI = (float *)malloc(mem_size_wI); - unsigned int size_wO = numLabels * hiddenNodes; + unsigned int size_wO = output_matrix_size.HA * output_matrix_size.WA; unsigned int mem_size_wO = sizeof(float) * size_wO; float *wO = (float *)malloc(mem_size_wO); - randomInit(wI, size_wI); - randomInit(wO, size_wO); + fixedInit(wI, size_wI); + fixedInit(wO, size_wO); - float *perm = (float *)malloc(numData); + int *permuteData = (int *)malloc(numData); float *Xi = (float *)malloc(sizeData); float *yi = (float *)malloc(numLabels); unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; unsigned int mem_size_pred = sizeof(float) * size_pred; float *pred = (float *)malloc(mem_size_pred); - for (int iter = 0; iter < 1000; iter++) { - generateRandom(numData, perm); - printf("here"); + + for (int iter = 0; iter < 1; iter++) { + generateRandom(numData, permuteData); + printf("predicting iteration %i \n", iter); for (int i = 0; i < numData; i++) { - int index = perm[i]; - printf("%i \n", index); - memcpy(Xi, (void **)&X[sizeData*i], sizeData * sizeof(float)); - memcpy(yi, (void **)&y[numLabels*i], numLabels * sizeof(float)); + int index = permuteData[i]; + memcpy(Xi, (void **)&X[sizeData*index], sizeData * sizeof(float)); + memcpy(yi, (void **)&y[numLabels*index], numLabels * sizeof(float)); + + printf("index %i \n", index); + printf("data: %f %f label: %f \n", Xi[0] , Xi[1], yi[0]); + pred = forward(Xi, yi, wI, wO, hidden_matrix_size, output_matrix_size); for (int j = 0; j < numLabels; j++) { - printf("%f \n", pred[j]); + printf("prediction: %f \n", pred[j]); } } + printf("forward done \n"); } + printf("predictions done \n"); + free(wI); + free(wO); free(Xi); free(yi); - free(perm); + free(permuteData); + free(pred); } void testMatrixMultiply() { - sMatrixSize matrix_size; - - initializeCUDA(matrix_size); + sMatrixSize matrix_size = { 3, 4, 2, 3, 2, 4}; // allocate host memory for matrices A and B unsigned int size_A = matrix_size.WA * matrix_size.HA; @@ -261,7 +290,7 @@ namespace CharacterRecognition { dim3 grid(matrix_size.WC / threads.x, matrix_size.HC / threads.y); // create and start timer - printf("Computing result using CUBLAS..."); + printf("Computing result using CUBLAS... \n"); cublasHandle_t handle; cublasCreate(&handle); @@ -276,12 +305,13 @@ namespace CharacterRecognition { // Destroy the handle cublasDestroy(handle); - printf("\nMatriz A:\n"); + printf("\n\n Matriz A:"); printMat(h_A, matrix_size.WA, matrix_size.HA); - printf("\nMatriz B:\n"); + printf("\n\n Matriz B:"); printMat(h_B, matrix_size.WB, matrix_size.HB); - printf("\nMatriz C:\n"); + printf("\n\n Matriz C:"); printMat(h_C, matrix_size.WC, matrix_size.HC); + printf("\n\n"); // clean up memory free(h_A); @@ -292,5 +322,4 @@ namespace CharacterRecognition { cudaFree(d_C); } - } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index a545785..e402872 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -12,27 +12,24 @@ #include "testing_helpers.hpp" #include -#define sizeData 10205 -#define numLabels 52 +#define sizeData 2 +#define numLabels 1 +#define numData 4 +#define hiddenNodes 2 void readData(float *X, float *y) { - int c = 0; - for (int i = 1; i <= numLabels; i++) { - float ascii = 65 + c; - if (i % 2 == 0) { c++; } - float yi = i % 2 == 0 ? ascii+32 : ascii; - y[(i-1)*(numLabels+1)] = yi; + for (int i = 0; i < sizeData; i++) { + for (int j = 0; j < sizeData; j++) { + X[sizeData*(2 * i + j)] = i; + X[sizeData*(2 * i + j) + 1] = j; + y[2*i + j] = i ^ j; + } + } - float xi; - std::string n = i < 10 ? "0" + std::to_string(i) : std::to_string(i); - std::string filePath = "../data-set/" + n + "info.txt"; - std::ifstream dataFile(filePath); - int k = 0; - while (!dataFile.fail() && !dataFile.eof()) - { - dataFile >> xi; - X[sizeData*(i-1) + k++] = xi; - }; + for (int i = 0; i <= 1; i++) { + for (int j = 0; j <= 1; j++) { + std::cout << "data: " << X[sizeData*(2 * i + j)] << X[sizeData*(2 * i + j) + 1] << " label: " << y[2 * i + j] << "\n"; + } } } @@ -42,22 +39,22 @@ int main(int argc, char* argv[]) { printf("** MLP TESTS **\n"); printf("****************\n"); - unsigned int size_X = sizeData*numLabels; + unsigned int size_X = sizeData* numData; unsigned int mem_size_X = sizeof(float) * size_X; float *X = (float *)malloc(mem_size_X); - unsigned int size_y = numLabels*numLabels; + unsigned int size_y = numLabels*numData; unsigned int mem_size_y = sizeof(float) * size_y; float *y = (float *)malloc(mem_size_y); printDesc("reading data"); readData(X,y); - printDesc("test multiply"); - CharacterRecognition::testMatrixMultiply(); + //printDesc("test multiply"); + //CharacterRecognition::testMatrixMultiply(); printDesc("training"); - CharacterRecognition::train(X, y, sizeData, 10, numLabels); + CharacterRecognition::train(X, y, sizeData, hiddenNodes, numLabels, numData); free(X); From bbbf620cf9a36da77f35a474f07148ad955fd402 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Mon, 16 Sep 2019 00:07:45 -0400 Subject: [PATCH 09/20] working on matrix multiply : issues seem to be there --- .../character_recognition/mlp.cu | 161 ++++++++++++++---- Project2-Character-Recognition/src/main.cpp | 8 +- 2 files changed, 130 insertions(+), 39 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index 618389a..d8f9e47 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -1,11 +1,15 @@ + + #include #include +//#include +//#include "device_launch_parameters.h" #include "common.h" #include "mlp.h" #include /*! Block size used for CUDA kernel launch. */ -#define blockSize 128 +#define blockSize 32 #define index(i,j,ld) (((j)*(ld))+(i)) @@ -47,8 +51,6 @@ namespace CharacterRecognition { data[0] = 41.0f; data[1] = -54.0f; } - printf("checking weight 0: %f \n", data[0]); - printf("checking weight 1: %f \n", data[1]); } void indexInit(float *data, int size) { @@ -56,7 +58,7 @@ namespace CharacterRecognition { data[i] = (float)i; } - int getNum(int &n, int *v) { + int getNum(int &n, float *v) { // Generate a random number srand(time(NULL)); // Make sure the number is within the index range @@ -70,8 +72,8 @@ namespace CharacterRecognition { return num; } - void generateRandom(int n, int *permuteData) { - int *v = (int *)malloc(n); + void generateRandom(int n, float *permuteData) { + float *v = (float *)malloc(n); // Fill the vector with the values 1, 2, 3, ..., n for (int i = 0; i < n; i++) { v[i] = i; @@ -117,9 +119,72 @@ namespace CharacterRecognition { } }*/ - //////////////////////////////////////////////////////////////////////////////// - //! Run a simple test matrix multiply using CUBLAS - //////////////////////////////////////////////////////////////////////////////// + template __global__ void MatrixMulCUDA(float *C, float *A, float *B, int wA, int wB) { + // Block index + int bx = blockIdx.x; + int by = blockIdx.y; + + // Thread index + int tx = threadIdx.x; + int ty = threadIdx.y; + + // Index of the first sub-matrix of A processed by the block + int aBegin = wA * BLOCK_SIZE * by; + + // Index of the last sub-matrix of A processed by the block + int aEnd = aBegin + wA - 1; + + // Step size used to iterate through the sub-matrices of A + int aStep = BLOCK_SIZE; + + // Index of the first sub-matrix of B processed by the block + int bBegin = BLOCK_SIZE * bx; + + // Step size used to iterate through the sub-matrices of B + int bStep = BLOCK_SIZE * wB; + + // Csub is used to store the element of the block sub-matrix + // that is computed by the thread + float Csub = 0; + + // Loop over all the sub-matrices of A and B + // required to compute the block sub-matrix + for (int a = aBegin, b = bBegin; + a <= aEnd; + a += aStep, b += bStep) { + // Declaration of the shared memory array As used to + // store the sub-matrix of A + __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; + + // Declaration of the shared memory array Bs used to + // store the sub-matrix of B + __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; + + // Load the matrices from device memory + // to shared memory; each thread loads + // one element of each matrix + As[ty][tx] = A[a + wA * ty + tx]; + Bs[ty][tx] = B[b + wB * ty + tx]; + + // Synchronize to make sure the matrices are loaded + __syncthreads(); + + // Multiply the two matrices together; + // each thread computes one element + // of the block sub-matrix + #pragma unroll + + for (int k = 0; k < BLOCK_SIZE; ++k) { + Csub += As[ty][k] * Bs[k][tx]; + } + + // Synchronize to make sure that the preceding + // computation is done before loading two new + // sub-matrices of A and B in the next iteration + __syncthreads(); + } + } + void matrixMultiply(cublasHandle_t* handle, sMatrixSize &matrix_size, float *d_A, float *d_B, float *d_C){ const float alpha = 1.0f; const float beta = 0.0f; @@ -135,22 +200,31 @@ namespace CharacterRecognition { } input[index] = 1.0f / (1 + exp(-input[index])); } + + __global__ void kernSigmoid2(int n, float *input) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + input[index] = 1.0f / (1 + exp(-input[index])); + } + void backward(){} - float *forward(float *Xi, float *yi, float *wI, float *wO, sMatrixSize &hidden_matrix_size, sMatrixSize &output_matrix_size) { + void forward(float *pred, float *Xi, float *wI, float *wO, sMatrixSize &hidden_matrix_size, sMatrixSize &output_matrix_size) { // allocate device memory float *dev_X, *dev_wI, *dev_wO, *dev_h1, *dev_pred; //deviceMemory(true, Xi, wI, wO, hidden_matrix_size, output_matrix_size, dev_X, dev_wI, dev_wO, dev_h1, dev_pred); - unsigned int size_X = hidden_matrix_size.WB * hidden_matrix_size.HB; - unsigned int mem_size_X = sizeof(float) * size_X; - unsigned int size_wI = hidden_matrix_size.WA * hidden_matrix_size.HA; - unsigned int mem_size_wI = sizeof(float) * size_wI; - unsigned int size_wO = output_matrix_size.WA * output_matrix_size.HA; - unsigned int mem_size_wO = sizeof(float) * size_wO; - unsigned int size_h1 = hidden_matrix_size.WC * hidden_matrix_size.HC; - unsigned int mem_size_h1 = sizeof(float) * size_h1; - unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; - unsigned int mem_size_pred = sizeof(float) * size_pred; + int size_X = hidden_matrix_size.WB * hidden_matrix_size.HB; + int mem_size_X = sizeof(float) * size_X; + int size_wI = hidden_matrix_size.WA * hidden_matrix_size.HA; + int mem_size_wI = sizeof(float) * size_wI; + int size_wO = output_matrix_size.WA * output_matrix_size.HA; + int mem_size_wO = sizeof(float) * size_wO; + int size_h1 = hidden_matrix_size.WC * hidden_matrix_size.HC; + int mem_size_h1 = sizeof(float) * size_h1; + int size_pred = output_matrix_size.WC * output_matrix_size.HC; + int mem_size_pred = sizeof(float) * size_pred; cudaMalloc((void **)&dev_X, mem_size_X); checkCUDAError("cudaMalloc dev_X"); @@ -162,10 +236,17 @@ namespace CharacterRecognition { checkCUDAError("cudaMalloc dev_h1"); cudaMalloc((void **)&dev_pred, mem_size_pred); checkCUDAError("cudaMalloc dev_pred"); + + cudaMemcpy(dev_X, Xi, mem_size_X, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_X"); + cudaMemcpy(dev_wI, wI, mem_size_wI, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_wI"); + cudaMemcpy(dev_wO, wO, mem_size_wO, cudaMemcpyHostToDevice); + checkCUDAError("cudaMemcpy dev_wO"); // allocate host memory for result //unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; //unsigned int mem_size_pred = sizeof(float) * size_pred; - float *pred = (float *)malloc(mem_size_pred); + float *h1 = (float *)malloc(mem_size_h1); cublasHandle_t handle; cublasCreate(&handle); @@ -175,6 +256,11 @@ namespace CharacterRecognition { dim3 grid((hidden_matrix_size.WC*hidden_matrix_size.HC + blockSize - 1) / blockSize); matrixMultiply(&handle, hidden_matrix_size, dev_wI, dev_X, dev_h1); + cudaMemcpy(h1, dev_h1, mem_size_pred, cudaMemcpyDeviceToHost); + checkCUDAError("cudaMemcpy pred"); + printf("\n\n Matriz h1:"); + printf("\n %f %f", h1[0], h1[1]); + kernSigmoid <<> > (hidden_matrix_size.HC*hidden_matrix_size.WC, dev_h1); checkCUDAError("kernSigmoid"); @@ -183,7 +269,13 @@ namespace CharacterRecognition { dim3 grid1((output_matrix_size.WC*output_matrix_size.HC + blockSize - 1) / blockSize); //output layer matrixMultiply(&handle, output_matrix_size, dev_wO, dev_h1, dev_pred); - kernSigmoid << > > (output_matrix_size.HC*output_matrix_size.WC, dev_pred); + cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); + checkCUDAError("cudaMemcpy pred"); + printf("\n\n Matriz pred:"); + printf("\n %f", pred[0]); + printf("\n"); + + kernSigmoid2 << > > (output_matrix_size.HC*output_matrix_size.WC, dev_pred); checkCUDAError("kernSigmoid"); cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); @@ -198,8 +290,6 @@ namespace CharacterRecognition { cudaFree(dev_wO); cudaFree(dev_h1); cudaFree(dev_pred); - - return pred; } void train(float *X, float *y, int sizeData, const int hiddenNodes, const int numLabels, const int numData) { @@ -217,7 +307,7 @@ namespace CharacterRecognition { fixedInit(wI, size_wI); fixedInit(wO, size_wO); - int *permuteData = (int *)malloc(numData); + float *permuteData = (float *)malloc(numData); float *Xi = (float *)malloc(sizeData); float *yi = (float *)malloc(numLabels); @@ -236,7 +326,7 @@ namespace CharacterRecognition { printf("index %i \n", index); printf("data: %f %f label: %f \n", Xi[0] , Xi[1], yi[0]); - pred = forward(Xi, yi, wI, wO, hidden_matrix_size, output_matrix_size); + forward(pred, Xi, wI, wO, hidden_matrix_size, output_matrix_size); for (int j = 0; j < numLabels; j++) { printf("prediction: %f \n", pred[j]); } @@ -245,16 +335,16 @@ namespace CharacterRecognition { } printf("predictions done \n"); - free(wI); - free(wO); - free(Xi); - free(yi); - free(permuteData); - free(pred); + //free(wI); + //free(wO); + //free(Xi); + //free(yi); + //free(permuteData); + //free(pred); } void testMatrixMultiply() { - sMatrixSize matrix_size = { 3, 4, 2, 3, 2, 4}; + sMatrixSize matrix_size = { 3, 4, 3, 2, 2, 4}; // allocate host memory for matrices A and B unsigned int size_A = matrix_size.WA * matrix_size.HA; @@ -287,7 +377,7 @@ namespace CharacterRecognition { // setup execution parameters dim3 threads(blockSize, blockSize); - dim3 grid(matrix_size.WC / threads.x, matrix_size.HC / threads.y); + dim3 grid(matrix_size.HB / threads.x, matrix_size.WA / threads.y); // create and start timer printf("Computing result using CUBLAS... \n"); @@ -296,7 +386,8 @@ namespace CharacterRecognition { cublasCreate(&handle); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - matrixMultiply(&handle, matrix_size, d_A, d_B, d_C); + //matrixMultiply(&handle, matrix_size, d_A, d_B, d_C); + MatrixMulCUDA << < grid, threads >> > (d_C, d_A, d_B, matrix_size.HA, matrix_size.HB); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // copy result from device to host diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index e402872..d8c0303 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -50,11 +50,11 @@ int main(int argc, char* argv[]) { printDesc("reading data"); readData(X,y); - //printDesc("test multiply"); - //CharacterRecognition::testMatrixMultiply(); + printDesc("test multiply"); + CharacterRecognition::testMatrixMultiply(); - printDesc("training"); - CharacterRecognition::train(X, y, sizeData, hiddenNodes, numLabels, numData); + //printDesc("training"); + //CharacterRecognition::train(X, y, sizeData, hiddenNodes, numLabels, numData); free(X); From 7d5d8c9a25adfa6fce39dafd60bfe376bbe7dcb9 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Mon, 16 Sep 2019 13:17:42 -0400 Subject: [PATCH 10/20] radix sort --- Project2-Stream-Compaction/src/main.cpp | 28 +++++ .../stream_compaction/CMakeLists.txt | 2 + .../stream_compaction/radix.cu | 114 ++++++++++++++++++ .../stream_compaction/radix.h | 11 ++ 4 files changed, 155 insertions(+) create mode 100644 Project2-Stream-Compaction/stream_compaction/radix.cu create mode 100644 Project2-Stream-Compaction/stream_compaction/radix.h diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index a0493db..c4fdb30 100644 --- a/Project2-Stream-Compaction/src/main.cpp +++ b/Project2-Stream-Compaction/src/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "testing_helpers.hpp" const int SIZE = 1 << 8; //1 << 8; // feel free to change the size of array @@ -147,6 +148,33 @@ int main(int argc, char* argv[]) { //printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); + + printf("\n"); + printf("**********************\n"); + printf("** RADIX SORT TESTS **\n"); + printf("**********************\n"); + + int unsorted_array[] = { 0, 4, 1, 2, 5, 6, 7, 3 }; + int sorted_array[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + int unsorted_arrayNPOT[] = { 0, 4, 1, 2, 5, 6, 3 }; + int sorted_arrayNPOT[] = { 0, 1, 2, 3, 4, 5, 6 }; + printArray(8, unsorted_array, true); + printArray(8, sorted_array, true); + + zeroArray(8, c); + printDesc("radix sort, power-of-two"); + StreamCompaction::Radix::sort(8, c, unsorted_array); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(8, c, true); + printCmpResult(8, sorted_array, c); + + zeroArray(7, c); + printDesc("radix sort, non-power-of-two"); + StreamCompaction::Radix::sort(7, c, unsorted_arrayNPOT); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(7, c, true); + printCmpResult(7, sorted_arrayNPOT, c); + system("pause"); // stop Win32 console from closing on exit delete[] a; delete[] b; diff --git a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt index 4bb0dc2..fc37515 100644 --- a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt +++ b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt @@ -9,6 +9,8 @@ set(SOURCE_FILES "efficient.cu" "thrust.h" "thrust.cu" + "radix.h" + "radix.cu" ) cuda_add_library(stream_compaction diff --git a/Project2-Stream-Compaction/stream_compaction/radix.cu b/Project2-Stream-Compaction/stream_compaction/radix.cu new file mode 100644 index 0000000..085f6d8 --- /dev/null +++ b/Project2-Stream-Compaction/stream_compaction/radix.cu @@ -0,0 +1,114 @@ +#include +#include +#include "common.h" +#include "naive.h" +#include "efficient.h" +#include +#include "radix.h" + +/*! Block size used for CUDA kernel launch. */ +#define blockSize 128 + +namespace StreamCompaction { + namespace Radix { + using StreamCompaction::Common::PerformanceTimer; + PerformanceTimer& timer() + { + static PerformanceTimer timer; + return timer; + } + + __global__ void kernFullAddress(int n, int totalFalses, int *indices, int *bools){ + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + indices[index] = bools[index] ? indices[index] : index - indices[index] + totalFalses; + } + + __global__ void kernBitMapToBoolean(int n, int bit, int *bools, const int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + bools[index] = idata[index] & bit ? 0 : 1; + } + + __global__ void kernFullScatter(int n, int *odata, const int *idata, const int *indices) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + odata[indices[index]] = idata[index]; + + } + + int findMax(int n, const int *idata) { + int mx = idata[0]; + for (int i = 1; i < n; i++) { + if (idata[i] > mx) { + mx = idata[i]; + } + } + return mx; + } + + void sort(int n, int *odata, const int *idata) { + int mx = findMax(n, idata); + int kbit = ilog2ceil(mx); + int maxBinary = 1 << kbit; + + int *dev_idata, *dev_odata, *dev_bools, *dev_indices; + + cudaMalloc((void**)&dev_idata, sizeof(int) * maxBinary); + checkCUDAError("cudaMalloc dev_idata failed!"); + cudaMalloc((void**)&dev_odata, sizeof(int) * maxBinary); + checkCUDAError("cudaMalloc dev_odata failed!"); + cudaMalloc((void**)&dev_bools, sizeof(int) * maxBinary); + checkCUDAError("cudaMalloc dev_bools failed!"); + cudaMalloc((void**)&dev_indices, sizeof(int) * maxBinary); + checkCUDAError("cudaMalloc dev_indices failed!"); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy idata failed!"); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((maxBinary + blockSize - 1) / blockSize); + + //timer().startGpuTimer(); + for(int i = 0; i <= kbit; i++){ + kernBitMapToBoolean<< > > (n, (1 << i), dev_bools, dev_idata); + checkCUDAError("Memcpy kernMapToBoolean failed!"); + + StreamCompaction::Naive::scan(n, dev_indices, dev_bools); + checkCUDAError("Naive scan failed!"); + + int totalFalses, lastBool; + cudaMemcpy(&totalFalses, dev_indices + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy totalFalses failed!"); + cudaMemcpy(&lastBool, dev_bools + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy lastBool failed!"); + + totalFalses += lastBool; + kernFullAddress << > > (n, totalFalses, dev_indices, dev_bools); + checkCUDAError("kernFullAddress failed!"); + + kernFullScatter << > > (n, dev_odata, dev_idata, dev_indices); + checkCUDAError("kernScatter failed!"); + + cudaMemcpy(dev_idata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToDevice); + checkCUDAError("Memcpy dev_odata to dev_idata failed!"); + } + + //timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, sizeof(int) * n, cudaMemcpyDeviceToHost); + + + cudaFree(dev_idata); + cudaFree(dev_odata); + cudaFree(dev_bools); + cudaFree(dev_indices); + } + } +} diff --git a/Project2-Stream-Compaction/stream_compaction/radix.h b/Project2-Stream-Compaction/stream_compaction/radix.h new file mode 100644 index 0000000..aa489d9 --- /dev/null +++ b/Project2-Stream-Compaction/stream_compaction/radix.h @@ -0,0 +1,11 @@ +#pragma once + +#include "common.h" + +namespace StreamCompaction { + namespace Radix { + StreamCompaction::Common::PerformanceTimer& timer(); + + void sort(int n, int *odata, const int *idata); + } +} From 47e53ecb667ccf34fbbbcffba88ab130f952f2ad Mon Sep 17 00:00:00 2001 From: klaywittler Date: Mon, 16 Sep 2019 15:50:20 -0400 Subject: [PATCH 11/20] radix with work efficient scan and added flag to trigger class example or run on full test array --- Project2-Stream-Compaction/src/main.cpp | 49 +++++++++++++------ .../stream_compaction/efficient.h | 2 + .../stream_compaction/radix.cu | 27 +++++----- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index c4fdb30..6a14b61 100644 --- a/Project2-Stream-Compaction/src/main.cpp +++ b/Project2-Stream-Compaction/src/main.cpp @@ -20,6 +20,8 @@ int *a = new int[SIZE]; int *b = new int[SIZE]; int *c = new int[SIZE]; +#define RADIX_CLASS_EXAMPLE 1; + int main(int argc, char* argv[]) { // Scan tests @@ -154,26 +156,43 @@ int main(int argc, char* argv[]) { printf("** RADIX SORT TESTS **\n"); printf("**********************\n"); - int unsorted_array[] = { 0, 4, 1, 2, 5, 6, 7, 3 }; - int sorted_array[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - int unsorted_arrayNPOT[] = { 0, 4, 1, 2, 5, 6, 3 }; - int sorted_arrayNPOT[] = { 0, 1, 2, 3, 4, 5, 6 }; - printArray(8, unsorted_array, true); - printArray(8, sorted_array, true); - - zeroArray(8, c); + #if RADIX_CLASS_EXAMPLE + const int size = 8; + const int npot = 7; + int unsorted_array[] = { 0, 4, 1, 2, 5, 6, 7, 3 }; //hard coding array to class example + int unsorted_arrayNPOT[] = { 0, 4, 1, 2, 5, 6, 3 }; + #else + const int size = SIZE; + const int npot = NPOT; + int unsorted_array[size]; + int unsorted_arrayNPOT[npot]; + memcpy(unsorted_array, a, sizeof(int) * size); + memcpy(unsorted_arrayNPOT, a, sizeof(int) * NPOT); + #endif // CLASS_EXAMPLE + + int sorted_array[size]; + memcpy(sorted_array, unsorted_array, sizeof(int) * size); // copy and sort to compare + std::sort(sorted_array, sorted_array + size); + + int sorted_arrayNPOT[npot]; + memcpy(sorted_arrayNPOT, unsorted_arrayNPOT, sizeof(int) * npot); // copy and sort to compare + std::sort(sorted_arrayNPOT, sorted_arrayNPOT + npot); + + printArray(size, unsorted_array, true); + + zeroArray(size, c); printDesc("radix sort, power-of-two"); - StreamCompaction::Radix::sort(8, c, unsorted_array); + StreamCompaction::Radix::sort(size, c, unsorted_array); printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - printArray(8, c, true); - printCmpResult(8, sorted_array, c); + printArray(size, c, true); + printCmpResult(size, sorted_array, c); - zeroArray(7, c); + zeroArray(npot, c); printDesc("radix sort, non-power-of-two"); - StreamCompaction::Radix::sort(7, c, unsorted_arrayNPOT); + StreamCompaction::Radix::sort(npot, c, unsorted_arrayNPOT); printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - printArray(7, c, true); - printCmpResult(7, sorted_arrayNPOT, c); + printArray(npot, c, true); + printCmpResult(npot, sorted_arrayNPOT, c); system("pause"); // stop Win32 console from closing on exit delete[] a; diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.h b/Project2-Stream-Compaction/stream_compaction/efficient.h index 803cb4f..1450345 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.h +++ b/Project2-Stream-Compaction/stream_compaction/efficient.h @@ -6,6 +6,8 @@ namespace StreamCompaction { namespace Efficient { StreamCompaction::Common::PerformanceTimer& timer(); + void workEfficientScan(int n, int *dev_idata, dim3 &threadsPerBlock, dim3 &fullBlocksPerGrid); + void scan(int n, int *odata, const int *idata); int compact(int n, int *odata, const int *idata); diff --git a/Project2-Stream-Compaction/stream_compaction/radix.cu b/Project2-Stream-Compaction/stream_compaction/radix.cu index 085f6d8..1f48d4d 100644 --- a/Project2-Stream-Compaction/stream_compaction/radix.cu +++ b/Project2-Stream-Compaction/stream_compaction/radix.cu @@ -54,34 +54,35 @@ namespace StreamCompaction { } void sort(int n, int *odata, const int *idata) { - int mx = findMax(n, idata); - int kbit = ilog2ceil(mx); - int maxBinary = 1 << kbit; + //int mx = findMax(n, idata); + int kbit = ilog2ceil(n); + int npad = 1 << kbit; int *dev_idata, *dev_odata, *dev_bools, *dev_indices; - - cudaMalloc((void**)&dev_idata, sizeof(int) * maxBinary); + cudaMalloc((void**)&dev_idata, sizeof(int) * npad); checkCUDAError("cudaMalloc dev_idata failed!"); - cudaMalloc((void**)&dev_odata, sizeof(int) * maxBinary); + cudaMalloc((void**)&dev_odata, sizeof(int) * npad); checkCUDAError("cudaMalloc dev_odata failed!"); - cudaMalloc((void**)&dev_bools, sizeof(int) * maxBinary); + cudaMalloc((void**)&dev_bools, sizeof(int) * npad); checkCUDAError("cudaMalloc dev_bools failed!"); - cudaMalloc((void**)&dev_indices, sizeof(int) * maxBinary); + cudaMalloc((void**)&dev_indices, sizeof(int) * npad); checkCUDAError("cudaMalloc dev_indices failed!"); cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); checkCUDAError("Memcpy idata failed!"); dim3 threadsPerBlock(blockSize); - dim3 fullBlocksPerGrid((maxBinary + blockSize - 1) / blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); - //timer().startGpuTimer(); + timer().startGpuTimer(); for(int i = 0; i <= kbit; i++){ kernBitMapToBoolean<< > > (n, (1 << i), dev_bools, dev_idata); checkCUDAError("Memcpy kernMapToBoolean failed!"); - StreamCompaction::Naive::scan(n, dev_indices, dev_bools); - checkCUDAError("Naive scan failed!"); + cudaMemcpy(dev_indices, dev_bools, sizeof(int) * n, cudaMemcpyDeviceToDevice); + checkCUDAError("Memcpy dev_bools to dev_indices failed!"); + StreamCompaction::Efficient::workEfficientScan(npad, dev_indices, threadsPerBlock, fullBlocksPerGrid); + checkCUDAError("Efficient scan failed!"); int totalFalses, lastBool; cudaMemcpy(&totalFalses, dev_indices + n - 1, sizeof(int), cudaMemcpyDeviceToHost); @@ -100,7 +101,7 @@ namespace StreamCompaction { checkCUDAError("Memcpy dev_odata to dev_idata failed!"); } - //timer().endGpuTimer(); + timer().endGpuTimer(); cudaMemcpy(odata, dev_idata, sizeof(int) * n, cudaMemcpyDeviceToHost); From 035a7d1980798c576202191abf72076556fa5411 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Mon, 16 Sep 2019 15:57:15 -0400 Subject: [PATCH 12/20] radix with work efficient scan and added flag to trigger class example or run on full test array --- .../2x2_XOR_excel_example.xlsx | Bin 14914 -> 14944 bytes Project2-Stream-Compaction/src/main.cpp | 47 +++++++ .../stream_compaction/CMakeLists.txt | 2 + .../stream_compaction/efficient.h | 2 + .../stream_compaction/radix.cu | 115 ++++++++++++++++++ .../stream_compaction/radix.h | 11 ++ 6 files changed, 177 insertions(+) create mode 100644 Project2-Stream-Compaction/stream_compaction/radix.cu create mode 100644 Project2-Stream-Compaction/stream_compaction/radix.h diff --git a/Project2-Character-Recognition/2x2_XOR_excel_example.xlsx b/Project2-Character-Recognition/2x2_XOR_excel_example.xlsx index d06377716852b8e845d565711b72d10e6ad41ff1..c30c9651a66cddc2118187381b10528f8793a3db 100644 GIT binary patch delta 5888 zcmY*dbx;%z)25rFJEb|`XgH7#iKCH_)H2=Z zcfR@N{r351XP()a-F;?vX1CXS&AWaM2cIJ)KdKND4ebSd4TlD1Y?+JzQ@*#ZctSrN z`+%Q5-UT7oH-4Cv3vCBvrrLh^q+`k${SALOPiHIteXqP1!s znjL_AEmfTs-}t~drGNA^kTvwvM>TjFar1(Vv@6^g=B~becNN~eM1Pjy-b0EYW*4I= zcITbLCDmZ44*gd7s!Yd0Ab`*!ej9nh$hHVzovnU-`x>peCZjJtjxctPxV2NQpgHqT z;##zf!~K+wP2d-!;TTFym%mgB9k`Q%_iB0Z6L8W)>wz_xt9mHkzO1N}Ok>*5=dV=l ztM@RXzW#cl%vz(!CyaHH>#&}UaQ|iDZmN6&m~4JG_kN&e=q1#g*f`KiM0>c88Krl) z_m-_mV8PFEoWB0n;kT~q)eiq|Nz+RKU&3#>9-zOh)qOa>Tj$s?bJZ)S)5um(;lkY= ztm&_cDz;3gU|R2pf5-&hb^sac8_hoK)R^D8k4I@MdaTJdW%c>jWSGtw3nKc)HfBt) z(OdG@DL>kC(*L$EqtLB1&r7vl{3j zTHUg{8RYsNc22Gr>JQg%D;~lgg@vDaKa0YZCmA2#gyA3mxGp{|$)J`IJkw3t6})C{ z0N?|YqkAzX<6iudN)^6+gi0_aqgd5*7i0)>S(9JrNGGz{r`ajyB&SGf56bt@(etB+Esz(?z<5T1F0u!YqxST6Hb(M`awCrBn2`1%H_wMV}GinlJsAnuXvfUg|s9GN6ir;h!QVEc+f_ z*%-4{oJszJ6La`W)}9s2;C6>TkW?IVz!9tg>-;(G7luKl^Pw~kUaG$q5g<%3$TPn4 z`n7?>ae=?P#KPBe|Lj)O5+%uutCI`uV5vY-kZmO?zm)kug#~^qbGCIW#Z64 z9~K&=KVZP-1j{HYcqiX2e7?K03K<)?s+jMYkB{UIZG}YHI92@BT)yA+?0_mBHSfP|LJX`VHJp-E&5T!WJ+Duks7a(QY!^qk(|jgmUn3s>qVrf3RQat45nlQE z_^M$X+6gK>7}NH6e&^lQX}a>%)Vi=*V7^x+5ZrNlR%sgib*WJ7xQf})>v`J^OoCs+ zYhJ#6de@Bid`8xE)84+VTWR1>z~1^|mj-2QQ-7|DIC6URLqE5g-X_W$Gk6h4>oSu3 zreNM>kZ>lFn@)Wg?X!5emaxAP(5d-liSv|p9&Ls0Kii!>cs7l^e4SUL!Zl*N;pfz5 zPw?;B^+p%ctEan4!uCswsIWa_Bvrvu_RBj2o;g4XzW^PI53r~#W7JC{!*kp|XC)v4d zjFb4COC2$0EnR&NS&*;T?cCq;D`_b?IncrdLla4ddjW^)5nn9PsS+{!-owO4^2MJz zi>Eq^_#7sib?rZ$BAZI?L|untYX%8`!-=|}ubCtL4I0{s_9|6Z+-;w9&%!Z#chArL$j(;(z*7%CK>(mLCO;zVjI&z&U_hu)b8Pj-2lQ^21+=?^ z<>wRy=Tl61?JMU3)DA_X!9Y*zOv>jx_ZZ|IT^Is4&aS32nTw1SX_|%!hvd+%jFHij zZM)egY!~NU(BUa-Jd4~5zxjpeQHg{a+Y^Zqvx59owmLOU*y|{3bHAz2^}c`be2eT* z>4f%cVV$XL70*?j218trsG2M-%`S1Uh!F}YgW-6sNPeXYOq`kLhHj6=ZQn(*W1<9N zn!{CdmQ4V{NwJ(Beue~4{vmYmfI(U_5vSaV#g}5MkxYyH2$Q?K&D3D?KixzYNAMI_ z;e=l-|DnWBScG^MuEeE>+}u`~r5_W8faM33wg6n2Jy62SDQOPYOYtuWpD{!;Y~^#l zLpX|syuG}HB=onCOtoUw@ zakxr@76G-I6dkG~Ep}RH3r{OZnIyD7^+Ryp5I7NSTZE=J0HTEb?IHx}KKUZ z#XyLF53$)tgJ4mjMvkD@StV6kUV=oM%tTAhh#W?TrNn{n1`ND8JC^fR>3w)y88BB5 z6Sr7-m?E?PlDr})&U5)&&+Fx?k}>5OC)iv9>fa`g7^HD}_urfYmVJ&o>P(}^spo<+ z!#@OT`%gGOG16yk^k}}=)H-fwQ7rgq!Z*a$$D_rcKetvNVW84`^$jcSXFY#d=cf)m zV4^K>lSB7hCf<`;yw6}c-1u$=DyqetQLG13JFS*vi0f)niL_wKR0uu|f?$r6W0V&8 z-H3wg>`%at@%R3EIA$0(-}qzCKMwNiuyLK+C2ym*KWsMP3$|pYfooHLgwMJqs#rjc zs>s@vm64SY2=v2BIhJpzuuS@D1HTe9zZ*2>IYxI$OLiXRO>^~OC4@6OAyp%;DhB3S zOgl~g?9sV;;Z9T#)6d;sE)q)sftiwDQmmfB$MM~ggNKrwl8XHA8QM2V$4Kn)HpP(U zC@y5*#k%%hN{e!Ij)h}O!ZjCii`Ud%;}Rte@fWI|iWW1Aw4 zD77v3)!7*^^?el-pc;`W4g&)-=68i>AU88p> zE;>l0wzJPf<|L6ZnDdSoB1+f8J*Z$(L7KR#naj$QR5wNyKn!&FJy^AE+e7?dMqqMp z);@L9ta7YEvf;R8r*X+93GoAaQXHiq*y^a%F$tzCUCT`huOOxtuIzKCr!e7=lb>nv zO;-|r_`iBHf|U%zULGDt{176oR>D^U{2`%ojf+Yd4PFQ8u}*5I5@`DlvtXain0alT z>wV*;SCqTju1Z(gLa;Ua4hQo9IB9qB$-DWXM4<_+JJNu5j(bpSJ*3nj0fgTL`9R+E zj|;2>eUXC%`2#D<=Z+zn*1((DSr}FeC9h_G;lN~ZPtAS zq$udzD@3_A%W>u3EGb9`8+ZFTRhO2hf0-2)RfUEIzo};X=pJWA=iFJ*Z48g_h=md| z`n}5@q&&frWkUi{%-My;NZ>&@!>q0?BPS!S5P#yP`kUxy1GfgSB?KXb6*eb@`344V zj#&!_ZiIJ+rzD=2P63D*uS%CXw}=4N|4-h8g2n}d@#PmLL}R^=YGb*Y#!>}c6Kz)_ zU=y?~Q`ukh{f_7ltU9ptTobP8YSMO8i)@<8RHs1%cY zoN^yM_AMEfXD=-N_p1mFYeo+jHGPhT#ZM2MlgEoOe*mDpQNXlZo66&#+Ko@LxCnxD zZR(-VUn;->rq|xgrCPzxl1do@``6FTVKCzhNu5|B$0%&js%VM?Q zVu}Dc3K7)}1eOv?Axlv}B}ZvG6l@;D-b%2$aaOljTEwBuPhUoafvKydW^Y44R?h0p zIzerP8K2pmbD=s$k-ctg*~= z2+kbB$h$NlAb~`Ultcu%?YG<%mx{M!w=9eaL>&_4_%{z78c#d=p5Y{Do=&-b{JOlQovwQcu(vrTfpV_+zZ4eqZnMAxJ z3LD^*`PHy>dA^cfr&Nhd{0%+op)70+?nP$UJrS-f63Kzf!6(4kKLoUEC3kBuXuGdk zR3p6N`h4PQzS4_`SzB8aIF)K?=kxaI0;0*x1D z05S+B*tdX4NTNf=dK8-$F%Ktuwp}wIy#8^9*)Pxdy&u5?r(Pv&CyBWh6?=p}^{BOE zc9l5;OKz9;XDi1N`F}l2#Ac$+NDekQobuhe31&YQIXi?dTN)bI6ks>Fz9m#~648E} zS0P%%Gbok!JkX10sT&)=;fKB511%Zsb3ztcC0VQG{%ZNO1(zm+?a@z2QiiR2((EKt zUQxPLlJAL8pz7+U8>OS;*d-6cS0wM}C*H*PS3P4|oOtuHW$%kz@`w5~vC-GxoY1lK zJeRxeEE*Lj$t3Fy4&`M~`pP+6J@>jzmtTvadLllMdlT%VV_!|deRV^g@|BufSZPoi zDvBBv*cDAU^bU?~*>CSn$0f$b4jgaazdAxzApH%nmp);yGK~C7ERm2b-ZOgSqzjsp-#F6cZ%T(bk?IUaMa4(^fVhYk z#cb|*kpTB;R$8)v@HG$Rc#jp>#)nAXisrIBpYqqq2#|}vW>E#xFdLp?F=BD(%^?y` zDO5@|g0(4$=DuXE&;4K2-wAq87iA_g%(FUx%D9f= z@nX*?84T$A)f+w#h6?{v2D@ZsLOv6%W{xKh->_1hklR|=pIQ8&%{iS88gk zWId25PZ1di+venV=)<+^$kf^J*2dlqYU)Ick}4`;cV}tU2W{b5zv0@X-Q%6ObOYfn z{g#5#9kXq?gnQVKx~QRNdZ1EB@DT$F*0iFK{@SA;lRyl(F2gFIXSPI3vC6=QE%zggBGw*|j10`uf4 zmvobJX97c8KHcJ{&NG*ezUEFE>hc)@S zk=N~=i!K-TyHoT>A^k~_e+z6nVO+cVg2wXxX)k2w`JU?v7Rso{7XGTVpYj*ewDLR& zj{N#*6t{mapx~E>{SMRBLj3j|e19f6cdm*-CL71UJ%nev!|dUK`{CO7y-_msr}nv_ zpRZmgm;9aJf9`87#zhM3^(bHAX*iz{1I#vkGzpljHKJgwVf!`uMk-%% zE#^I1#LyzSJhiF*+2ymJf$2vlVJdAMRh0<0lg}ZE%6}WJXNP6BfdTUs<)7PcytA!5 zFP5)$tzR~+F78hQ&Zl>O`*l^8p$19rYMG?H+yo7qjIw4ea@|`FTEJEvUh>ET6fF3t z>?o7vTm;#2_cN{POkXuqJ3KC`Yy6rD!W(ZLTW3VZROcr^}YKemMfNbkrx5N5pK76^GrX%MIBFdQ}UA z4T)vn(XTaO83;p$1%bs`yl;R!)dlr0a*GDELc!iB#l~>K=xj6FSO37M;k5ZMDBA{3 zaWgs-d$>k6z-d#@iT#`2A6RUK3CH1L7+P5y6U%~x9|g_Mf75cy7!=q2ll}2v?~to0e-@PCmx5xUaT5zb8L``-w6qump>k(^hHITYY% zsd-t%u~7XKqHk`$Fc0+{d+;3NJl(L^TBUJ-rvnwmSifU(YGzj`hgtbK!=hxMZD}%s zR2=2#;ne#Nmi$02O=5FP<9t_~0WXfXS>@#3O~^O23?30-h;FHm6jvpnRv(z!`)(>4 z?hrIaa*=*)Q&8J>EB5V-f(lJBC1%6zbO3G&hB5$~$61J&=ke5RBJ##7-|)p4Tch^2 zwB<(9g2CU7H93Tf(5w&Bv_1`LyX^Y?`TeBPh3L$&CYmOcH*2EXwPK39mQp@&SY*5{ z=~UgZCCdvc>9&4HwAG-YE2ZU*tfGA~W^8bt@KOu}Q5-N>q_;R+w3-)*ad>Y~{Eq$! zD(25=(76677cBbA?ocpp`22}tS1$X`yPcgRVT3mEMIRsQk>{_+_`eK|2r)~{|N1MV zgLTle;Cf&I#{YX+$)TaqqW!lm6TyEI(Ze~xq;M3N9{m-36|8{n2WOFz#z+x>rwUO2 zOELUsB>xdj3J(p9^uNQ=MB$&L*ctv$R17pUvj4}6!xyFG(7WJt((E`1lK)cw1Mm1l Ap#T5? delta 5882 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ΝN38v>I3Ys8cVBHZu)pJcs+QpQe(Q(mxlM@;Balw2(C%>Bk&0)Xnp+Ek`Y`{1_U)Yjb^@ui81VqBJH-!iY>6> zZ;HYUKoC?l3vID*SnaT>Y^Ctf*mIF`mMW&g120!FR+^hae85A+nnOf>*HO(T`$hH? z8QVk{>i@9KA5a@GcUmwMg8ETO@)|>=qX<>4Ir)>@|11~Y`r{i7!45~yQ@=dry^l$vh`O6YXFAc;r^2bA+cz+`qy!xD zh6w>>;Q#!W>4Y`VU`ejRJkhkdzcHEAEG&;Ek@Qzs3e7Z0TtpPjCkZ0L sM*aVt`ri)9q;?S|>i?*S4ge7Um;5KAPP!41Kr2WR5@o`475X>zFPFSVivR!s diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index a0493db..6a14b61 100644 --- a/Project2-Stream-Compaction/src/main.cpp +++ b/Project2-Stream-Compaction/src/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "testing_helpers.hpp" const int SIZE = 1 << 8; //1 << 8; // feel free to change the size of array @@ -19,6 +20,8 @@ int *a = new int[SIZE]; int *b = new int[SIZE]; int *c = new int[SIZE]; +#define RADIX_CLASS_EXAMPLE 1; + int main(int argc, char* argv[]) { // Scan tests @@ -147,6 +150,50 @@ int main(int argc, char* argv[]) { //printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); + + printf("\n"); + printf("**********************\n"); + printf("** RADIX SORT TESTS **\n"); + printf("**********************\n"); + + #if RADIX_CLASS_EXAMPLE + const int size = 8; + const int npot = 7; + int unsorted_array[] = { 0, 4, 1, 2, 5, 6, 7, 3 }; //hard coding array to class example + int unsorted_arrayNPOT[] = { 0, 4, 1, 2, 5, 6, 3 }; + #else + const int size = SIZE; + const int npot = NPOT; + int unsorted_array[size]; + int unsorted_arrayNPOT[npot]; + memcpy(unsorted_array, a, sizeof(int) * size); + memcpy(unsorted_arrayNPOT, a, sizeof(int) * NPOT); + #endif // CLASS_EXAMPLE + + int sorted_array[size]; + memcpy(sorted_array, unsorted_array, sizeof(int) * size); // copy and sort to compare + std::sort(sorted_array, sorted_array + size); + + int sorted_arrayNPOT[npot]; + memcpy(sorted_arrayNPOT, unsorted_arrayNPOT, sizeof(int) * npot); // copy and sort to compare + std::sort(sorted_arrayNPOT, sorted_arrayNPOT + npot); + + printArray(size, unsorted_array, true); + + zeroArray(size, c); + printDesc("radix sort, power-of-two"); + StreamCompaction::Radix::sort(size, c, unsorted_array); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(size, c, true); + printCmpResult(size, sorted_array, c); + + zeroArray(npot, c); + printDesc("radix sort, non-power-of-two"); + StreamCompaction::Radix::sort(npot, c, unsorted_arrayNPOT); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(npot, c, true); + printCmpResult(npot, sorted_arrayNPOT, c); + system("pause"); // stop Win32 console from closing on exit delete[] a; delete[] b; diff --git a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt index 4bb0dc2..fc37515 100644 --- a/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt +++ b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt @@ -9,6 +9,8 @@ set(SOURCE_FILES "efficient.cu" "thrust.h" "thrust.cu" + "radix.h" + "radix.cu" ) cuda_add_library(stream_compaction diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.h b/Project2-Stream-Compaction/stream_compaction/efficient.h index 803cb4f..1450345 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.h +++ b/Project2-Stream-Compaction/stream_compaction/efficient.h @@ -6,6 +6,8 @@ namespace StreamCompaction { namespace Efficient { StreamCompaction::Common::PerformanceTimer& timer(); + void workEfficientScan(int n, int *dev_idata, dim3 &threadsPerBlock, dim3 &fullBlocksPerGrid); + void scan(int n, int *odata, const int *idata); int compact(int n, int *odata, const int *idata); diff --git a/Project2-Stream-Compaction/stream_compaction/radix.cu b/Project2-Stream-Compaction/stream_compaction/radix.cu new file mode 100644 index 0000000..1f48d4d --- /dev/null +++ b/Project2-Stream-Compaction/stream_compaction/radix.cu @@ -0,0 +1,115 @@ +#include +#include +#include "common.h" +#include "naive.h" +#include "efficient.h" +#include +#include "radix.h" + +/*! Block size used for CUDA kernel launch. */ +#define blockSize 128 + +namespace StreamCompaction { + namespace Radix { + using StreamCompaction::Common::PerformanceTimer; + PerformanceTimer& timer() + { + static PerformanceTimer timer; + return timer; + } + + __global__ void kernFullAddress(int n, int totalFalses, int *indices, int *bools){ + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + indices[index] = bools[index] ? indices[index] : index - indices[index] + totalFalses; + } + + __global__ void kernBitMapToBoolean(int n, int bit, int *bools, const int *idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + bools[index] = idata[index] & bit ? 0 : 1; + } + + __global__ void kernFullScatter(int n, int *odata, const int *idata, const int *indices) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + odata[indices[index]] = idata[index]; + + } + + int findMax(int n, const int *idata) { + int mx = idata[0]; + for (int i = 1; i < n; i++) { + if (idata[i] > mx) { + mx = idata[i]; + } + } + return mx; + } + + void sort(int n, int *odata, const int *idata) { + //int mx = findMax(n, idata); + int kbit = ilog2ceil(n); + int npad = 1 << kbit; + + int *dev_idata, *dev_odata, *dev_bools, *dev_indices; + cudaMalloc((void**)&dev_idata, sizeof(int) * npad); + checkCUDAError("cudaMalloc dev_idata failed!"); + cudaMalloc((void**)&dev_odata, sizeof(int) * npad); + checkCUDAError("cudaMalloc dev_odata failed!"); + cudaMalloc((void**)&dev_bools, sizeof(int) * npad); + checkCUDAError("cudaMalloc dev_bools failed!"); + cudaMalloc((void**)&dev_indices, sizeof(int) * npad); + checkCUDAError("cudaMalloc dev_indices failed!"); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + checkCUDAError("Memcpy idata failed!"); + + dim3 threadsPerBlock(blockSize); + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + timer().startGpuTimer(); + for(int i = 0; i <= kbit; i++){ + kernBitMapToBoolean<< > > (n, (1 << i), dev_bools, dev_idata); + checkCUDAError("Memcpy kernMapToBoolean failed!"); + + cudaMemcpy(dev_indices, dev_bools, sizeof(int) * n, cudaMemcpyDeviceToDevice); + checkCUDAError("Memcpy dev_bools to dev_indices failed!"); + StreamCompaction::Efficient::workEfficientScan(npad, dev_indices, threadsPerBlock, fullBlocksPerGrid); + checkCUDAError("Efficient scan failed!"); + + int totalFalses, lastBool; + cudaMemcpy(&totalFalses, dev_indices + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy totalFalses failed!"); + cudaMemcpy(&lastBool, dev_bools + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + checkCUDAError("Memcpy lastBool failed!"); + + totalFalses += lastBool; + kernFullAddress << > > (n, totalFalses, dev_indices, dev_bools); + checkCUDAError("kernFullAddress failed!"); + + kernFullScatter << > > (n, dev_odata, dev_idata, dev_indices); + checkCUDAError("kernScatter failed!"); + + cudaMemcpy(dev_idata, dev_odata, sizeof(int) * n, cudaMemcpyDeviceToDevice); + checkCUDAError("Memcpy dev_odata to dev_idata failed!"); + } + + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, sizeof(int) * n, cudaMemcpyDeviceToHost); + + + cudaFree(dev_idata); + cudaFree(dev_odata); + cudaFree(dev_bools); + cudaFree(dev_indices); + } + } +} diff --git a/Project2-Stream-Compaction/stream_compaction/radix.h b/Project2-Stream-Compaction/stream_compaction/radix.h new file mode 100644 index 0000000..aa489d9 --- /dev/null +++ b/Project2-Stream-Compaction/stream_compaction/radix.h @@ -0,0 +1,11 @@ +#pragma once + +#include "common.h" + +namespace StreamCompaction { + namespace Radix { + StreamCompaction::Common::PerformanceTimer& timer(); + + void sort(int n, int *odata, const int *idata); + } +} From 5aa2bbf95709a68b7b035f695f1e286de95baed6 Mon Sep 17 00:00:00 2001 From: klaywittler Date: Mon, 16 Sep 2019 16:02:09 -0400 Subject: [PATCH 13/20] console output --- .../img/console_output.png | Bin 0 -> 77274 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Project2-Stream-Compaction/img/console_output.png diff --git a/Project2-Stream-Compaction/img/console_output.png b/Project2-Stream-Compaction/img/console_output.png new file mode 100644 index 0000000000000000000000000000000000000000..88999516cabe60a7b26e9b4846b2152af0ddb5c9 GIT binary patch literal 77274 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
Psz)j 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 Date: Tue, 17 Sep 2019 13:28:07 -0400 Subject: [PATCH 14/20] readme --- Project2-Character-Recognition/README.md | 13 +- .../character_recognition/mlp.cu | 128 +- Project2-Character-Recognition/src/main.cpp | 10 +- Project2-Stream-Compaction/README.html | 1164 +++++++++++++++++ Project2-Stream-Compaction/README.md | 132 +- .../img/array_compact_speed.png | Bin 0 -> 25015 bytes .../img/array_scan_speed.png | Bin 0 -> 27822 bytes .../img/binary_tree.png | Bin 0 -> 22237 bytes .../img/block_compact_speed.png | Bin 0 -> 25622 bytes .../img/block_scan_speed.png | Bin 0 -> 33971 bytes .../img/console_output.png | Bin 0 -> 76496 bytes .../img/data/dataCollection.xlsx | Bin 0 -> 18817 bytes .../img/data/scan_compact_figures.pptx | Bin 0 -> 274739 bytes Project2-Stream-Compaction/img/downSweep.png | Bin 0 -> 40413 bytes Project2-Stream-Compaction/img/naiveScan.png | Bin 0 -> 39404 bytes .../img/parallel_stream_compact.png | Bin 0 -> 19116 bytes .../img/radix_explanation.png | Bin 0 -> 73909 bytes .../img/radix_parallel_pass.png | Bin 0 -> 47456 bytes .../img/stream_compact.png | Bin 0 -> 5602 bytes .../img/threadManagement.png | Bin 0 -> 140334 bytes Project2-Stream-Compaction/img/upSweep.png | Bin 0 -> 23706 bytes Project2-Stream-Compaction/src/main.cpp | 6 +- README.md | 4 +- 23 files changed, 1344 insertions(+), 113 deletions(-) create mode 100644 Project2-Stream-Compaction/README.html create mode 100644 Project2-Stream-Compaction/img/array_compact_speed.png create mode 100644 Project2-Stream-Compaction/img/array_scan_speed.png create mode 100644 Project2-Stream-Compaction/img/binary_tree.png create mode 100644 Project2-Stream-Compaction/img/block_compact_speed.png create mode 100644 Project2-Stream-Compaction/img/block_scan_speed.png create mode 100644 Project2-Stream-Compaction/img/console_output.png create mode 100644 Project2-Stream-Compaction/img/data/dataCollection.xlsx create mode 100644 Project2-Stream-Compaction/img/data/scan_compact_figures.pptx create mode 100644 Project2-Stream-Compaction/img/downSweep.png create mode 100644 Project2-Stream-Compaction/img/naiveScan.png create mode 100644 Project2-Stream-Compaction/img/parallel_stream_compact.png create mode 100644 Project2-Stream-Compaction/img/radix_explanation.png create mode 100644 Project2-Stream-Compaction/img/radix_parallel_pass.png create mode 100644 Project2-Stream-Compaction/img/stream_compact.png create mode 100644 Project2-Stream-Compaction/img/threadManagement.png create mode 100644 Project2-Stream-Compaction/img/upSweep.png diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index f252333..bf2098a 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -10,7 +10,14 @@ CUDA Character Recognition ## Sections * [Introduction](#introduction) -* [Performance Analaysis](#performance-analysis) - * [Questions](#questions) -* [Addition Optimization](#additional-optimization) +* [Impelmentation](#implementation) +* [Additions](#additions) + +# Introduction + +# Implementation + +# Additions +## Matrix multiplication +Each layer of the network is handle with a matrix multiplication through cublas then feed through the sigmoid activation in parallel. diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index d8f9e47..cf9f37d 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -29,9 +29,9 @@ namespace CharacterRecognition { void printMat(float*P, int uWP, int uHP) { int i, j; for (i = 0; i < uHP; i++) { - printf("\n"); for (j = 0; j < uWP; j++) - printf("%f ", P[index(i, j, uHP)]); + printf("%f", P[index(i, j, uHP)]); + printf("\n"); } } @@ -119,76 +119,11 @@ namespace CharacterRecognition { } }*/ - template __global__ void MatrixMulCUDA(float *C, float *A, float *B, int wA, int wB) { - // Block index - int bx = blockIdx.x; - int by = blockIdx.y; - - // Thread index - int tx = threadIdx.x; - int ty = threadIdx.y; - - // Index of the first sub-matrix of A processed by the block - int aBegin = wA * BLOCK_SIZE * by; - - // Index of the last sub-matrix of A processed by the block - int aEnd = aBegin + wA - 1; - - // Step size used to iterate through the sub-matrices of A - int aStep = BLOCK_SIZE; - - // Index of the first sub-matrix of B processed by the block - int bBegin = BLOCK_SIZE * bx; - - // Step size used to iterate through the sub-matrices of B - int bStep = BLOCK_SIZE * wB; - - // Csub is used to store the element of the block sub-matrix - // that is computed by the thread - float Csub = 0; - - // Loop over all the sub-matrices of A and B - // required to compute the block sub-matrix - for (int a = aBegin, b = bBegin; - a <= aEnd; - a += aStep, b += bStep) { - // Declaration of the shared memory array As used to - // store the sub-matrix of A - __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; - - // Declaration of the shared memory array Bs used to - // store the sub-matrix of B - __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; - - // Load the matrices from device memory - // to shared memory; each thread loads - // one element of each matrix - As[ty][tx] = A[a + wA * ty + tx]; - Bs[ty][tx] = B[b + wB * ty + tx]; - - // Synchronize to make sure the matrices are loaded - __syncthreads(); - - // Multiply the two matrices together; - // each thread computes one element - // of the block sub-matrix - #pragma unroll - - for (int k = 0; k < BLOCK_SIZE; ++k) { - Csub += As[ty][k] * Bs[k][tx]; - } - - // Synchronize to make sure that the preceding - // computation is done before loading two new - // sub-matrices of A and B in the next iteration - __syncthreads(); - } - } - void matrixMultiply(cublasHandle_t* handle, sMatrixSize &matrix_size, float *d_A, float *d_B, float *d_C){ const float alpha = 1.0f; const float beta = 0.0f; - cublasSgemm(*handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.WB, matrix_size.HA, matrix_size.WA, &alpha, d_B, matrix_size.WB, d_A, matrix_size.WA, &beta, d_C, matrix_size.WB); + //cublasSgemm(*handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.HA, matrix_size.HB, matrix_size.WA, &alpha, d_A, matrix_size.HA, d_B, matrix_size.HB, &beta, d_C, matrix_size.HC); + cublasSgemm(*handle, CUBLAS_OP_N, CUBLAS_OP_N, matrix_size.WA, matrix_size.WB, matrix_size.HA, &alpha, d_A, matrix_size.HA, d_B, matrix_size.HB, &beta, d_C, matrix_size.HC); checkCUDAError("matrix multiply"); } @@ -200,14 +135,6 @@ namespace CharacterRecognition { } input[index] = 1.0f / (1 + exp(-input[index])); } - - __global__ void kernSigmoid2(int n, float *input) { - int index = threadIdx.x + (blockIdx.x * blockDim.x); - if (index >= n) { - return; - } - input[index] = 1.0f / (1 + exp(-input[index])); - } void backward(){} @@ -258,8 +185,9 @@ namespace CharacterRecognition { matrixMultiply(&handle, hidden_matrix_size, dev_wI, dev_X, dev_h1); cudaMemcpy(h1, dev_h1, mem_size_pred, cudaMemcpyDeviceToHost); checkCUDAError("cudaMemcpy pred"); - printf("\n\n Matriz h1:"); - printf("\n %f %f", h1[0], h1[1]); + printf("Matriz h1: \n"); + //printf("%f %f \n", h1[0], h1[1]); + printMat(h1, hidden_matrix_size.WC, hidden_matrix_size.HC); kernSigmoid <<> > (hidden_matrix_size.HC*hidden_matrix_size.WC, dev_h1); checkCUDAError("kernSigmoid"); @@ -271,11 +199,10 @@ namespace CharacterRecognition { matrixMultiply(&handle, output_matrix_size, dev_wO, dev_h1, dev_pred); cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); checkCUDAError("cudaMemcpy pred"); - printf("\n\n Matriz pred:"); - printf("\n %f", pred[0]); - printf("\n"); + printf("Matriz pred: \n"); + printMat(pred, output_matrix_size.WC, output_matrix_size.HC); - kernSigmoid2 << > > (output_matrix_size.HC*output_matrix_size.WC, dev_pred); + kernSigmoid << > > (output_matrix_size.HC*output_matrix_size.WC, dev_pred); checkCUDAError("kernSigmoid"); cudaMemcpy(pred, dev_pred, mem_size_pred, cudaMemcpyDeviceToHost); @@ -307,9 +234,9 @@ namespace CharacterRecognition { fixedInit(wI, size_wI); fixedInit(wO, size_wO); - float *permuteData = (float *)malloc(numData); - float *Xi = (float *)malloc(sizeData); - float *yi = (float *)malloc(numLabels); + float *permuteData = (float *)malloc(sizeof(float)*numData); + float *Xi = (float *)malloc(sizeof(float)*sizeData); + float *yi = (float *)malloc(sizeof(float)*numLabels); unsigned int size_pred = output_matrix_size.WC * output_matrix_size.HC; unsigned int mem_size_pred = sizeof(float) * size_pred; @@ -323,28 +250,29 @@ namespace CharacterRecognition { memcpy(Xi, (void **)&X[sizeData*index], sizeData * sizeof(float)); memcpy(yi, (void **)&y[numLabels*index], numLabels * sizeof(float)); - printf("index %i \n", index); - printf("data: %f %f label: %f \n", Xi[0] , Xi[1], yi[0]); + printf("index: %i data: %f %f label: %f \n", index, Xi[0] , Xi[1], yi[0]); forward(pred, Xi, wI, wO, hidden_matrix_size, output_matrix_size); for (int j = 0; j < numLabels; j++) { - printf("prediction: %f \n", pred[j]); + printf("prediction: %f \n\n", pred[j]); } } printf("forward done \n"); } printf("predictions done \n"); - //free(wI); - //free(wO); - //free(Xi); - //free(yi); - //free(permuteData); - //free(pred); + free(wI); + free(wO); + free(Xi); + free(yi); + free(permuteData); + free(pred); } + void test(){} + void testMatrixMultiply() { - sMatrixSize matrix_size = { 3, 4, 3, 2, 2, 4}; + sMatrixSize matrix_size = { 2, 2, 1, 2, 1, 2}; // allocate host memory for matrices A and B unsigned int size_A = matrix_size.WA * matrix_size.HA; @@ -358,8 +286,9 @@ namespace CharacterRecognition { srand(2006); // initialize host memory - indexInit(h_A, size_A); - indexInit(h_B, size_B); + fixedInit(h_A, size_A); + h_B[0] = 1; h_B[1] = 1; + //fixedInit(h_B, size_B); // allocate device memory float *d_A, *d_B, *d_C; @@ -386,8 +315,7 @@ namespace CharacterRecognition { cublasCreate(&handle); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //matrixMultiply(&handle, matrix_size, d_A, d_B, d_C); - MatrixMulCUDA << < grid, threads >> > (d_C, d_A, d_B, matrix_size.HA, matrix_size.HB); + matrixMultiply(&handle, matrix_size, d_A, d_B, d_C); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // copy result from device to host diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index d8c0303..14ecf69 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -17,6 +17,8 @@ #define numData 4 #define hiddenNodes 2 +#define index(i,j,ld) (((j)*(ld))+(i)) + void readData(float *X, float *y) { for (int i = 0; i < sizeData; i++) { for (int j = 0; j < sizeData; j++) { @@ -50,11 +52,11 @@ int main(int argc, char* argv[]) { printDesc("reading data"); readData(X,y); - printDesc("test multiply"); - CharacterRecognition::testMatrixMultiply(); + //printDesc("test multiply"); + //CharacterRecognition::testMatrixMultiply(); - //printDesc("training"); - //CharacterRecognition::train(X, y, sizeData, hiddenNodes, numLabels, numData); + printDesc("training"); + CharacterRecognition::train(X, y, sizeData, hiddenNodes, numLabels, numData); free(X); diff --git a/Project2-Stream-Compaction/README.html b/Project2-Stream-Compaction/README.html new file mode 100644 index 0000000..86b869d --- /dev/null +++ b/Project2-Stream-Compaction/README.html @@ -0,0 +1,1164 @@ +README

CUDA Stream Compactionî…—

+

University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2

+
    +
  • Klayton Wittler +
  • +
  • Tested on: Windows 10 Pro, i7-7700K @ 4.20GHz 16.0GB, GTX 1070 8.192GB (my PC)
  • +
+

Sectionsî…—

+ +

Introductionî…—

+

This project implements parallel algorithms in reduction, scanning, and stream compaction in CUDA on the GPU. There is both a naive and work efficient scanning algorithm, however the work efficient still requires memory optimizations to see its real benefits. Stream compaction is used to compress data by removing unnecessary elements.

+

Scanningî…—

+

Scanning an array is getting the accumulated sum of the elements. This can be both inclusive of the last element or exclusive of the last element. For the purposes of this project we will focus on the exclusive version, knowing that we can convert between the two with a shift.

+

Naiveî…—

+

The naive approach to scanning in parallel would be to make multiple passes in the array, each pass moving the summing elements down the array in an exponenial fasion as seen in the figure below.

+

naive-scan

+

To implement this in parallel, we have the psuedocode below that in practice requires two arrays to buffer read and writes. +

for d = 1 to log_2(n)
+    for all k in parallel
+        if k >= 2^(d-1)
+            output[k] = input[k - 2^d+1] + input[k]
+        else
+            output[k] = input[k]
+        end
+    end
+    swap(input,output)
+end
+
+This will return an inclusive scan so to convert we shift all elements to the right and add in an identity in the first element, which addition is zero.

+

Work Efficientî…—

+

The work efficient approach perform two sweeps (referred to as up and down sweep) of these exponential passes but allows for inplace operation. The upsweep performs a parallel reduction and the downsweep cleverly utilizes the array as a binary tree to return the exclusive scan.

+

The upsweep, parallel reduction, uses a divide and conquer approach to achieve the total sum of the array in the last element in this example.

+

upsweep +

for d = 0 to log_2(n-1)
+    for all k to n by 2^(d+1) in parallel
+        input[k + 2^(d+1) - 1] += input[k + 2^d]
+    end
+end
+

+

The downsweep treats the array as a binary tree that only saves the left child as seen below and traverses down it following 3 rules as it goes.

+

binary-tree

+

Rules +1. Set the root of the entire tree to 0 +2. Add the parent value and left child value to the right child +3. Copy the parent value to the left child

+

The implementation of this is seen below in the figure where the black arrow represents rule 1, green arrows as rule 2, and orange arrows as rule 3. The psuedocode also shows how this would be done algorithmically in parallel.

+

downsweep +

input[n - 1] = 0
+for d = log_2(n-1) to 0
+    for all k = 0 to n-1 by 2^(d+1) in parallel
+        temp = input[k + 2^d]
+        input[k + 2^d - 1] = input[k + 2^(d+1) - 1]
+        input[k + 2^(d+1) - 1] += temp
+    end
+end
+

+

Stream Compactionî…—

+

Stream compaction in parallel has three parts: condition mapping, scanning, and scatter.

+

parallel-stream-compaction

+

We create an array that indicates whether the element should be included. Then scan the map array to determine the elements index in the return array. Then scatter the results into the new array by checking the condition map array, looking up the index, and placing the element in that index.

+

Performance Analysisî…—

+

To do a performance analysis, the run time of each algorithm is clocked in milliseconds. The faster the algorithm the lower the run time and the better the approach. The scan comparisons are separate from the compact comparison, but are denoted on the graphs as the same name since compact utilizes the scan.

+

Optimize Block Sizeî…—

+

In order to perform an accurate comparison between implementations, the implemented methods of naive and work efficient need to have the block sizes optimized.

+

Below we see the outcome of run time at two different array sizes (1 << 16 and 1 << 10 respectively) accross block sizes that are multiples of warp size.

+

block-scan-speed +block-compact-speed

+

The optimal block size for each approach was chosen and based on the run time analaysis this is generally 128 threads per block.

+

Compare Implementationsî…—

+

In the run time graphs below we see that the ‘efficient’ approach does not perform well even compared to the CPU version. We only see the CPU start to become comparable in compactions with a scan and this is because everything is being done serially. On the other hand the naive implementation is somewhat comparable to the CPU versions but still generally performs worse. We can also see the thrust implementation at the very bottom remains relatively constant through array sizes.

+

array-scan-speed +array-compact-speed

+

Explanationî…—

+
    +
  • Write a brief explanation of the phenomena you see here. Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation?
  • +
+

The primary reasons for these performance gaps in the ‘efficient’ approach despite the parallel capabilities of the GPU is the thread management. In the figure below, what has been implemented is on the left and whats left to optimize is on the right. We can see thread divergence in each pass with a red circle and warp retirement can be seen in greyed out threads. In the implemented version we can see each level has thread divergence which means that threads are sitting waiting for other threads in the warp to finish a set of instructions. The optimized version does not have a thread divergence until the very last pass in which it is returning the answer anyway. We can also see that the optimized version frees up warps much faster as it goes allowing more work to be done overall.

+

thread-management

+

The performance gaps in the naive approach and secondarily in the ‘efficient’ approach is memory optimizations. We can further resturture the way we access memory to use shared memory within blocks, change access patterns to avoid bank conflicts and utilize the full band width on the bus.

+

We can see from the graphs that the thrust implementation is the quickest and does not suffer from the performance issues since they have done these optimizations inside their functions. In running the NSight performance analysis it can also been seen that they are pulling alot of device information to optimize for my specific hardware.

+

Outputî…—

+

This output is for 1 << 8 array size and includes a test of radix sort algorithm. There are two flags RUN_RADIX and RADIX_CLASS_EXAMPLE at the top of main.cpp. Radix tests can be disabled by setting RUN_RADIX 0 and to run radix sort on the full test array set RADIX_CLASS_EXAMPLE 0 but is currently displaying the class example as to quickly verify its working.

+

console-output

+

Additionsî…—

+

Radix Sortî…—

+

Radix sort can be done in parallel by breaking the array into tiles and sorting each tile in parallel then merging. Further more sorting within it tile can be parallelized by utilizing scanning approaches developed previously. It should also be noted that radix sort operates on bits and requires k-bit passes to sort where k is the number of bits in each cell. It starts at the least significant bit (LSB) and moves to the most significant bit (MSB), partitioning the array and rearranging the array as it goes as seen below.

+

radix-explanation

+

Where within each pass we do parallel scans and scatters as seen below. We mantain a true and false condition array (in practice we can use one and just take the opposite when we need the other), scan the false array and then scan the true array starting where the false left off (in practice we can skip the true array and compute real time based on the false array). Finally, we can use the full array of indices and scatter the results.

+

radix-parallel-pass

\ No newline at end of file diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 3d5ef16..f645d6e 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -10,7 +10,135 @@ CUDA Stream Compaction ## Sections * [Introduction](#introduction) + * [Scanning](#scanning) + * [Stream Compaction](#stream-compaction) * [Performance Analaysis](#performance-analysis) - * [Questions](#questions) -* [Addition Optimization](#additional-optimization) + * [Optimize Block Size](#optimize-block-size) + * [Compare Implementations](#compare-implementations) + * [Explanation](#explanation) +* [Additions](#additionals) + * [Radix Sort](#radix-sort) +# Introduction +This project implements parallel algorithms in reduction, scanning, and stream compaction in CUDA on the GPU. There is both a naive and work efficient scanning algorithm, however the work efficient still requires memory optimizations to see its real benefits. Stream compaction is used to compress data by removing unnecessary elements. + +## Scanning +Scanning an array is getting the accumulated sum of the elements. This can be both inclusive of the last element or exclusive of the last element. For the purposes of this project we will focus on the exclusive version, knowing that we can convert between the two with a shift. + +### Naive +The naive approach to scanning in parallel would be to make multiple passes in the array, each pass moving the summing elements down the array in an exponenial fasion as seen in the figure below. + +![naive-scan](img/naiveScan.png) + +To implement this in parallel, we have the psuedocode below that in practice requires two arrays to buffer read and writes. +``` +for d = 1 to log_2(n) + for all k in parallel + if k >= 2^(d-1) + output[k] = input[k - 2^d+1] + input[k] + else + output[k] = input[k] + end + end + swap(input,output) +end +``` +This will return an inclusive scan so to convert we shift all elements to the right and add in an identity in the first element, which addition is zero. + + +### Work Efficient +The work efficient approach perform two sweeps (referred to as up and down sweep) of these exponential passes but allows for inplace operation. The upsweep performs a parallel reduction and the downsweep cleverly utilizes the array as a binary tree to return the exclusive scan. + +The upsweep, parallel reduction, uses a divide and conquer approach to achieve the total sum of the array in the last element in this example. + +![upsweep](img/upSweep.png) +``` +for d = 0 to log_2(n-1) + for all k to n by 2^(d+1) in parallel + input[k + 2^(d+1) - 1] += input[k + 2^d] + end +end +``` + +The downsweep treats the array as a binary tree that only saves the left child as seen below and traverses down it following 3 rules as it goes. + +![binary-tree](img/binary_tree.png) + +Rules + +1. Set the root of the entire tree to 0 + +2. Add the parent value and left child value to the right child + +3. Copy the parent value to the left child + +The implementation of this is seen below in the figure where the black arrow represents rule 1, green arrows as rule 2, and orange arrows as rule 3. The psuedocode also shows how this would be done algorithmically in parallel. + +![downsweep](img/downSweep.png) +``` +input[n - 1] = 0 +for d = log_2(n-1) to 0 + for all k = 0 to n-1 by 2^(d+1) in parallel + temp = input[k + 2^d] + input[k + 2^d - 1] = input[k + 2^(d+1) - 1] + input[k + 2^(d+1) - 1] += temp + end +end +``` + +## Stream Compaction +Stream compaction in parallel has three parts: condition mapping, scanning, and scatter. + +![parallel-stream-compaction](img/parallel_stream_compact.png) + +We create an array that indicates whether the element should be included. Then scan the map array to determine the elements index in the return array. Then scatter the results into the new array by checking the condition map array, looking up the index, and placing the element in that index. + +# Performance Analysis +To do a performance analysis, the run time of each algorithm is clocked in milliseconds. The faster the algorithm the lower the run time and the better the approach. The scan comparisons are separate from the compact comparison, but are denoted on the graphs as the same name since compact utilizes the scan. + +## Optimize Block Size +In order to perform an accurate comparison between implementations, the implemented methods of naive and work efficient need to have the block sizes optimized. + +Below we see the outcome of run time at two different array sizes (1 << 16 and 1 << 10 respectively) accross block sizes that are multiples of warp size. + +![block-scan-speed](img/block_scan_speed.png) +![block-compact-speed](img/block_compact_speed.png) + +The optimal block size for each approach was chosen and based on the run time analaysis this is generally 128 threads per block. + +## Compare Implementations + +In the run time graphs below we see that the 'efficient' approach does not perform well even compared to the CPU version. We only see the CPU start to become comparable in compactions with a scan and this is because everything is being done serially. On the other hand the naive implementation is somewhat comparable to the CPU versions but still generally performs worse. We can also see the thrust implementation at the very bottom remains relatively constant through array sizes. + +![array-scan-speed](img/array_scan_speed.png) +![array-compact-speed](img/array_compact_speed.png) + + +### Explanation + +* Write a brief explanation of the phenomena you see here. Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? + +The primary reasons for these performance gaps in the 'efficient' approach despite the parallel capabilities of the GPU is the thread management. In the figure below, what has been implemented is on the left and whats left to optimize is on the right. We can see thread divergence in each pass with a red circle and warp retirement can be seen in greyed out threads. In the implemented version we can see each level has thread divergence which means that threads are sitting waiting for other threads in the warp to finish a set of instructions. The optimized version does not have a thread divergence until the very last pass in which it is returning the answer anyway. We can also see that the optimized version frees up warps much faster as it goes allowing more work to be done overall. + +![thread-management](img/threadManagement.png) + +The performance gaps in the naive approach and secondarily in the 'efficient' approach is memory optimizations. We can further resturture the way we access memory to use shared memory within blocks, change access patterns to avoid bank conflicts and utilize the full band width on the bus. + +We can see from the graphs that the thrust implementation is the quickest and does not suffer from the performance issues since they have done these optimizations inside their functions. In running the NSight performance analysis it can also been seen that they are pulling alot of device information to optimize for my specific hardware. + +## Output +This output is for 1 << 8 array size and includes a test of radix sort algorithm. There are two flags ```RUN_RADIX``` and ```RADIX_CLASS_EXAMPLE``` at the top of ```main.cpp```. Radix tests can be disabled by setting ```RUN_RADIX 0``` and to run radix sort on the full test array set ```RADIX_CLASS_EXAMPLE 0``` but is currently displaying the class example as to quickly verify its working. + +![console-output](img/console_output.png) + +# Additions + +## Radix Sort + +Radix sort can be done in parallel by breaking the array into tiles and sorting each tile in parallel then merging. Further more sorting within it tile can be parallelized by utilizing scanning approaches developed previously. It should also be noted that radix sort operates on bits and requires k-bit passes to sort where k is the number of bits in each cell. It starts at the least significant bit (LSB) and moves to the most significant bit (MSB), partitioning the array and rearranging the array as it goes as seen below. + +![radix-explanation](img/radix_explanation.png) + +Where within each pass we do parallel scans and scatters as seen below. We mantain a true and false condition array (in practice we can use one and just take the opposite when we need the other), scan the false array and then scan the true array starting where the false left off (in practice we can skip the true array and compute real time based on the false array). Finally, we can use the full array of indices and scatter the results. + +![radix-parallel-pass](img/radix_parallel_pass.png) diff --git a/Project2-Stream-Compaction/img/array_compact_speed.png b/Project2-Stream-Compaction/img/array_compact_speed.png new file mode 100644 index 0000000000000000000000000000000000000000..06b74c5e81b3713506ced9d893011e3526bb499f GIT binary patch literal 25015 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 literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/img/array_scan_speed.png b/Project2-Stream-Compaction/img/array_scan_speed.png new file mode 100644 index 0000000000000000000000000000000000000000..c7fc67aec81e410c9c8f2de9e8a6b5281fb3c866 GIT binary patch literal 27822 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 L>hh(JUj+USFlYsI literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/img/binary_tree.png b/Project2-Stream-Compaction/img/binary_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb6d0d616437069a36f521a79af819aa3b6beb7 GIT binary patch literal 22237 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 literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/img/block_compact_speed.png b/Project2-Stream-Compaction/img/block_compact_speed.png new file mode 100644 index 0000000000000000000000000000000000000000..cf45f61b05c898722487f2c6caf1897372de3d8f GIT binary patch literal 25622 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