From 7d4711c2c71c29515ebf356cd30085b7d0f57994 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Sat, 14 Sep 2019 02:44:10 -0400 Subject: [PATCH 01/26] stream compaction completed. --- Project2-Stream-Compaction/src/main.cpp | 2 +- .../stream_compaction/CMakeLists.txt | 2 +- .../stream_compaction/common.cu | 14 +- .../stream_compaction/cpu.cu | 60 ++++++- .../stream_compaction/efficient.cu | 153 +++++++++++++++++- .../stream_compaction/naive.cu | 69 +++++++- 6 files changed, 283 insertions(+), 17 deletions(-) diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index d016553..fd98f07 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 << 3; // 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/CMakeLists.txt b/Project2-Stream-Compaction/stream_compaction/CMakeLists.txt index cdbef77..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_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..01fe486 100644 --- a/Project2-Stream-Compaction/stream_compaction/common.cu +++ b/Project2-Stream-Compaction/stream_compaction/common.cu @@ -23,7 +23,10 @@ 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 = (blockDim.x * blockIdx.x) + threadIdx.x; + if (index >= n) return; + + bools[index] = idata[index] != 0; } /** @@ -32,7 +35,14 @@ namespace StreamCompaction { */ __global__ void kernScatter(int n, int *odata, const int *idata, const int *bools, const int *indices) { - // TODO + + int index = (blockDim.x * blockIdx.x) + threadIdx.x; + if (index >= n) return; + + if (bools[index]) { + odata[indices[index]] = idata[index]; + } + } } diff --git a/Project2-Stream-Compaction/stream_compaction/cpu.cu b/Project2-Stream-Compaction/stream_compaction/cpu.cu index a2d3e6c..34a1de9 100644 --- a/Project2-Stream-Compaction/stream_compaction/cpu.cu +++ b/Project2-Stream-Compaction/stream_compaction/cpu.cu @@ -18,9 +18,31 @@ namespace StreamCompaction { * (Optional) For better understanding before starting moving to GPU, you can simulate your GPU scan in this function first. */ void scan(int n, int *odata, const int *idata) { - timer().startCpuTimer(); - // TODO - timer().endCpuTimer(); + bool exception = false; + try { + timer().startCpuTimer(); + } + catch (const std::runtime_error& ex) { + exception = true; + } + + + if (n <= 0) return; + if (n == 1) { + odata[0] = 0; + return; + } + odata[0] = 0; + odata[1] = idata[0]; + + + for (int i = 2; i < n; i++) { + odata[i] = odata[i - 1] + idata[i - 1]; + } + + if(!exception) + timer().endCpuTimer(); + } /** @@ -30,9 +52,17 @@ 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]; + k++; + } + } + timer().endCpuTimer(); - return -1; + return k; } /** @@ -42,9 +72,25 @@ namespace StreamCompaction { */ int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO + + int *checkNonZero = new int[n]; + for (int i = 0; i < n; i++) { + if (idata[i] != 0) checkNonZero[i] = 1; + else checkNonZero[i] = 0; + } + + int *prefixSum = new int[n]; + scan(n, prefixSum, checkNonZero); + + for (int i = 0; i < n; i++) { + if (checkNonZero[i] != 0) { + odata[prefixSum[i]] = idata[i]; + } + } + + int count = checkNonZero[n - 1] == 0 ? prefixSum[n - 1] : prefixSum[n - 1] + 1; timer().endCpuTimer(); - return -1; + return count; } } } diff --git a/Project2-Stream-Compaction/stream_compaction/efficient.cu b/Project2-Stream-Compaction/stream_compaction/efficient.cu index 2db346e..5d1df68 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.cu +++ b/Project2-Stream-Compaction/stream_compaction/efficient.cu @@ -3,6 +3,8 @@ #include "common.h" #include "efficient.h" +#define checkCUDAErrorWithLine(msg) checkCUDAError(msg) + namespace StreamCompaction { namespace Efficient { using StreamCompaction::Common::PerformanceTimer; @@ -12,13 +14,106 @@ namespace StreamCompaction { return timer; } + __global__ void resetZeros(int n, int *a) { + int index = (blockDim.x*blockIdx.x) + threadIdx.x; + if (index >= n) return; + a[index] = 0; + } + + + __global__ void upSweep(int n, int d, int *idata) { + int index = (blockDim.x*blockIdx.x) + threadIdx.x; + + int twoPowd1 = 1 << (d + 1); + int twoPowd = 1 << d; + + + if ((index % twoPowd1 != twoPowd1-1) || index >= n) return; + + int k = index - twoPowd1 + 1; + idata[index] += idata[k + twoPowd - 1]; + } + + __global__ void downSweep(int n, int d, int *idata) { + int index = (blockDim.x*blockIdx.x) + threadIdx.x; + + int twoPowd1 = 1 << (d + 1); + int twoPowd = 1 << d; + + + if ((index % twoPowd1 != twoPowd1 - 1) || index >= n) return; + + int k = index - twoPowd1 + 1; + int t = idata[k + twoPowd - 1]; + idata[k + twoPowd - 1] = idata[index]; + idata[index] += t; + } + + void printxxx(int n, const int *a) { + for (int i = 0; i < n; i++) { + printf("%d ", a[i]); + } + printf("\n\n\n"); + } + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); - // TODO - timer().endGpuTimer(); + bool exception = false; + try { + timer().startGpuTimer(); + } catch (const std::runtime_error& ex) { + exception = true; + } + + int *dev_idata; + + int numThreads = 128; + int numBlocks = (n + numThreads - 1) / numThreads; + + int d_max = ilog2ceil(n); + + int twoPowN = 1 << d_max; + if (n != twoPowN) { + + int diff = twoPowN - n; + + cudaMalloc((void **)&dev_idata, (n + diff) * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata1 failed!"); + + resetZeros << > > (n + diff, dev_idata); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + n = n + diff; + } else { + cudaMalloc((void **)&dev_idata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + } + + for (int d = 0; d < d_max; d++) { + upSweep<<>>(n, d, dev_idata); + } + + // reset last element to zero + int* zero = new int[1]; + zero[0] = 0; + cudaMemcpy(dev_idata + n - 1, zero, sizeof(int), cudaMemcpyHostToDevice); + + + for(int d = d_max-1; d >= 0; d--) { + downSweep << > > (n, d, dev_idata); + } + + + cudaMemcpy(odata, dev_idata, n * sizeof(int), cudaMemcpyDeviceToHost); + + cudaFree(dev_idata); + + if(!exception) + timer().endGpuTimer(); } /** @@ -32,9 +127,57 @@ namespace StreamCompaction { */ int compact(int n, int *odata, const int *idata) { timer().startGpuTimer(); - // TODO + + int numThreads = 128; + int numBlocks = (n + numThreads - 1) / numThreads; + + int *dev_checkZeros, *dev_sumIndices, *dev_odata, *dev_idata; + + cudaMalloc((void **) &dev_checkZeros, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_checkZeros failed!"); + cudaMalloc((void **) &dev_sumIndices, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_sumIndices failed!"); + cudaMalloc((void **)&dev_odata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata failed!"); + cudaMalloc((void **)&dev_idata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + StreamCompaction::Common::kernMapToBoolean<<>>(n, dev_checkZeros, dev_idata); + + int *checkZeros = new int[n]; + cudaMemcpy(checkZeros, dev_checkZeros, n * sizeof(int), cudaMemcpyDeviceToHost); + + //printxxx(n, checkZeros); + + int *sumIndices = new int[n]; + scan(n, sumIndices, checkZeros); + + cudaMemcpy(dev_sumIndices, sumIndices , n * sizeof(int), cudaMemcpyHostToDevice); + + StreamCompaction::Common::kernScatter<<>>(n, dev_odata, dev_idata, dev_checkZeros, dev_sumIndices); + + + + cudaMemcpy(odata, dev_odata, n * sizeof(int), cudaMemcpyDeviceToHost); + + + + int count = checkZeros[n - 1] == 0 ? sumIndices[n - 1] : sumIndices[n - 1] + 1; + + //delete[] checkZeros; + //delete[] sumIndices; + + //printf("hey\n"); + + cudaFree(dev_idata); + cudaFree(dev_odata); + cudaFree(dev_checkZeros); + cudaFree(dev_sumIndices); + timer().endGpuTimer(); - return -1; + return count; } } } diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index 4308876..35e8d38 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 checkCUDAErrorWithLine(msg) checkCUDAError(msg) + namespace StreamCompaction { namespace Naive { using StreamCompaction::Common::PerformanceTimer; @@ -13,12 +15,77 @@ namespace StreamCompaction { } // TODO: __global__ + __global__ void prefixSum(int n, int d, int *odata, const int *idata) { + int index = (blockDim.x*blockIdx.x) + threadIdx.x; + + int min_k = 1 << (d-1); + if (index < min_k || index >= n) return; + + odata[index] = idata[index - min_k] + idata[index]; + } + + __global__ void shiftRight(int n, int *odata, const int *idata) { + int index = (blockDim.x*blockIdx.x) + threadIdx.x; + + if (index >= n) return; + + if (index == 0) { + odata[index] = 0; + return; + } + + odata[index] = idata[index-1]; + } + + void printxxx(int n, const int *a) { + for (int i = 0; i < n; i++) { + printf("%d ", a[i]); + } + printf("\n\n\n"); + } + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { timer().startGpuTimer(); - // TODO + + int numThreads = 128; + int numBlocks = (n + numThreads - 1) / numThreads; + + int d_max = ilog2ceil(n); + + int *dev_idata, *dev_odata1, *dev_odata2; + cudaMalloc((void **)&dev_idata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMalloc((void **)&dev_odata1, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata1 failed!"); + + cudaMalloc((void **)&dev_odata2, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata2 failed!"); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + cudaMemcpy(dev_odata1, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + int *out1 = dev_idata; + int *out2 = dev_odata1; + + + for (int d = 1; d <= d_max; d++) { + prefixSum<<>>(n, d, out2, out1); + cudaMemcpy(out1, out2, n * sizeof(int), cudaMemcpyDeviceToDevice); + } + + shiftRight<<>>(n, out2, out1); + + cudaMemcpy(odata, out2, n * sizeof(int), cudaMemcpyDeviceToHost); + + + cudaFree(dev_idata); + cudaFree(dev_odata1); + cudaFree(dev_odata2); + timer().endGpuTimer(); } } From 3dffcd1bac18d50af0a506bc9f632b52978c75ac Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Sat, 14 Sep 2019 02:47:22 -0400 Subject: [PATCH 02/26] change main back to 8 size. --- Project2-Stream-Compaction/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/src/main.cpp b/Project2-Stream-Compaction/src/main.cpp index fd98f07..d016553 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 << 3; // feel free to change the size of array +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]; From 0c985e3e760c55668139a9dc0bae5efa15f0e90c Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Sat, 14 Sep 2019 21:14:15 -0400 Subject: [PATCH 03/26] thrust done. --- .../stream_compaction/thrust.cu | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Project2-Stream-Compaction/stream_compaction/thrust.cu b/Project2-Stream-Compaction/stream_compaction/thrust.cu index 1def45e..f18f724 100644 --- a/Project2-Stream-Compaction/stream_compaction/thrust.cu +++ b/Project2-Stream-Compaction/stream_compaction/thrust.cu @@ -6,6 +6,8 @@ #include "common.h" #include "thrust.h" +#define checkCUDAErrorWithLine(msg) checkCUDAError(msg) + namespace StreamCompaction { namespace Thrust { using StreamCompaction::Common::PerformanceTimer; @@ -22,6 +24,23 @@ 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()); + + int *dev_idata, *dev_odata; + cudaMalloc((void **)&dev_odata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata failed!"); + cudaMalloc((void **)&dev_idata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + thrust::device_ptr dev_idataItr(dev_idata); + thrust::device_ptr dev_odataItr(dev_odata); + + thrust::exclusive_scan(dev_idataItr, dev_idataItr + n, dev_odataItr); + + cudaMemcpy(odata, dev_odata, n * sizeof(int), cudaMemcpyDeviceToHost); + + timer().endGpuTimer(); } } From a1c360939d3e85cb94ebe193d6a9317f56aa33af Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Sun, 15 Sep 2019 01:10:58 -0400 Subject: [PATCH 04/26] matrix multiply update and outer framework setup. --- .../character_recognition/CMakeLists.txt | 2 +- .../character_recognition/mlp.cu | 76 ++++++++ .../character_recognition/mlp.h | 3 + Project2-Character-Recognition/src/main.cpp | 169 +++++------------- 4 files changed, 127 insertions(+), 123 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-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index 5a3ed7f..f629ab4 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -3,6 +3,10 @@ #include "common.h" #include "mlp.h" +#define blockSize 128 + +#define checkCUDAErrorWithLine(msg) checkCUDAError(msg) + namespace CharacterRecognition { using Common::PerformanceTimer; PerformanceTimer& timer() @@ -22,6 +26,78 @@ namespace CharacterRecognition { timer().endGpuTimer(); } */ + __global__ void kernMatrixMultiply(float *A, float *B, float *C, int m, int n, int k) { + /* + A -> m X n and B is n X k + C -> m X k the output array + */ + + int row = (blockDim.y * blockIdx.y) + threadIdx.y; + int col = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (row >= m || col >= k) return; + + float sum = 0.0; + for (int i = 0; i < n; i++) { + sum += A[row*n + i] * B[i*k + col]; + } + C[row*k + col] = sum; + } + + + void matrixMultiply(float *A, float *B, float *C, int m, int n, int k) { + + float *dev_A, *dev_B, *dev_C; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + cudaMalloc((void **)&dev_B, n * k * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + cudaMalloc((void **)&dev_C, m * k * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_B, B, n * k * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (k + blockSize - 1) / blockSize; + + dim3 dimGrid(gridRows, gridCols); + dim3 dimBlock(blockSize, blockSize); + + kernMatrixMultiply << > > (dev_A, dev_B, dev_C, m, n, k); + + cudaMemcpy(C, dev_C, m * k * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_B); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_C); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + + + + + void printArray2D(float *X, int nR, int nC) { + for (int i = 0; i < nR; i++) { + for (int j = 0; j < nC; j++) + printf("%.2f ", X[i*nC + j]); + printf("\n"); + } + } // TODO: implement required elements for MLP sections 1 and 2 here + + void forwardPass(int N, int d, int C, int h1, float *X, int *y, float *loss, float *W1, float *W2) { + + float *X2 = new float[N * h1 * sizeof(float)]; + matrixMultiply(X, W1, X2, N, d, h1); + + printArray2D(X2, N, h1); + + } + + } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index 2096228..d8cfeab 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -6,4 +6,7 @@ namespace CharacterRecognition { Common::PerformanceTimer& timer(); // TODO: implement required elements for MLP sections 1 and 2 here + + void forwardPass(int N, int d, int C, int h1, float *X, int *y, float *loss, float *W1, float *W2); + void matrixMultiply(float *A, float *B, float *C, int m, int n, int k); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index 11dd534..c414c7f 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -10,6 +10,7 @@ #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 @@ -17,136 +18,60 @@ int *a = new int[SIZE]; int *b = new int[SIZE]; int *c = new int[SIZE]; -int main(int argc, char* argv[]) { - // Scan tests - - printf("\n"); - printf("****************\n"); - printf("** SCAN 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); - - 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); - - 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); - - /* 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"); +void printArray2D(float *X, int nR, int nC) { + for (int i = 0; i < nR; i++) { + for (int j = 0; j < nC; j++) + printf("%.2f ", X[i*nC + j]); + printf("\n"); + } +} - // Compaction tests +void fillInputXOR(float *X, int *y) { + X[0] = 0.0, X[1] = 0.0; + X[2] = 0.0, X[3] = 1.0; + X[4] = 1.0, X[5] = 0.0; + X[6] = 1.0, X[7] = 1.0; - genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case - a[SIZE - 1] = 0; - printArray(SIZE, a, true); + y[0] = 0; + y[1] = 1; + y[2] = 1; + y[3] = 0; +} - int count, expectedCount, expectedNPOT; +void generateRandomWeights(float *W, int nR, int nC) { + std::random_device rd; //Will be used to obtain a seed for the random number engine + std::mt19937 gen(rd()); + std::uniform_real_distribution<> dis(0.0, 1.0); + for (int i = 0; i < nR; i++) { + for (int j = 0; j < nC; j++) + W[i*nC + j] = dis(gen); + } +} - // 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); +int main(int argc, char* argv[]) { + // Scan tests - 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); + int N = 4; + int d = 2; + int C = 2; + int h1 = 2; - 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); + float *X = new float[N * d * sizeof(float)]; + int *y = new int[N * 1 * sizeof(int)]; + float *W1 = new float[d * h1 * sizeof(float)]; + float *W2 = new float[h1 * C * sizeof(float)]; - 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); + fillInputXOR(X, y); + generateRandomWeights(W1, d, h1); + generateRandomWeights(W2, h1, 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); + float *X2 = new float[N * h1 * sizeof(float)]; + CharacterRecognition::matrixMultiply(X, W1, X2, N, d, h1); - system("pause"); // stop Win32 console from closing on exit - delete[] a; - delete[] b; - delete[] c; + printArray2D(X, N, d); + printf("\n"); + printArray2D(W1, d, h1); + printf("\n"); + printArray2D(W2, h1, C); } From b0c44cbe26283f84aa12d28a79a5fbf85f9a5cee Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Mon, 16 Sep 2019 00:34:39 -0400 Subject: [PATCH 05/26] XOR complete. --- .../character_recognition/mlp.cu | 463 +++++++++++++++++- .../character_recognition/mlp.h | 2 +- Project2-Character-Recognition/src/main.cpp | 44 +- 3 files changed, 486 insertions(+), 23 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index f629ab4..0026403 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -3,7 +3,7 @@ #include "common.h" #include "mlp.h" -#define blockSize 128 +#define blockSize 32 #define checkCUDAErrorWithLine(msg) checkCUDAError(msg) @@ -26,6 +26,71 @@ namespace CharacterRecognition { timer().endGpuTimer(); } */ + + __global__ void kernExp(float *A, int m, int n) { + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= m || tx >= n) return; + + A[ty*n + tx] = exp(A[ty*n + tx]); + } + + __global__ void kernReLU(float *A, int m, int n) { + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= m || tx >= n) return; + + if (A[ty*n + tx] < 0) A[ty*n + tx] = 0.0; + } + + __global__ void kernDerivativeReLU(float *A, int m, int n) { + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= m || tx >= n) return; + + if (A[ty*n + tx] > 0) { + A[ty*n + tx] = 1.0; + } + else { + A[ty*n + tx] = 0.0; + } + } + + __global__ void kernMax(float *A, float *max, int N, int C) { + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (tx >= N) return; + + int maxIndex = 0; + float maxVal = A[tx*N + 0]; + for (int i = 1; i < C; i++) { + float val = A[tx*N + i]; + if (val > maxVal) { + maxVal = val; + maxIndex = i; + } + } + + max[tx] = maxIndex; + } + + __global__ void kernMatrixTranspose(float* A, float* B, int m, int n) + { + int tx = blockIdx.x * blockDim.x + threadIdx.x; + int ty = blockIdx.y * blockDim.y + threadIdx.y; + + if (tx >= n || ty >= m) return; + + + int idx = ty * n + tx; + int transIdx = tx * m + ty; + B[transIdx] = A[idx]; + + } + __global__ void kernMatrixMultiply(float *A, float *B, float *C, int m, int n, int k) { /* A -> m X n and B is n X k @@ -41,9 +106,186 @@ namespace CharacterRecognition { for (int i = 0; i < n; i++) { sum += A[row*n + i] * B[i*k + col]; } + C[row*k + col] = sum; } + __global__ void kernElementMatrixMultiply(float *A, float *B, float *C, int m, int n) { + /* + A -> m X n and B is n X k + C -> m X k the output array + */ + + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= m || tx >= n) return; + + C[ty*n + tx] = A[ty*n + tx] * B[ty*n + tx]; + + } + + __global__ void kernElementMatrixAdd(float *A, float *B, float *C, float alpha, int m, int n) { + /* + A -> m X n and B is n X k + C -> m X k the output array + */ + + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= m || tx >= n) return; + + C[ty*n + tx] = A[ty*n + tx] - alpha * B[ty*n + tx]; + + } + + __global__ void kernDerivativeLossScores(float *probs, int *y, float *dscores, int N, int C) { + int ty = (blockDim.y * blockIdx.y) + threadIdx.y; + int tx = (blockDim.x * blockIdx.x) + threadIdx.x; + + if (ty >= N || tx >= C) return; + + if (tx == y[ty]) { + dscores[ty*C + tx] = probs[ty*C + tx] - 1; + dscores[ty*C + tx] /= N; + } else { + dscores[ty*C + tx] /= N; + } + + } + + void softmaxExp(float *A, int m, int n) { + float *dev_A; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + + kernExp << > > (dev_A, m, n); + + + cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + void softmaxNormalize(float *A, int m, int n) { + // TODO: Should be parallelized + + for (int i = 0; i < m; i++) { + float sum = 0; + for (int j = 0; j < n; j++) + sum += A[i*n + j]; + + for (int j = 0; j < n; j++) + A[i*n + j] /= sum; + } + } + + void calculateLoss(float *probs, int N, int C, int *y, float* loss) { + // TODO: Should be parallelized + float totalLoss = 0; + for (int i = 0; i < N; i++) { + int label = y[i]; + totalLoss += -log(probs[i*C + label]); + } + + totalLoss /= N; + + *loss = totalLoss; + } + + + + + void softmaxDerivativeWrtScores(float *probs, int *y, float *dscores, int N, int C) { + /* + Calcluates dL/dscores . probs = softmax(scores) + dL/dscores = probs[range(N), y] -= 1 + */ + + float *dev_dscores, *dev_probs; + cudaMalloc((void **)&dev_dscores, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_dscores failed!"); + cudaMalloc((void **)&dev_probs, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_probs failed!"); + + int *dev_y; + cudaMalloc((void **)&dev_y, N * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_y failed!"); + + cudaMemcpy(dev_dscores, probs, N * C * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_probs, probs, N * C * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_y, y, N * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (N + blockSize - 1) / blockSize; + int gridCols = (C + blockSize - 1) / blockSize; + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + kernDerivativeLossScores<<>>(dev_probs, dev_y, dev_dscores, N, C); + + cudaMemcpy(dscores, dev_dscores, N * C * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_dscores); + checkCUDAErrorWithLine("cudaFree dev_dscores failed!"); + cudaFree(dev_probs); + checkCUDAErrorWithLine("cudaFree dev_probs failed!"); + cudaFree(dev_y); + checkCUDAErrorWithLine("cudaFree dev_y failed!"); + + } + + void reLU(float *A, int m, int n) { + float *dev_A; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + + kernReLU<<>>(dev_A, m, n); + + + cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + void derivativeReLU(float *A, int m, int n) { + float *dev_A; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + + kernDerivativeReLU << > > (dev_A, m, n); + + + cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } void matrixMultiply(float *A, float *B, float *C, int m, int n, int k) { @@ -61,7 +303,7 @@ namespace CharacterRecognition { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (k + blockSize - 1) / blockSize; - dim3 dimGrid(gridRows, gridCols); + dim3 dimGrid(gridCols, gridRows); dim3 dimBlock(blockSize, blockSize); kernMatrixMultiply << > > (dev_A, dev_B, dev_C, m, n, k); @@ -76,26 +318,235 @@ namespace CharacterRecognition { checkCUDAErrorWithLine("cudaFree dev_A failed!"); } + void matrixElementMultiply(float *A, float *B, float *C, int m, int n) { + + float *dev_A, *dev_B, *dev_C; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + cudaMalloc((void **)&dev_B, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + cudaMalloc((void **)&dev_C, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_B, B, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + + kernElementMatrixMultiply << > > (dev_A, dev_B, dev_C, m, n); + + cudaMemcpy(C, dev_C, m * n * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_B); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_C); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + void updateWeights(float *A, float *B, float *C, float alpha, int m, int n) { + + float *dev_A, *dev_B, *dev_C; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + cudaMalloc((void **)&dev_B, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + cudaMalloc((void **)&dev_C, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_B, B, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + kernElementMatrixAdd << > > (dev_A, dev_B, dev_C, alpha, m, n); + + cudaMemcpy(C, dev_C, m * n * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_B); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_C); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + void matrixTranspose(float *A, float *B, int m, int n) { + + float *dev_A, *dev_B; + cudaMalloc((void **)&dev_A, m * n * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); + cudaMalloc((void **)&dev_B, n * m * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); + + cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; + + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); + + kernMatrixTranspose << > > (dev_A, dev_B, m, n); + + cudaMemcpy(B, dev_B, n * m * sizeof(float), cudaMemcpyDeviceToHost); + + cudaFree(dev_A); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + cudaFree(dev_B); + checkCUDAErrorWithLine("cudaFree dev_A failed!"); + } + + + void calculateDerviativeW2(int N, int d, int C, int h1, float *dscores, float *X2, float *dW2) { + float *X2Trans = new float[h1 * N * sizeof(float)]; + matrixTranspose(X2, X2Trans, N, h1); + + + // dL/dW2 = X2.T * dscores (h1xN X NxC = h1xC) + matrixMultiply(X2Trans, dscores, dW2, h1, N, C); + + delete[] X2Trans; + } + + void calculateDerviativeW1(int N, int d, int C, int h1, float *X, float *fc, float *W2, float *dscores, float *dW1) { + float *dW1_1 = new float[N * h1 * sizeof(float)]; + float *W2Trans = new float[C * h1 * sizeof(float)]; + matrixTranspose(W2, W2Trans, h1, C); + + // dW1_1 = dscores * W2.T + matrixMultiply(dscores, W2Trans, dW1_1, N, C, h1); + + float *dfcRelu = new float[N * h1 * sizeof(float)]; + dfcRelu = (float *)memcpy((void *)dfcRelu, (void *)fc, N * h1 * sizeof(float)); + derivativeReLU(dfcRelu, N, h1); + + float *dfc = new float[N * h1 * sizeof(float)]; + matrixElementMultiply(dW1_1, dfcRelu, dfc, N, h1); + + float *XTrans = new float[d * N * sizeof(float)]; + matrixTranspose(X, XTrans, N, d); + + matrixMultiply(XTrans, dfc, dW1, d, N, h1); + + delete[] dW1_1; + delete[] W2Trans; + delete[] dfc; + delete[] dfcRelu; + delete[] XTrans; + } + + void predict(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2, int* predictions) { + int *dev_pred; + cudaMalloc((void **)&dev_pred, N * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_pred failed!"); + + + } void printArray2D(float *X, int nR, int nC) { for (int i = 0; i < nR; i++) { for (int j = 0; j < nC; j++) - printf("%.2f ", X[i*nC + j]); + printf("%.4f ", X[i*nC + j]); printf("\n"); } + printf("\n"); } // TODO: implement required elements for MLP sections 1 and 2 here - void forwardPass(int N, int d, int C, int h1, float *X, int *y, float *loss, float *W1, float *W2) { + void trainStep(int N, int d, int C, int h1, float alpha, float *X, int *y, float *loss, float *W1, float *W2) { + + /* + X -> N x d + y -> N x 1 + W1 -> d x h1 + W2 -> h1 x C + */ + //printf("W1\n"); + //printArray2D(W1, d, h1); + //printf("W2\n"); + //printArray2D(W2, h1, C); + + // first fully connected layer X2 = X*W1 + float *fc = new float[N * h1 * sizeof(float)]; + matrixMultiply(X, W1, fc, N, d, h1); + //printf("FC:\n"); + //printArray2D(fc, N, h1); + + // Apply ReLU activation: X2 = ReLU(X2); float *X2 = new float[N * h1 * sizeof(float)]; - matrixMultiply(X, W1, X2, N, d, h1); + X2 = (float *) memcpy((void *)X2, (void *)fc, N * h1 * sizeof(float)); + reLU(X2, N, h1); + //printf("ReLU:\n"); + //printArray2D(X2, N, h1); + + // calculate log_scores for softmax + float *scores = new float[N * C * sizeof(float)]; + matrixMultiply(X2, W2, scores, N, h1, C); + //printf("Log scores:\n"); + //printArray2D(scores, N, C); + + // calculate softmax probability: apply exp on all elements and normalize by sum of columns + softmaxExp(scores, N, C); + softmaxNormalize(scores, N, C); + //printf("Softmax probabilities:\n"); + //printArray2D(scores, N, C); + + // calculate the loss + calculateLoss(scores, N, C, y, loss); + printf("Loss: %.4f\n", *loss); + + + // **** BACKPROPAGATION STARTS ***** + // dL/dscores + float *dscores = new float[N * C * sizeof(float)]; + softmaxDerivativeWrtScores(scores, y, dscores, N, C); + + //printf("dL/dscores\n"); + //printArray2D(dscores, N, C); + + float *dW2 = new float[h1 * C * sizeof(float)]; + calculateDerviativeW2(N, d, C, h1, dscores, X2, dW2); + + //printf("dW2\n"); + //printArray2D(dW2, h1, C); + + float *dW1 = new float[d * h1 * sizeof(float)]; + calculateDerviativeW1(N, d, C, h1, X, fc, W2, dscores, dW1); + + //printf("dW1\n"); + //printArray2D(dW1, d, h1); + + updateWeights(W1, dW1, W1, alpha, d, h1); + //printf("W1\n"); + //printArray2D(W1, d, h1); + + updateWeights(W2, dW2, W2, alpha, h1, C); + //printf("W2\n"); + //printArray2D(W2, h1, C); + + delete[] X2; + delete[] scores; + delete[] dscores; + delete[] dW1; + delete[] dW2; + delete[] fc; + + } - printArray2D(X2, N, h1); + void predictAndAcc(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2) { } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index d8cfeab..496b646 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -7,6 +7,6 @@ namespace CharacterRecognition { // TODO: implement required elements for MLP sections 1 and 2 here - void forwardPass(int N, int d, int C, int h1, float *X, int *y, float *loss, float *W1, float *W2); + void trainStep(int N, int d, int C, int h1, float alpha, float *X, int *y, float *loss, float *W1, float *W2); void matrixMultiply(float *A, float *B, float *C, int m, int n, int k); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index c414c7f..4272d23 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -12,11 +12,9 @@ #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]; +std::random_device rd; //Will be used to obtain a seed for the random number engine +std::mt19937 gen(rd()); +std::uniform_real_distribution<> dis(-1.0, 1.0); void printArray2D(float *X, int nR, int nC) { @@ -28,10 +26,10 @@ void printArray2D(float *X, int nR, int nC) { } void fillInputXOR(float *X, int *y) { - X[0] = 0.0, X[1] = 0.0; - X[2] = 0.0, X[3] = 1.0; - X[4] = 1.0, X[5] = 0.0; - X[6] = 1.0, X[7] = 1.0; + X[0] = 0.0, X[1] = 0.0, X[2] = 1.0; + X[3] = 0.0, X[4] = 1.0, X[5] = 1.0; + X[6] = 1.0, X[7] = 0.0, X[8] = 1.0; + X[9] = 1.0, X[10] = 1.0; X[11] = 1.0; y[0] = 0; y[1] = 1; @@ -40,9 +38,6 @@ void fillInputXOR(float *X, int *y) { } void generateRandomWeights(float *W, int nR, int nC) { - std::random_device rd; //Will be used to obtain a seed for the random number engine - std::mt19937 gen(rd()); - std::uniform_real_distribution<> dis(0.0, 1.0); for (int i = 0; i < nR; i++) { for (int j = 0; j < nC; j++) W[i*nC + j] = dis(gen); @@ -53,25 +48,42 @@ int main(int argc, char* argv[]) { // Scan tests int N = 4; - int d = 2; + int d = 3; int C = 2; - int h1 = 2; + int h1 = 4; float *X = new float[N * d * sizeof(float)]; int *y = new int[N * 1 * sizeof(int)]; float *W1 = new float[d * h1 * sizeof(float)]; float *W2 = new float[h1 * C * sizeof(float)]; + float loss_val = 0.0; + float *loss = &loss_val; + + float alpha = 0.5; fillInputXOR(X, y); generateRandomWeights(W1, d, h1); generateRandomWeights(W2, h1, C); - float *X2 = new float[N * h1 * sizeof(float)]; - CharacterRecognition::matrixMultiply(X, W1, X2, N, d, h1); + printf("X:\n"); printArray2D(X, N, d); printf("\n"); + printf("W1:\n"); printArray2D(W1, d, h1); printf("\n"); + printf("W2:\n"); printArray2D(W2, h1, C); + printf("\n"); + + for (int i = 1; i <= 1000; i++) { + printf("\n\nIteration %d\n\n", i); + CharacterRecognition::trainStep(N, d, C, h1, alpha, X, y, loss, W1, W2); + + } + + delete[] X; + delete[] y; + delete[] W1; + delete[] W2; } From 89b2e480868499fae8b5a9a243727ac1160064d7 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Mon, 16 Sep 2019 02:38:50 -0400 Subject: [PATCH 06/26] image learning complete. --- .../character_recognition/mlp.cu | 83 +++++++++-- .../character_recognition/mlp.h | 2 +- Project2-Character-Recognition/src/main.cpp | 137 +++++++++++++++--- 3 files changed, 191 insertions(+), 31 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index 0026403..a4b58dd 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -14,6 +14,15 @@ namespace CharacterRecognition { static PerformanceTimer timer; return timer; } + + void printArray2D(float *X, int nR, int nC) { + for (int i = 0; i < nR; i++) { + for (int j = 0; j < nC; j++) + printf("%.4f ", X[i*nC + j]); + printf("\n"); + } + printf("\n"); + } // TODO: __global__ @@ -59,15 +68,15 @@ namespace CharacterRecognition { } } - __global__ void kernMax(float *A, float *max, int N, int C) { + __global__ void kernMax(float *A, int *max, int N, int C) { int tx = (blockDim.x * blockIdx.x) + threadIdx.x; if (tx >= N) return; int maxIndex = 0; - float maxVal = A[tx*N + 0]; + float maxVal = A[tx*C + 0]; for (int i = 1; i < C; i++) { - float val = A[tx*N + i]; + float val = A[tx*C + i]; if (val > maxVal) { maxVal = val; maxIndex = i; @@ -446,23 +455,48 @@ namespace CharacterRecognition { } void predict(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2, int* predictions) { + + float *fc = new float[N * h1 * sizeof(float)]; + matrixMultiply(X, W1, fc, N, d, h1); + + // Apply ReLU activation: X2 = ReLU(X2); + float *X2 = new float[N * h1 * sizeof(float)]; + X2 = (float *)memcpy((void *)X2, (void *)fc, N * h1 * sizeof(float)); + reLU(X2, N, h1); + + // calculate log_scores for softmax + float *scores = new float[N * C * sizeof(float)]; + matrixMultiply(X2, W2, scores, N, h1, C); + printArray2D(scores, N, C); + + float *dev_scores; + cudaMalloc((void **)&dev_scores, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc dev_scores failed!"); int *dev_pred; cudaMalloc((void **)&dev_pred, N * sizeof(int)); checkCUDAErrorWithLine("cudaMalloc dev_pred failed!"); + cudaMemcpy(dev_scores, scores, N * C * sizeof(float), cudaMemcpyHostToDevice); - } + int numBlocks = (N + blockSize - 1) / blockSize; + kernMax << > > (dev_scores, dev_pred, N, C); - void printArray2D(float *X, int nR, int nC) { - for (int i = 0; i < nR; i++) { - for (int j = 0; j < nC; j++) - printf("%.4f ", X[i*nC + j]); - printf("\n"); - } - printf("\n"); + cudaMemcpy(predictions, dev_pred, N * sizeof(int), cudaMemcpyDeviceToHost); + + delete[] fc; + delete[] X2; + delete[] scores; + cudaFree(dev_scores); + checkCUDAErrorWithLine("cudaFree dev_scores failed!"); + cudaFree(dev_pred); + checkCUDAErrorWithLine("cudaFree dev_pred failed!"); + } + + + // TODO: implement required elements for MLP sections 1 and 2 here void trainStep(int N, int d, int C, int h1, float alpha, float *X, int *y, float *loss, float *W1, float *W2) { @@ -498,6 +532,19 @@ namespace CharacterRecognition { //printf("Log scores:\n"); //printArray2D(scores, N, C); + //for (int i = 0; i < N; i++) { + // float max = scores[i*C + 0]; + // for (int j = 1; j < C; j++) { + // if (scores[i*C + j] > max) max = scores[i*C + j]; + // } + // + // for (int j = 0; j < C; j++) { + // scores[i*C + j] -= max; + // } + //} + + //printArray2D(scores, N, C); + // calculate softmax probability: apply exp on all elements and normalize by sum of columns softmaxExp(scores, N, C); softmaxNormalize(scores, N, C); @@ -548,6 +595,20 @@ namespace CharacterRecognition { void predictAndAcc(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2) { + int* predictions = new int[N]; + predict(N, d, C, h1, X, y, W1, W2, predictions); + + for (int i = 0; i < N; i++) { + printf("Predictions for %d example is %d\n", i + 1, predictions[i]); + } + + float accuracy = 0.0; + for (int i = 0; i < N; i++) { + accuracy += (predictions[i] == y[i]); + } + accuracy /= N; + + printf("\n\nAccuracy is %.4f\n\n", accuracy); } diff --git a/Project2-Character-Recognition/character_recognition/mlp.h b/Project2-Character-Recognition/character_recognition/mlp.h index 496b646..812dfc0 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.h +++ b/Project2-Character-Recognition/character_recognition/mlp.h @@ -8,5 +8,5 @@ namespace CharacterRecognition { // TODO: implement required elements for MLP sections 1 and 2 here void trainStep(int N, int d, int C, int h1, float alpha, float *X, int *y, float *loss, float *W1, float *W2); - void matrixMultiply(float *A, float *B, float *C, int m, int n, int k); + void predictAndAcc(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index 4272d23..27e82d6 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -11,10 +11,14 @@ #include #include "testing_helpers.hpp" #include +#include +#include +#include std::random_device rd; //Will be used to obtain a seed for the random number engine std::mt19937 gen(rd()); -std::uniform_real_distribution<> dis(-1.0, 1.0); +std::uniform_real_distribution<> dis(-0.01, 0.01); + void printArray2D(float *X, int nR, int nC) { @@ -25,6 +29,14 @@ void printArray2D(float *X, int nR, int nC) { } } +void printArray2D(int *X, int nR, int nC) { + for (int i = 0; i < nR; i++) { + for (int j = 0; j < nC; j++) + printf("%d ", X[i*nC + j]); + printf("\n"); + } +} + void fillInputXOR(float *X, int *y) { X[0] = 0.0, X[1] = 0.0, X[2] = 1.0; X[3] = 0.0, X[4] = 1.0, X[5] = 1.0; @@ -37,6 +49,45 @@ void fillInputXOR(float *X, int *y) { y[3] = 0; } + +void fillImage(float *X, int *y) { + + int j = 0; + for (int i = 1; i <= 52; i++) { + std::string fileName; + if (i <= 9) { + fileName = "C:\\\\Users\\sri07\\Desktop\\Project2-Number-Algorithms\\Project2-Character-Recognition\\data-set\\0" + std::to_string(i) + "info.txt"; + } + else { + fileName = "C:\\\\Users\\sri07\\Desktop\\Project2-Number-Algorithms\\Project2-Character-Recognition\\data-set\\" + std::to_string(i) + "info.txt"; + } + + std::ifstream myfile(fileName); + std::string line; + //std::cout << fileName << '\n'; + if (myfile.is_open()) + { + std::getline(myfile, line); + y[i - 1] = std::stoi(line) - 1 ; + + std::getline(myfile, line); + + + std::getline(myfile, line); + std::string buf; + std::stringstream ss(line); + while (ss >> buf) { + //std::cout << j << '\n'; + X[j++] = ((float)std::stoi(buf))/255.0; + } + + + myfile.close(); + } + } + +} + void generateRandomWeights(float *W, int nR, int nC) { for (int i = 0; i < nR; i++) { for (int j = 0; j < nC; j++) @@ -47,41 +98,89 @@ void generateRandomWeights(float *W, int nR, int nC) { int main(int argc, char* argv[]) { // Scan tests - int N = 4; - int d = 3; - int C = 2; - int h1 = 4; + //int N = 4; + //int d = 3; + //int C = 2; + //int h1 = 4; + + //float *X = new float[N * d * sizeof(float)]; + //int *y = new int[N * 1 * sizeof(int)]; + //float *W1 = new float[d * h1 * sizeof(float)]; + //float *W2 = new float[h1 * C * sizeof(float)]; + //float loss_val = 0.0; + //float *loss = &loss_val; + + //float alpha = 0.5; + + //fillInputXOR(X, y); + //generateRandomWeights(W1, d, h1); + //generateRandomWeights(W2, h1, C); + + + //printf("X:\n"); + //printArray2D(X, N, d); + //printf("\n"); + //printf("W1:\n"); + //printArray2D(W1, d, h1); + //printf("\n"); + //printf("W2:\n"); + //printArray2D(W2, h1, C); + //printf("\n"); + + //for (int i = 1; i <= 1000; i++) { + // printf("\n\nIteration %d\n\n", i); + // CharacterRecognition::trainStep(N, d, C, h1, alpha, X, y, loss, W1, W2); + // + //} + + //CharacterRecognition::predictAndAcc(N, d, C, h1, X, y, W1, W2); - float *X = new float[N * d * sizeof(float)]; - int *y = new int[N * 1 * sizeof(int)]; + + float *X = new float[52 * 101 * 101]; + int *y = new int[52]; + fillImage(X, y); + + + int N = 52; + int d = 101*101; + int C = 52; + int h1 = 1000; + + //float *X = new float[N * d * sizeof(float)]; + //int *y = new int[N * 1 * sizeof(int)]; float *W1 = new float[d * h1 * sizeof(float)]; float *W2 = new float[h1 * C * sizeof(float)]; float loss_val = 0.0; float *loss = &loss_val; - float alpha = 0.5; + float alpha = 0.1; fillInputXOR(X, y); generateRandomWeights(W1, d, h1); generateRandomWeights(W2, h1, C); - printf("X:\n"); - printArray2D(X, N, d); - printf("\n"); - printf("W1:\n"); - printArray2D(W1, d, h1); - printf("\n"); - printf("W2:\n"); - printArray2D(W2, h1, C); - printf("\n"); + //printf("X:\n"); + //printArray2D(X, N, d); + //printf("\n"); + //printf("W1:\n"); + //printArray2D(W1, d, h1); + //printf("\n"); + //printf("W2:\n"); + //printArray2D(W2, h1, C); + //printf("\n"); - for (int i = 1; i <= 1000; i++) { + for (int i = 1; i <= 100; i++) { printf("\n\nIteration %d\n\n", i); CharacterRecognition::trainStep(N, d, C, h1, alpha, X, y, loss, W1, W2); - + } + CharacterRecognition::predictAndAcc(N, d, C, h1, X, y, W1, W2); + + //printArray2D(img, 101, 101); + + delete[] X; delete[] y; delete[] W1; From 68566829d430a70629763d3af2f2ee712f77da56 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Mon, 16 Sep 2019 23:51:42 -0400 Subject: [PATCH 07/26] graphing. --- .../character_recognition/mlp.cu | 464 ++++++++---------- Project2-Character-Recognition/src/main.cpp | 75 ++- 2 files changed, 264 insertions(+), 275 deletions(-) diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index a4b58dd..cbf594a 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -3,7 +3,7 @@ #include "common.h" #include "mlp.h" -#define blockSize 32 +#define blockSize 16 #define checkCUDAErrorWithLine(msg) checkCUDAError(msg) @@ -15,13 +15,19 @@ namespace CharacterRecognition { return timer; } - void printArray2D(float *X, int nR, int nC) { + void printArray2D(float *dev_X, int nR, int nC) { + + float* X = new float[nR * nC]; + cudaMemcpy(X, dev_X, nR * nC * sizeof(float), cudaMemcpyDeviceToHost); + for (int i = 0; i < nR; i++) { for (int j = 0; j < nC; j++) printf("%.4f ", X[i*nC + j]); printf("\n"); } printf("\n"); + + delete[] X; } // TODO: __global__ @@ -164,12 +170,7 @@ namespace CharacterRecognition { } - void softmaxExp(float *A, int m, int n) { - float *dev_A; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + void softmaxExp(float *dev_A, int m, int n) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (n + blockSize - 1) / blockSize; @@ -178,17 +179,14 @@ namespace CharacterRecognition { dim3 dimBlock(blockSize, blockSize); kernExp << > > (dev_A, m, n); - - - cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void softmaxNormalize(float *A, int m, int n) { + void softmaxNormalize(float *dev_A, int m, int n) { // TODO: Should be parallelized + float *A = new float[m * n]; + cudaMemcpy(A, dev_A, m*n * sizeof(float), cudaMemcpyDeviceToHost); + for (int i = 0; i < m; i++) { float sum = 0; for (int j = 0; j < n; j++) @@ -197,10 +195,21 @@ namespace CharacterRecognition { for (int j = 0; j < n; j++) A[i*n + j] /= sum; } + + cudaMemcpy(dev_A, A, m*n * sizeof(float), cudaMemcpyHostToDevice); + + delete[] A; } - void calculateLoss(float *probs, int N, int C, int *y, float* loss) { + void calculateLoss(float *dev_probs, int N, int C, int *dev_y, float* loss) { // TODO: Should be parallelized + + float *probs = new float[N * C]; + cudaMemcpy(probs, dev_probs, N * C * sizeof(float), cudaMemcpyDeviceToHost); + + int *y = new int[N]; + cudaMemcpy(y, dev_y, N * 1 * sizeof(int), cudaMemcpyDeviceToHost); + float totalLoss = 0; for (int i = 0; i < N; i++) { int label = y[i]; @@ -210,55 +219,12 @@ namespace CharacterRecognition { totalLoss /= N; *loss = totalLoss; - } - - - - void softmaxDerivativeWrtScores(float *probs, int *y, float *dscores, int N, int C) { - /* - Calcluates dL/dscores . probs = softmax(scores) - dL/dscores = probs[range(N), y] -= 1 - */ - - float *dev_dscores, *dev_probs; - cudaMalloc((void **)&dev_dscores, N * C * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_dscores failed!"); - cudaMalloc((void **)&dev_probs, N * C * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_probs failed!"); - - int *dev_y; - cudaMalloc((void **)&dev_y, N * sizeof(int)); - checkCUDAErrorWithLine("cudaMalloc dev_y failed!"); - - cudaMemcpy(dev_dscores, probs, N * C * sizeof(float), cudaMemcpyHostToDevice); - cudaMemcpy(dev_probs, probs, N * C * sizeof(float), cudaMemcpyHostToDevice); - cudaMemcpy(dev_y, y, N * sizeof(float), cudaMemcpyHostToDevice); - - int gridRows = (N + blockSize - 1) / blockSize; - int gridCols = (C + blockSize - 1) / blockSize; - dim3 dimGrid(gridCols, gridRows); - dim3 dimBlock(blockSize, blockSize); - kernDerivativeLossScores<<>>(dev_probs, dev_y, dev_dscores, N, C); - - cudaMemcpy(dscores, dev_dscores, N * C * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_dscores); - checkCUDAErrorWithLine("cudaFree dev_dscores failed!"); - cudaFree(dev_probs); - checkCUDAErrorWithLine("cudaFree dev_probs failed!"); - cudaFree(dev_y); - checkCUDAErrorWithLine("cudaFree dev_y failed!"); - + delete[] probs; + delete[] y; } - void reLU(float *A, int m, int n) { - float *dev_A; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); - + void reLU(float *dev_A, int m, int n) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (n + blockSize - 1) / blockSize; @@ -266,21 +232,9 @@ namespace CharacterRecognition { dim3 dimBlock(blockSize, blockSize); kernReLU<<>>(dev_A, m, n); - - - cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void derivativeReLU(float *A, int m, int n) { - float *dev_A; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); - + void derivativeReLU(float *dev_A, int m, int n) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (n + blockSize - 1) / blockSize; @@ -288,26 +242,9 @@ namespace CharacterRecognition { dim3 dimBlock(blockSize, blockSize); kernDerivativeReLU << > > (dev_A, m, n); - - - cudaMemcpy(A, dev_A, m * n * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void matrixMultiply(float *A, float *B, float *C, int m, int n, int k) { - - float *dev_A, *dev_B, *dev_C; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - cudaMalloc((void **)&dev_B, n * k * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - cudaMalloc((void **)&dev_C, m * k * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); - cudaMemcpy(dev_B, B, n * k * sizeof(float), cudaMemcpyHostToDevice); + void matrixMultiply(float *dev_A, float *dev_B, float *dev_C, int m, int n, int k) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (k + blockSize - 1) / blockSize; @@ -316,30 +253,9 @@ namespace CharacterRecognition { dim3 dimBlock(blockSize, blockSize); kernMatrixMultiply << > > (dev_A, dev_B, dev_C, m, n, k); - - cudaMemcpy(C, dev_C, m * k * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_B); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_C); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void matrixElementMultiply(float *A, float *B, float *C, int m, int n) { - - float *dev_A, *dev_B, *dev_C; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - cudaMalloc((void **)&dev_B, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - cudaMalloc((void **)&dev_C, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); - cudaMemcpy(dev_B, B, m * n * sizeof(float), cudaMemcpyHostToDevice); - + void matrixElementMultiply(float *dev_A, float *dev_B, float *dev_C, int m, int n) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (n + blockSize - 1) / blockSize; @@ -347,29 +263,10 @@ namespace CharacterRecognition { dim3 dimBlock(blockSize, blockSize); kernElementMatrixMultiply << > > (dev_A, dev_B, dev_C, m, n); - - cudaMemcpy(C, dev_C, m * n * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_B); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_C); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void updateWeights(float *A, float *B, float *C, float alpha, int m, int n) { - - float *dev_A, *dev_B, *dev_C; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - cudaMalloc((void **)&dev_B, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - cudaMalloc((void **)&dev_C, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); - cudaMemcpy(dev_B, B, m * n * sizeof(float), cudaMemcpyHostToDevice); + void matrixTranspose(float *dev_A, float *dev_B, int m, int n) { int gridRows = (m + blockSize - 1) / blockSize; int gridCols = (n + blockSize - 1) / blockSize; @@ -377,125 +274,167 @@ namespace CharacterRecognition { dim3 dimGrid(gridCols, gridRows); dim3 dimBlock(blockSize, blockSize); - kernElementMatrixAdd << > > (dev_A, dev_B, dev_C, alpha, m, n); - - cudaMemcpy(C, dev_C, m * n * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_B); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_C); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); + kernMatrixTranspose << > > (dev_A, dev_B, m, n); } - void matrixTranspose(float *A, float *B, int m, int n) { - - float *dev_A, *dev_B; - cudaMalloc((void **)&dev_A, m * n * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_A failed!"); - cudaMalloc((void **)&dev_B, n * m * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_B failed!"); - - cudaMemcpy(dev_A, A, m * n * sizeof(float), cudaMemcpyHostToDevice); + void softmaxDerivativeWrtScores(float *dev_probs, int *dev_y, float *dev_dscores, int N, int C) { + /* + Calcluates dL/dscores . probs = softmax(scores) + dL/dscores = probs[range(N), y] -= 1 + */ - int gridRows = (m + blockSize - 1) / blockSize; - int gridCols = (n + blockSize - 1) / blockSize; + cudaMemcpy(dev_dscores, dev_probs, N * C * sizeof(float), cudaMemcpyDeviceToDevice); + int gridRows = (N + blockSize - 1) / blockSize; + int gridCols = (C + blockSize - 1) / blockSize; dim3 dimGrid(gridCols, gridRows); dim3 dimBlock(blockSize, blockSize); + kernDerivativeLossScores << > > (dev_probs, dev_y, dev_dscores, N, C); - kernMatrixTranspose << > > (dev_A, dev_B, m, n); - - cudaMemcpy(B, dev_B, n * m * sizeof(float), cudaMemcpyDeviceToHost); - - cudaFree(dev_A); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); - cudaFree(dev_B); - checkCUDAErrorWithLine("cudaFree dev_A failed!"); } - void calculateDerviativeW2(int N, int d, int C, int h1, float *dscores, float *X2, float *dW2) { - float *X2Trans = new float[h1 * N * sizeof(float)]; + float *X2Trans; + cudaMalloc((void**)&X2Trans, h1 * N * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc X2Trans failed"); matrixTranspose(X2, X2Trans, N, h1); // dL/dW2 = X2.T * dscores (h1xN X NxC = h1xC) matrixMultiply(X2Trans, dscores, dW2, h1, N, C); - delete[] X2Trans; + cudaFree(X2Trans); + checkCUDAErrorWithLine("cudaFree X2Trans faild"); } void calculateDerviativeW1(int N, int d, int C, int h1, float *X, float *fc, float *W2, float *dscores, float *dW1) { - float *dW1_1 = new float[N * h1 * sizeof(float)]; - float *W2Trans = new float[C * h1 * sizeof(float)]; + float *dW1_1; + cudaMalloc((void**)&dW1_1, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed"); + float *W2Trans; + cudaMalloc((void**)&W2Trans, C * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed"); + matrixTranspose(W2, W2Trans, h1, C); // dW1_1 = dscores * W2.T matrixMultiply(dscores, W2Trans, dW1_1, N, C, h1); - float *dfcRelu = new float[N * h1 * sizeof(float)]; - dfcRelu = (float *)memcpy((void *)dfcRelu, (void *)fc, N * h1 * sizeof(float)); + float *dfcRelu; + cudaMalloc((void**)&dfcRelu, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed"); + + cudaMemcpy(dfcRelu, fc, N * h1 * sizeof(float), cudaMemcpyDeviceToDevice); + derivativeReLU(dfcRelu, N, h1); - float *dfc = new float[N * h1 * sizeof(float)]; + float *dfc; + cudaMalloc((void**)&dfc, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed"); + matrixElementMultiply(dW1_1, dfcRelu, dfc, N, h1); - float *XTrans = new float[d * N * sizeof(float)]; + float *XTrans; + cudaMalloc((void**)&XTrans, d * N * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed"); + matrixTranspose(X, XTrans, N, d); matrixMultiply(XTrans, dfc, dW1, d, N, h1); - delete[] dW1_1; - delete[] W2Trans; - delete[] dfc; - delete[] dfcRelu; - delete[] XTrans; + cudaFree(dW1_1); + checkCUDAErrorWithLine("cudaFree failed"); + cudaFree(W2Trans); + checkCUDAErrorWithLine("cudaFree failed"); + cudaFree(dfcRelu); + checkCUDAErrorWithLine("cudaFree failed"); + cudaFree(dfc); + checkCUDAErrorWithLine("cudaFree failed"); + cudaFree(XTrans); + checkCUDAErrorWithLine("cudaFree failed"); + } - void predict(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2, int* predictions) { + void updateWeights(float *dev_A, float *dev_B, float *dev_C, float alpha, int m, int n) { - float *fc = new float[N * h1 * sizeof(float)]; - matrixMultiply(X, W1, fc, N, d, h1); + int gridRows = (m + blockSize - 1) / blockSize; + int gridCols = (n + blockSize - 1) / blockSize; - // Apply ReLU activation: X2 = ReLU(X2); - float *X2 = new float[N * h1 * sizeof(float)]; - X2 = (float *)memcpy((void *)X2, (void *)fc, N * h1 * sizeof(float)); - reLU(X2, N, h1); + dim3 dimGrid(gridCols, gridRows); + dim3 dimBlock(blockSize, blockSize); - // calculate log_scores for softmax - float *scores = new float[N * C * sizeof(float)]; - matrixMultiply(X2, W2, scores, N, h1, C); - printArray2D(scores, N, C); + kernElementMatrixAdd << > > (dev_A, dev_B, dev_C, alpha, m, n); + } - float *dev_scores; - cudaMalloc((void **)&dev_scores, N * C * sizeof(float)); - checkCUDAErrorWithLine("cudaMalloc dev_scores failed!"); - int *dev_pred; - cudaMalloc((void **)&dev_pred, N * sizeof(int)); - checkCUDAErrorWithLine("cudaMalloc dev_pred failed!"); - cudaMemcpy(dev_scores, scores, N * C * sizeof(float), cudaMemcpyHostToDevice); - int numBlocks = (N + blockSize - 1) / blockSize; - kernMax << > > (dev_scores, dev_pred, N, C); + void allocateMemoryTrain(int N, int d, int C, int h1, float **fc, float **X2, float **scores, float **dscores, float **dW1, float **dW2) { - cudaMemcpy(predictions, dev_pred, N * sizeof(int), cudaMemcpyDeviceToHost); + cudaMalloc((void **)fc, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)X2, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)scores, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)dscores, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)dW2, h1 * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)dW1, d * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); - delete[] fc; - delete[] X2; - delete[] scores; - cudaFree(dev_scores); - checkCUDAErrorWithLine("cudaFree dev_scores failed!"); - cudaFree(dev_pred); - checkCUDAErrorWithLine("cudaFree dev_pred failed!"); - } + void freeMemoryTrain(float *fc, float *X2, float *scores, float *dscores, float *dW1, float *dW2) { + cudaFree(fc); + checkCUDAErrorWithLine("cudaFree fc failed!"); + + cudaFree(X2); + checkCUDAErrorWithLine("cudaFree fc failed!"); + cudaFree(scores); + checkCUDAErrorWithLine("cudaFree fc failed!"); + cudaFree(dscores); + checkCUDAErrorWithLine("cudaaFree fc failed!"); + + cudaFree(dW2); + checkCUDAErrorWithLine("cudaFree fc failed!"); + + cudaFree(dW1); + checkCUDAErrorWithLine("cudaFree fc failed!"); + } + + void allocateMemoryInference(int N, int d, int C, int h1, float **fc, float **X2, float **scores) { + + cudaMalloc((void **)fc, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)X2, N * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + cudaMalloc((void **)scores, N * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); + + } + + void freeMemoryInference(float *fc, float *X2, float *scores) { + cudaFree(fc); + checkCUDAErrorWithLine("cudaFree fc failed!"); + + cudaFree(X2); + checkCUDAErrorWithLine("cudaFree fc failed!"); + + cudaFree(scores); + checkCUDAErrorWithLine("cudaFree fc failed!"); + + } // TODO: implement required elements for MLP sections 1 and 2 here @@ -507,48 +446,39 @@ namespace CharacterRecognition { W1 -> d x h1 W2 -> h1 x C */ - //printf("W1\n"); - //printArray2D(W1, d, h1); - //printf("W2\n"); - //printArray2D(W2, h1, C); + cudaEvent_t start, stop; + cudaEventCreate(&start); + cudaEventCreate(&stop); + + cudaEventRecord(start); + // first fully connected layer X2 = X*W1 - float *fc = new float[N * h1 * sizeof(float)]; + float *fc, *X2, *scores, *dscores, *dW1, *dW2; + allocateMemoryTrain(N, d, C, h1, &fc, &X2, &scores, &dscores, &dW1, &dW2); + + matrixMultiply(X, W1, fc, N, d, h1); - //printf("FC:\n"); + //printf("Fc:\n"); //printArray2D(fc, N, h1); // Apply ReLU activation: X2 = ReLU(X2); - float *X2 = new float[N * h1 * sizeof(float)]; - X2 = (float *) memcpy((void *)X2, (void *)fc, N * h1 * sizeof(float)); + + cudaMemcpy(X2, fc, N * h1 * sizeof(float), cudaMemcpyDeviceToDevice); reLU(X2, N, h1); //printf("ReLU:\n"); //printArray2D(X2, N, h1); // calculate log_scores for softmax - float *scores = new float[N * C * sizeof(float)]; matrixMultiply(X2, W2, scores, N, h1, C); - //printf("Log scores:\n"); - //printArray2D(scores, N, C); - - //for (int i = 0; i < N; i++) { - // float max = scores[i*C + 0]; - // for (int j = 1; j < C; j++) { - // if (scores[i*C + j] > max) max = scores[i*C + j]; - // } - // - // for (int j = 0; j < C; j++) { - // scores[i*C + j] -= max; - // } - //} - + //printf("Scores:\n"); //printArray2D(scores, N, C); // calculate softmax probability: apply exp on all elements and normalize by sum of columns softmaxExp(scores, N, C); softmaxNormalize(scores, N, C); - //printf("Softmax probabilities:\n"); + //printf("Probs:\n"); //printArray2D(scores, N, C); // calculate the loss @@ -557,57 +487,79 @@ namespace CharacterRecognition { // **** BACKPROPAGATION STARTS ***** - // dL/dscores - float *dscores = new float[N * C * sizeof(float)]; softmaxDerivativeWrtScores(scores, y, dscores, N, C); - //printf("dL/dscores\n"); - //printArray2D(dscores, N, C); - - float *dW2 = new float[h1 * C * sizeof(float)]; calculateDerviativeW2(N, d, C, h1, dscores, X2, dW2); - //printf("dW2\n"); - //printArray2D(dW2, h1, C); - - float *dW1 = new float[d * h1 * sizeof(float)]; calculateDerviativeW1(N, d, C, h1, X, fc, W2, dscores, dW1); - - //printf("dW1\n"); - //printArray2D(dW1, d, h1); updateWeights(W1, dW1, W1, alpha, d, h1); - //printf("W1\n"); - //printArray2D(W1, d, h1); updateWeights(W2, dW2, W2, alpha, h1, C); - //printf("W2\n"); - //printArray2D(W2, h1, C); - delete[] X2; - delete[] scores; - delete[] dscores; - delete[] dW1; - delete[] dW2; - delete[] fc; + freeMemoryTrain(fc, X2, scores, dscores, dW1, dW2); + + cudaEventRecord(stop); + cudaEventSynchronize(stop); + float milliseconds = 0; + cudaEventElapsedTime(&milliseconds, start, stop); + printf("Time taken for one train step: %.2f\n", milliseconds); + } + + void predict(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2, int* predictions) { + + float *fc, *X2, *scores; + allocateMemoryInference(N, d, C, h1, &fc, &X2, &scores); + + matrixMultiply(X, W1, fc, N, d, h1); + + cudaMemcpy(X2, fc, N * h1 * sizeof(float), cudaMemcpyDeviceToDevice); + reLU(X2, N, h1); + + matrixMultiply(X2, W2, scores, N, h1, C); + + // calculate softmax probability: apply exp on all elements and normalize by sum of columns + softmaxExp(scores, N, C); + softmaxNormalize(scores, N, C); + + + int *dev_pred; + cudaMalloc((void **)&dev_pred, N * C * sizeof(int)); + + int numBlocks = (N + blockSize - 1) / blockSize; + kernMax << > > (scores, dev_pred, N, C); + cudaMemcpy(predictions, dev_pred, N * sizeof(int), cudaMemcpyDeviceToHost); + + freeMemoryInference(fc, X2, scores); + } + void predictAndAcc(int N, int d, int C, int h1, float *X, int *y, float *W1, float *W2) { int* predictions = new int[N]; predict(N, d, C, h1, X, y, W1, W2, predictions); for (int i = 0; i < N; i++) { - printf("Predictions for %d example is %d\n", i + 1, predictions[i]); + printf("Predictions for %d example is %d\n", i + 1, predictions[i]+1); } + int *host_y = new int[N]; + cudaMemcpy(host_y, y, N * 1 * sizeof(int), cudaMemcpyDeviceToHost); + float accuracy = 0.0; for (int i = 0; i < N; i++) { - accuracy += (predictions[i] == y[i]); + accuracy += (predictions[i] == host_y[i]); } accuracy /= N; + //printf("W1:\n"); + //printArray2D(W1, d, h1); + + //printf("W2:\n"); + //printArray2D(W2, h1, C); + printf("\n\nAccuracy is %.4f\n\n", accuracy); } diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index 27e82d6..fcf4451 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -15,10 +15,15 @@ #include #include +#include +#include + std::random_device rd; //Will be used to obtain a seed for the random number engine std::mt19937 gen(rd()); std::uniform_real_distribution<> dis(-0.01, 0.01); +#define checkCUDAErrorWithLine(msg) checkCUDAError(msg) + void printArray2D(float *X, int nR, int nC) { @@ -68,7 +73,7 @@ void fillImage(float *X, int *y) { if (myfile.is_open()) { std::getline(myfile, line); - y[i - 1] = std::stoi(line) - 1 ; + y[i-1] = std::stoi(line) - 1 ; std::getline(myfile, line); @@ -88,6 +93,23 @@ void fillImage(float *X, int *y) { } +void fillImageRandom(float *X, int *y) { + + int j = 0; + for (int i = 1; i <= 52; i++) { + //std::cout << fileName << '\n'; + + y[i - 1] = i - 1; + + for(int k = 0; k < 1000000; k++) { + //std::cout << j << '\n'; + X[j++] = gen(); + } + + } +} + + void generateRandomWeights(float *W, int nR, int nC) { for (int i = 0; i < nR; i++) { for (int j = 0; j < nC; j++) @@ -138,48 +160,63 @@ int main(int argc, char* argv[]) { float *X = new float[52 * 101 * 101]; int *y = new int[52]; + fillImage(X, y); - int N = 52; int d = 101*101; int C = 52; - int h1 = 1000; + int h1 = 10; - //float *X = new float[N * d * sizeof(float)]; - //int *y = new int[N * 1 * sizeof(int)]; float *W1 = new float[d * h1 * sizeof(float)]; float *W2 = new float[h1 * C * sizeof(float)]; float loss_val = 0.0; float *loss = &loss_val; - float alpha = 0.1; + float alpha = 0.5; - fillInputXOR(X, y); generateRandomWeights(W1, d, h1); generateRandomWeights(W2, h1, C); + float *dev_X, *dev_W1, *dev_W2; + int *dev_y; + cudaMalloc((void **)&dev_X, N * d * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed!"); + cudaMalloc((void **)&dev_y, N * 1 * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc failed!"); + cudaMalloc((void **)&dev_W1, d * h1 * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc failed!"); + cudaMalloc((void **)&dev_W2, h1 * C * sizeof(float)); + checkCUDAErrorWithLine("cudaMalloc fc failed!"); - //printf("X:\n"); - //printArray2D(X, N, d); - //printf("\n"); - //printf("W1:\n"); - //printArray2D(W1, d, h1); - //printf("\n"); - //printf("W2:\n"); - //printArray2D(W2, h1, C); - //printf("\n"); + cudaMemcpy(dev_X, X, N * d * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_y, y, N * 1 * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_W1, W1, d * h1 * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(dev_W2, W2, h1 * C * sizeof(float), cudaMemcpyHostToDevice); + + + //std::ofstream myfile; + //myfile.open("loss_curve_XOR.txt"); for (int i = 1; i <= 100; i++) { printf("\n\nIteration %d\n\n", i); - CharacterRecognition::trainStep(N, d, C, h1, alpha, X, y, loss, W1, W2); + CharacterRecognition::trainStep(N, d, C, h1, alpha, dev_X, dev_y, loss, dev_W1, dev_W2); + //myfile << i << " " << *loss << '\n'; } - CharacterRecognition::predictAndAcc(N, d, C, h1, X, y, W1, W2); + CharacterRecognition::predictAndAcc(N, d, C, h1, dev_X, dev_y, dev_W1, dev_W2); - //printArray2D(img, 101, 101); + //myfile.close(); + cudaFree(dev_X); + checkCUDAErrorWithLine("cudaFree fc failed!"); + cudaFree(dev_y); + checkCUDAErrorWithLine("cudaFree fc failed!"); + cudaFree(dev_W1); + checkCUDAErrorWithLine("cudaFree fc failed!"); + cudaFree(dev_W2); + checkCUDAErrorWithLine("cudaFree fc failed!"); delete[] X; delete[] y; From d8c71be07b2c3196ada6e4f3865807f94f402523 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 01:00:57 -0400 Subject: [PATCH 08/26] get data. --- .../character_recognition/mlp.cu | 5 ++-- .../data/final_predictions.png | Bin 0 -> 55554 bytes .../data/memorize_random.png | Bin 0 -> 14193 bytes .../data/perf_image.png | Bin 0 -> 5899 bytes .../data/perf_neurons.png | Bin 0 -> 5587 bytes .../data/training_curve.png | Bin 0 -> 9524 bytes .../data/training_curve_xor.png | Bin 0 -> 12105 bytes .../data/xor_predictions.PNG | Bin 0 -> 5245 bytes Project2-Character-Recognition/src/main.cpp | 23 +++++++++--------- 9 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 Project2-Character-Recognition/data/final_predictions.png create mode 100644 Project2-Character-Recognition/data/memorize_random.png create mode 100644 Project2-Character-Recognition/data/perf_image.png create mode 100644 Project2-Character-Recognition/data/perf_neurons.png create mode 100644 Project2-Character-Recognition/data/training_curve.png create mode 100644 Project2-Character-Recognition/data/training_curve_xor.png create mode 100644 Project2-Character-Recognition/data/xor_predictions.PNG diff --git a/Project2-Character-Recognition/character_recognition/mlp.cu b/Project2-Character-Recognition/character_recognition/mlp.cu index cbf594a..7ead417 100644 --- a/Project2-Character-Recognition/character_recognition/mlp.cu +++ b/Project2-Character-Recognition/character_recognition/mlp.cu @@ -503,7 +503,7 @@ namespace CharacterRecognition { cudaEventSynchronize(stop); float milliseconds = 0; cudaEventElapsedTime(&milliseconds, start, stop); - printf("Time taken for one train step: %.2f\n", milliseconds); + //printf("Time taken for one train step: %.2f\n", milliseconds); } @@ -525,6 +525,7 @@ namespace CharacterRecognition { softmaxExp(scores, N, C); softmaxNormalize(scores, N, C); + //printArray2D(scores, N, C); int *dev_pred; cudaMalloc((void **)&dev_pred, N * C * sizeof(int)); @@ -542,7 +543,7 @@ namespace CharacterRecognition { predict(N, d, C, h1, X, y, W1, W2, predictions); for (int i = 0; i < N; i++) { - printf("Predictions for %d example is %d\n", i + 1, predictions[i]+1); + printf("Predictions for %d example is %d\n", i + 1, predictions[i]); } int *host_y = new int[N]; diff --git a/Project2-Character-Recognition/data/final_predictions.png b/Project2-Character-Recognition/data/final_predictions.png new file mode 100644 index 0000000000000000000000000000000000000000..dc444a50b49e67009c275ef78af0957573e5980b GIT binary patch literal 55554 zcmc$H2{@E{8@IMoC|Z@2sZ`pe4H2eAbaF~ULd{f?eTm3A(Sl7bRvUz`X0 zx2V%G6BiK?i30H7Sw)VqmqkRrhW)N}$j}|u%Yi2E&~@=N}c9 zUw+mb_U_>6s3DQrNe;nza^yQYKK{#NokVtvZ~ObVP33K+TUTA_D)D*p(ez4*Mn1~x zZ>|`maT&L$u`yJXCXRZ~$wYDp$oJe59D9&i1mzM)NTwdITY=ps2~9@Bp%^Gm69eUv z=TlYzZ&4(V^iCWD2{Zf7s(a^cDF4blR}*R2fOu6wMM+>NR3t1JQb#b<>;|(TPV$)_2s`5WF+O8gu8X z8kXB88P}AZ17(k35hoh7Sr#(j^Lnf3A&0_mTe3D*h=mPZOA)oS=5?Bj+A!FgMd-w-6P9em*#?+T@GVWAWg#3{j zXz6P#cdsXpshe-RSGI;apY>`j_}n87Zpfkf9$#Zon{4~->JAbZ9jM2%TOKutX65t9 zH}H&Ez=QIxr*6e|UHCpX`xBh}@I*eQROMd~z76 zJwz)g_{`oq*V~l*Ua_l;K8oI~<-YeR}(l5)qZCWjv`q4BCW>7ZPZL&AJ2yq!=vz&99Q`gRNs{8NhuN}J(?SiIS;r$Lf`)+rjp8BSGFgJ$5eTCh0TXw@&d9}FD8ry5yt2Dm03<;eRpT!_CD zt&ne3v9EIzp6}=FBP=KmtzZPxC>*7WXHdo~+(h|b)8e6q0UWQLNyqUJoDw)UT;*Cp z!r<~0pVcP$Wg3P~C!hyHcQwe^g0GtQEfdP= zRwN*KbSRwH*TQ|MGCccHx?bd8JrB;zwh?u#HK?uNCWU@^cgH#b8413N5{S#TwsVo# za-N>uUPP_?Z18#dlf*GM1hR`6;b5_eJVT%ft^;% zeX>uFe+jNDl)&`dzwml??NWB4kHPoGcX0Kkuz}=@bBQqbwa=i9P>pBL=+`5eO9c<_ z+KJ`AK_}&Ze5tLKg(VR_st?6S+{=vYI<&sJmmyggRQ}t=zBtjQq?jFZ)aN}^kzMn`XIND+Kk!DYum4-C%sKau5mwDpPVmV>`Z#Ew z?01!rEpv}`!yDGW)WC>%_^J61>XqbhV zIB4{R)vARyaJp;rnWh%bNE!#OM|qjVfL?3VWd~0-=s8h znJuTGYrmH<_tPC~pB2c?B7$qb(Tf;b<0FgY~H>eII}SUzuGLnB9=?Jx(6SS{nY-CqHb0g z>tROZQrK+{>U&KaP%3tDU|S@AzN-f_%Q?#Uz7@UVv$L1>z1;=AV%t2uNX7D)@HIH) z=Vv+Oj7;-p;3=Qxlb&-AGasXS5^_zcHSO=WV%5y8@tCtbxecyg)u2oBw*$|Z3$!P~ z%S9VaWlnyc&35zNg)MKG?@#j%ShmHt2GM}q*n+wvzkN;I!A%Wm749aDWVP>bCbrWv zG^s7tn5UI#mj7)}WHZr&U30d?r)PIcPdl|pCPtqZ{lfLDDJF>u9`&#qM;wofBDL_i zEUF@sk&rdw7%0_HSvV9@qPFni1!a4^Lt68eQW{@o!TN94V+zhW6Uwh4UUF1-iLO|f zG+1%-vmN{yq|=vx#Z=)g+pb!witVPQx*yw9bg&RQf2TRSTA6Xq8yFBA%-6Rk6q@!M zB*zqxoVh2A%*>G;k6Yb?>Q(5N2nebcJe%E(YC?=jBCSTp_V@ZQ!18NV;?U4M3Nn{l z1?5yx(L4saY|ahiL;M!hys~YD<83l{Mq?HEpP5MOF#qBbxltu2I1x?`kje3I#i>*T8da2Gfb;z*(QRiSFSP78Sd)U`IRZ#!-y(`)^LVY zTX?7aPTxM}qa_QmL#lfB4qw?+^&+vk@Sx@&6-{iUFgX12=>xkaCh=IVI#c{OD2A0l=FUr!!JidE+%6I4xn zdm}Rc9y2onKKsu*isrn}& z>FE&Z8hFMdO;~c+=6hztBRxo6&b8+%r1nOf3GV_=E|Es;lPs4^$jz7L`R{`NqvW@Suk}k5%@KJcxLY+d@P0#vMdEcSBVTebRdpimn^DyB1a^BH!?q z1gqISgP4)Po`HQkR_e~He%C|aYIALNxU)n7(mwb2#q<`MVggS65S^+f4n^sh(;4Pb9(9)BTZh zDEeIO#piQVt-ZuHYz}?fUDz+iQwkAO7a=u@|fYQ4)q%SUv15ir6ln zXWUJ;1rOW^v_zi8%_~7S1CSE1!jU-pp{#elX{d&?#Ut@4p{$3bZgO@6(x9gHD@1kk z{>W%r8E1P}(O$vX8@~-}I$s2HIRD~6$x7h26~Wvm|C#w8mauzZ@oKM3-ol#gz7F+- zSr?!7DCvGMZcU_$QN)3#c2_cTf3~t|+rB=I#3zk>Qqe02tz33zhfkyqyDe&;TQ+xK zbvVQW(q9}&A-%5#4;`x~ZAN~t-=1w&%rkAH>PM43-8$n)x$xCmYouS$Gc4UXM$K8^ zr+CnI6-@U5GaJF((oi{8zGw9O0xr~RYE%|!A!?t@!KlHL;m;D{+_d}fdSzXdu*F#c zdZLRgczcv-3{t^SVg05ShAPe$wCbPKuLlQp7K*FIOWN!D0B_)iaT>5M9HpnlI?{>0 zIMXzgrk5Tp$4UQI!rEh6mk&9nE7CFevZ7Bp#gzP)wrX<{>YaL0 zwH39z?o!WiQkbDk3=ysg@3&x9Zs@}|P_jxQYCfE*w<^-<=+l0t%|Q*-(=!}XUI{Lr zKRI^q7U}f3FuK1j*RIBOdgtUR(0Vd?hYzL_t+DVqBVpTjIxmCNcfaFPv- zR*uKnZTG>RQ#`Ra^ZQ^8FO=DR(6P2C3z^FOz|}bmyaPzYSTRh~tCJRB&g@UIlfY=h zhn32lE=L&5{(Nnq{4CY|rgb%LILjEV;CxCfLuI<^b-#wNvK5X2KbCH(Q`D_Lo15#` zzyi75UV93Hm*rA&yGPR7@_YO;bJkK|J{jMe;I@Xy^*(mD!;IiQoD*#|(z;%Vk4(st zxPn?nRFbg$N3Mp2G>1G_j_B~c?#aBoBR7Wxe3GjwhIZcuj@9vgy(quH3cJI>eeJNK zPqVWI>YU>VuN>9*3SC_B&M+_Afhg z$JUF-(p!T1?y3H^hpYN-b$d+4C*n6?9K9+dE)7?zbw)dDtc8V99OLU(v*e2A7P(aC zW+6Ya-;~46z)7{$$f&JGO~if0(tb~PF7Q7@+)vyw#xCMFU2^UMjJphf@NV@PT@t6P zkbZu~9A=2eQ|!{qA8oa2zrN>K#=^G#y4S4xhyVIKL*n#h)Y zH#CMc8-wAyam_jW$}CxkQQP-w-8;)0>G`B+?KW!f@m+Ov-P@5KvgabEr|`$};br_(yBA4TmvrU9)UiQ%5p zB(mT=r)(C-p^E9>OEDGfJ`=EzQ96S2est(|86CRoyoP#w{gBb?DGUI=)`KPj>}9Qx?_c}9RC%6w`O+M zqB)&Be1ue0Pm43c#7_4oW4E$>q6M{}p*hGJqv?DSCHp?T&B_p2cZTVn1q)y2P~r1s zx?+9|LGeFrcZ+uD*XkX4IY&KCOEJqBCZcn%j5tfRy1jcRDIZ$aR`cCI_qB9TfXnfN zPx_mr6J!;41v0}l={SMxC;h81Ny6U0Ibv}%Q@q#?4k!9we5UaZwx**HR|;Q4#H+l=aCBNxWC z>+X7*5#yU_dgJM|J=y63(32NmVaeqCu7pu?PxAiK$vf+|C5n=x?%=rYKf+fwYMpI zTl^a8T@XHD{7*(72^Eko=143yyf*C1?mAnRL-z z;ZP57jz!yrd`v~muHN}txAobo4{1OpM>hbI?axb@%ao55ma-ly!m-bPs7Dd%Tia&_ z52~gd2}H)TI$d&kuOwdm7RobA<#*GtKP}FKV!h z=W(8{NcH-!Y8gs1b!Jt#qdyIrZ-Z>+=CQXB9TNm=ZXxnboXHI@|Dl&VhM!VWmo6wJ zr*FWTw*3ttS*v|22>S|xuby17D+v{i5`jPxY_BH?tU^pq#ly)5rRQLhO=H2_DV7tyC60XOB7txnJ1ql)-wl3(G zzAz4vxwa@SQXl%8-o(2!5ZUIc*TNq?!I`_{5`$miy8hhd3#oX;hUaHz5B$C0bq%-c zaWP)arPYX7LQQZ{swt1<(BeWR`b$P0%0~+)?%UeJiMxst&^k~9zZK?yTVvL8gx{yL z;S>6FdwZ!OhPQ-7BxExN0|Xzkm4SPif4{YUOFv#5nQjH~p+#an+g{8yKJegP@$43U zmwt*0@p^V!XRmDhT1?9l#<>+XQk(Juna1gD)iE*ba}2lM7_Rd=x8zl|X4Ik{vmRU| zlHr{d)~)NG26e;}ce*un-bM>`5$scXZuLGhH@e!=xPp4dpq1eKpjDH@({5_n7W^(D*WGOZqGf@*UUZsA9Nd~n zYv68TAU3L-w8buig-is?!t0tZ>_~@kLQYeh9qm_9e(ct@wIty*!0*;)5zZ!lHs2J=PYV z#LTJ_jNaF(awQft7-CGsa%Wp689#>eUn?%3g#CUdsyl{~TVYuzhee!YUL5FcJKWsK zOyH4E$vn1nT$L~D198B^D5L{5`~{i=94wqZCWJQ#AwYQyJmrO%-|OXb=?mYSardd- zf{i>cx|M~nUi^rJGblb#8QDfP_9XBNEUAS%h(ex2@;G!7aQYbU#fGkl zXT|CK6xFY%nvv`0iHIH4bHxep@ZsH)@bDxw%F$s6Etm@V7}q=7%r zVz(Utaej}KqV{pIWMh9?h2_Afgq1swJM}4ktr{EHe{vL91}f0^Bxd%YYBH~ty_e@X zD^`U`StFYx@#nh|CGJ*gAJEKyp9!F|SXyR49Px=g_(fYT0#AWjb+b7U zopm5B)3273?3dnh{B5C2=Q5{m6&D2p7Z|sk$p8rb}Q+PM7L{?Ktt$`Z(__R9o{7?aAR%1K?GmC_$aCNkL-gqGF5(Se8xN$2#u(IF(>EcEWAF;ax zkM~Qo(|*LqUGr`COqkCxE2N=%!ueeJjt81J*s>Uw!*y@p*Ml@nlQ8R{9ur&!)M4^A z`yJ4%#DasOSE^`fBE~oWR#&l34r4v1k#>bOrtf_Qu|`V#aoBgRt@pL(dm366@4=G8 z>8{K$JJBQI)iEmbJ*PBxGvv(m{%CCR7;Ejv-hk*}v*aY0yZJXhE9(xQiRx-@Ycs-e zFE?+-EH`1S4vq!st)54RRr*Lb-khL@ziQp?RO{)>0Kar>_%>|0zr%`fgxY|NPv``| z?0hl^n7yYLzrLQt-QTiGqo;!2M(b6@c0i9}!*PfrL3i2T3ZC9n+$)6Im#5UL{VjYz`3#Ff(;*fW3SP~Q zJ^yL=ymy&f^gbWPsY18k%v%UQF|zEJi}$i%T;k*8^ZDo4vf5NMD#YVTbrVq4CtGlh zF=^C?ql0@XN@o(J)<4hJ1Gi*4;_KWP@pa4^kSL^=?TyDh(Ck|GdQv;T#6jJwzSuW5 zB6!)jpQu0hBE4;+U+{3pmKm1sYEjH508N9nuBP(QH2wUiOJwJ_CxO#Dt;18me@zk1(@c~An}~Xdj0iJ%28?1`J1C|MJZ{joqE!jK#3L>c%tZ$(0I+d>=;jeM}KZ1 z=;#gUPzO%%ppm%(eJi1=%~h56IzA}{>imb3x=!Cymhl*3@gF1NC2dq3w}lHO@VHQ} zF$Jh#;GZBjclR0$^hxHY?MKh#YHzd|prNCDHXUcFt+V@aRDaE^BhS#^J^zNtW-JFgQ?jVsR4}Zd~9>Zaec>Wr1X0l9hG{5^u-|l2@(%RW>5E&54Df#+!a@~_07|tIOaO~x#^dhG7%Sea~ z+W@{3P2niIZc(~dt60R{5h5*X!$m+7ud79}69 zP_Ki|`Irf|@bq8M+z<2i^uTNG8lWThWSrQ)XSM-;GS|%=<+`WY)<TpB)mIoX|X4#um44Tn}k%V z#TEDSkt0p}CUo@KH>GpW+%ew%Q^s3Q>wJM(ljgTJdqW;${}W5?rZVN%?QW770H+`B zAs4gVU-Ii`bclzYdJK>e*K@v<_erlWT)1*u?DxJTIW*SBnVoUt(jCGf{&tX2OkZi| z2*T*4dQ_u^Gd|Q22Qf_f*?P+MTHd{50Gc-j1dI>zF!vbwV;{(Bjw<>0LdTZ&4puPv zvaj5V*vn6qCp31vK~EKgbOyt{?~po5h9v;pRJHk~nj#WP1*9a8lE}w$G6cY+k)9-` zvXGrxthE+?t1eAr-jWr^N?k&_JhLQia1t(`&L5}(WH7|MjfT6@3U%ghOZ9O@JRFDL z{wipY^T{j(SZ+TBAZUxQ zod~eodpY!6Ne|6GsNqK{Pp%(027+_?P~a!A34ClWV-*)1=7I5yNql5~YK=x+98CqF z$}&u(zL$(s-5xfZLPg#L=y~ihr74=5qj6kO8{y*KzSTaA_IVY-K1@EpjH67E%YO

v^jKQ2G^ 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 literal 0 HcmV?d00001 diff --git a/Project2-Character-Recognition/data/perf_neurons.png b/Project2-Character-Recognition/data/perf_neurons.png new file mode 100644 index 0000000000000000000000000000000000000000..60830b45a14a2b50daab58ab04c99bb15eddcd8b GIT binary patch literal 5587 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 WA^*z9L%>TXkj_Q@3;9}i9{(2@o^am) literal 0 HcmV?d00001 diff --git a/Project2-Character-Recognition/data/training_curve_xor.png b/Project2-Character-Recognition/data/training_curve_xor.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d28619ad5b96e7944cdadec2b5b91a8fbd9a46 GIT binary patch literal 12105 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 Im)|e{1+)9uumAu6 literal 0 HcmV?d00001 diff --git a/Project2-Character-Recognition/data/xor_predictions.PNG b/Project2-Character-Recognition/data/xor_predictions.PNG new file mode 100644 index 0000000000000000000000000000000000000000..bdd7564baf72868b08b10bc9d7ed743f7e1ead7d GIT binary patch literal 5245 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 literal 0 HcmV?d00001 diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index fcf4451..cd5d146 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -93,17 +93,17 @@ void fillImage(float *X, int *y) { } -void fillImageRandom(float *X, int *y) { +void fillImageRandom(float *X, int *y, int N, int d) { int j = 0; - for (int i = 1; i <= 52; i++) { + for (int i = 1; i <= N; i++) { //std::cout << fileName << '\n'; y[i - 1] = i - 1; - for(int k = 0; k < 1000000; k++) { + for(int k = 0; k < d; k++) { //std::cout << j << '\n'; - X[j++] = gen(); + X[j++] = dis(gen); } } @@ -157,17 +157,18 @@ int main(int argc, char* argv[]) { //CharacterRecognition::predictAndAcc(N, d, C, h1, X, y, W1, W2); - - float *X = new float[52 * 101 * 101]; - int *y = new int[52]; - - fillImage(X, y); - int N = 52; - int d = 101*101; + int d = 101 * 101; int C = 52; int h1 = 10; + float *X = new float[N * d]; + int *y = new int[N]; + // + ////fillImageRandom(X, y, N, d); + fillImage(X, y); + //fillInputXOR(X, y); + float *W1 = new float[d * h1 * sizeof(float)]; float *W2 = new float[h1 * C * sizeof(float)]; float loss_val = 0.0; From e019b45332a7214f806360d4217687c5b70db083 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 02:54:54 -0400 Subject: [PATCH 09/26] data. --- .../data/compact_perf_15.png | Bin 0 -> 7156 bytes .../data/compact_perf_20.png | Bin 0 -> 7351 bytes .../data/compact_perf_25.png | Bin 0 -> 7280 bytes .../data/data_perf.xlsx | Bin 0 -> 36856 bytes .../data/scan_perf_15.png | Bin 0 -> 6554 bytes .../data/scan_perf_20.png | Bin 0 -> 6958 bytes .../data/scan_perf_25.png | Bin 0 -> 6434 bytes Project2-Stream-Compaction/data_perf.xlsx | Bin 0 -> 36856 bytes Project2-Stream-Compaction/src/main.cpp | 2 +- .../stream_compaction/efficient.cu | 157 ++++++++++++++++-- .../stream_compaction/naive.cu | 16 +- .../stream_compaction/thrust.cu | 8 +- 12 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 Project2-Stream-Compaction/data/compact_perf_15.png create mode 100644 Project2-Stream-Compaction/data/compact_perf_20.png create mode 100644 Project2-Stream-Compaction/data/compact_perf_25.png create mode 100644 Project2-Stream-Compaction/data/data_perf.xlsx create mode 100644 Project2-Stream-Compaction/data/scan_perf_15.png create mode 100644 Project2-Stream-Compaction/data/scan_perf_20.png create mode 100644 Project2-Stream-Compaction/data/scan_perf_25.png create mode 100644 Project2-Stream-Compaction/data_perf.xlsx diff --git a/Project2-Stream-Compaction/data/compact_perf_15.png b/Project2-Stream-Compaction/data/compact_perf_15.png new file mode 100644 index 0000000000000000000000000000000000000000..8f3dbca4f847373e9e9a87e33b15847b30c498c1 GIT binary patch literal 7156 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 Y?iS#gz2pnlS3-c9(RsrneaBn>1enLi#sB~S literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/data/data_perf.xlsx b/Project2-Stream-Compaction/data/data_perf.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..de9d548fa9f2b3dfd3de4d2e393fe51cb9dda3bd GIT binary patch literal 36856 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 z zI%vx(y_?$@R8W^bYi>uDevxz>zR;7TbWvqhB4p-1mEpQB5M#1@G(?^#ours0jcu*r zK_#-grNg*^OO#t@cZ(5WzRmzqDhhR)4D<4pj;apdK~fzPWbqV_%tfb2dU6?9EzC^( zmZ;!=UD~mYA%<4*E1ZzK$or@&=MSimG8?Ik=rk}wng_{ENGpA|we%@#D0@_TbsWB- zeO<=;$!)iL*L6dQMCOM6C5&C2r)o9zk9`S+~WT{8q)#!VCHof9P6 zJO0ggac{C+q@Atq247U~onGM%MSpAVC0)BEJhlb~I_6|hsV80ND227jM_ZK_ETF~?*w^}guwmI9TrhJKrGr$GhMtQjCYA=U<;!9BS6ro;Y)p>bDxJ>9_1~Myh>!YG zHqicKTF_?wVm@kgD#Hfcx(;2ByfkS_ERkk&5R?Ng4T7kxBd1vL?}B(qKS)QCysY7H zjJ?G=!`kk1L<7yw&P%{7Y9b&?vl|JafpE8<+xN5-HXuf&-tn7QClgJYp7@gSA-feX2 z{iy#xp!XQ4E#CSR*cY79S`hnQ2PFc@*s_{%=aKheA>k2BTyBys+`Fc?wj#%3@W2OwV1obsBEvxPlSD1I0ja{#|Gki zxvMvRSJ7P`_dLUY-A;?T$ZPtfefZV=Hl}9UFD`-mjbgxD-KQ3$rV*AU;@YGRZX%m| zcM~Yf;@qd%w)ujw+x~-{!4j*>J|g~~8@8B_{NVWx@jbF+cqGH}=Z%1vCnJsB9%<_b zNa?+0ba^Tm%U5`mYdJ3=fPtB;e> z)yupYzSW@%m;R4o!7CeJu8$bqTS&tFo9Ai&Jq8Mes<#2W$k|E0<4n@aYa;5IJUss&V?wiNc-f%2uKPQ4Ut8 zrBg#Wh8U3^ED#if_-{Dg=CQ?KMzMeLAa}5}zflu5#Vggq`^|U}$F9tFm>8HN!@p2g zX|&^X(pUJUx;e#g50i%L5fxEdzirPw{kfxCan9R)!$PmJWWs!cRri~aI3JW{TT=WY zeUzC_z_$Wv$Cl6b(E`!%4fD^wsxD47cBo9x4x1jR`zgYqrIVnt4;Asi2<3=A*dYr_ z{NVF{ge67Z{|qKJqN!EcNhDF!Ng$Q6g|?Z4Fna$i&NfW{fL`yOwG#%`KWohm8}S@2qgFRD-#X&zBXH zTR9h|rE51n?nlCmC*7#M#vPXtnkrnx)qxtc^Kd+eEi8^37CUV1d&lJG!dTcGys!^lHKyEiIJj|X2#$tpQY+-yB@^}YRd4jV&FALay7%?z zx`W#2=tH*l9hN$YQ5m5AKo*g`vkxB>v4pN&s`G+9WJNZQ5QT(TqIRn4WR03*=ePc8 zq2`eU)!3p(chE}NAqvqxs3#R>nIrU%7GHWbkId10aXLKhfzRpi7 ztBD+Lw=cLrimw|Eynq3p06ufnGWkr=(od8K3~vG^!RQFA4i_tnmO&8UGr0 zOMFRD&1p(|aD}IGq%q7Q-mugnq};{bLbGz5Z&z zuL!1U;^6>5K5%9U{_}ajk*N2dD)RYlxD`d2Z3?ogqFn^>_Y}Z!TSuFMqh6Q)4&Mpu A+5i9m literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/data/scan_perf_20.png b/Project2-Stream-Compaction/data/scan_perf_20.png new file mode 100644 index 0000000000000000000000000000000000000000..5f95d5e91108de87822248bb207a3f6dbf668af3 GIT binary patch literal 6958 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 YHU0@u&&nr&|8W5a_F9_e7`q_<0W(FhRR910 literal 0 HcmV?d00001 diff --git a/Project2-Stream-Compaction/data/scan_perf_25.png b/Project2-Stream-Compaction/data/scan_perf_25.png new file mode 100644 index 0000000000000000000000000000000000000000..27f15227e8a0121256ee1ac5f0d90c02473af057 GIT binary patch literal 6434 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 #include "testing_helpers.hpp" -const int SIZE = 1 << 8; // feel free to change the size of array +const int SIZE = 1 << 10; // 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/efficient.cu b/Project2-Stream-Compaction/stream_compaction/efficient.cu index 5d1df68..7f3e3c7 100644 --- a/Project2-Stream-Compaction/stream_compaction/efficient.cu +++ b/Project2-Stream-Compaction/stream_compaction/efficient.cu @@ -61,11 +61,6 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { bool exception = false; - try { - timer().startGpuTimer(); - } catch (const std::runtime_error& ex) { - exception = true; - } int *dev_idata; @@ -93,6 +88,14 @@ namespace StreamCompaction { cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); } + try { + timer().startGpuTimer(); + } + catch (const std::runtime_error& ex) { + exception = true; + } + + for (int d = 0; d < d_max; d++) { upSweep<<>>(n, d, dev_idata); } @@ -108,14 +111,84 @@ namespace StreamCompaction { } + if (!exception) + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, n * sizeof(int), cudaMemcpyDeviceToHost); + + cudaFree(dev_idata); - if(!exception) - timer().endGpuTimer(); + } + void scanCompact(int n, int *odata, const int *idata) { + bool exception = false; + + int *dev_idata; + + int numThreads = 128; + int numBlocks = (n + numThreads - 1) / numThreads; + + int d_max = ilog2ceil(n); + + int twoPowN = 1 << d_max; + if (n != twoPowN) { + + int diff = twoPowN - n; + + cudaMalloc((void **)&dev_idata, (n + diff) * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_odata1 failed!"); + + resetZeros << > > (n + diff, dev_idata); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyDeviceToDevice); + n = n + diff; + } + else { + cudaMalloc((void **)&dev_idata, n * sizeof(int)); + checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyDeviceToDevice); + } + + try { + timer().startGpuTimer(); + } + catch (const std::runtime_error& ex) { + exception = true; + } + + + for (int d = 0; d < d_max; d++) { + upSweep << > > (n, d, dev_idata); + } + + // reset last element to zero + int* zero = new int[1]; + zero[0] = 0; + cudaMemcpy(dev_idata + n - 1, zero, sizeof(int), cudaMemcpyHostToDevice); + + + for (int d = d_max - 1; d >= 0; d--) { + downSweep << > > (n, d, dev_idata); + } + + if (!exception) + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, n * sizeof(int), cudaMemcpyDeviceToDevice); + + + + cudaFree(dev_idata); + + + } + + /** * Performs stream compaction on idata, storing the result into odata. * All zeroes are discarded. @@ -126,7 +199,7 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + int numThreads = 128; int numBlocks = (n + numThreads - 1) / numThreads; @@ -144,12 +217,13 @@ namespace StreamCompaction { cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + timer().startGpuTimer(); + StreamCompaction::Common::kernMapToBoolean<<>>(n, dev_checkZeros, dev_idata); int *checkZeros = new int[n]; cudaMemcpy(checkZeros, dev_checkZeros, n * sizeof(int), cudaMemcpyDeviceToHost); - //printxxx(n, checkZeros); int *sumIndices = new int[n]; scan(n, sumIndices, checkZeros); @@ -159,6 +233,7 @@ namespace StreamCompaction { StreamCompaction::Common::kernScatter<<>>(n, dev_odata, dev_idata, dev_checkZeros, dev_sumIndices); + timer().endGpuTimer(); cudaMemcpy(odata, dev_odata, n * sizeof(int), cudaMemcpyDeviceToHost); @@ -176,8 +251,70 @@ namespace StreamCompaction { cudaFree(dev_checkZeros); cudaFree(dev_sumIndices); - timer().endGpuTimer(); + return count; } + + + //int compact(int n, int *odata, const int *idata) { + + + // int numThreads = 128; + // int numBlocks = (n + numThreads - 1) / numThreads; + + // int *dev_checkZeros, *dev_sumIndices, *dev_odata, *dev_idata; + + // cudaMalloc((void **)&dev_checkZeros, n * sizeof(int)); + // checkCUDAErrorWithLine("cudaMalloc dev_checkZeros failed!"); + // cudaMalloc((void **)&dev_sumIndices, n * sizeof(int)); + // checkCUDAErrorWithLine("cudaMalloc dev_sumIndices failed!"); + // cudaMalloc((void **)&dev_odata, n * sizeof(int)); + // checkCUDAErrorWithLine("cudaMalloc dev_odata failed!"); + // cudaMalloc((void **)&dev_idata, n * sizeof(int)); + // checkCUDAErrorWithLine("cudaMalloc dev_idata failed!"); + + // cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + // timer().startGpuTimer(); + + // StreamCompaction::Common::kernMapToBoolean << > > (n, dev_checkZeros, dev_idata); + + // //int *checkZeros = new int[n]; + // //cudaMemcpy(checkZeros, dev_checkZeros, n * sizeof(int), cudaMemcpyDeviceToHost); + + + // //int *sumIndices = new int[n]; + // scanCompact(n, dev_sumIndices, dev_checkZeros); + + // //cudaMemcpy(dev_sumIndices, sumIndices, n * sizeof(int), cudaMemcpyHostToDevice); + + // StreamCompaction::Common::kernScatter << > > (n, dev_odata, dev_idata, dev_checkZeros, dev_sumIndices); + + + // timer().endGpuTimer(); + + // cudaMemcpy(odata, dev_odata, n * sizeof(int), cudaMemcpyDeviceToHost); + + // int *sumIndices = new int[n]; + // int *checkZeros = new int[n]; + // cudaMemcpy(checkZeros, dev_checkZeros, n * sizeof(int), cudaMemcpyDeviceToHost); + // cudaMemcpy(sumIndices, dev_sumIndices, n * sizeof(int), cudaMemcpyDeviceToHost); + // int count = checkZeros[n - 1] == 0 ? sumIndices[n - 1] : sumIndices[n - 1] + 1; + + // //delete[] checkZeros; + // //delete[] sumIndices; + + // //printf("hey\n"); + + // cudaFree(dev_idata); + // cudaFree(dev_odata); + // cudaFree(dev_checkZeros); + // cudaFree(dev_sumIndices); + + // delete[] sumIndices; + // delete[] checkZeros; + + // return count; + //} } } diff --git a/Project2-Stream-Compaction/stream_compaction/naive.cu b/Project2-Stream-Compaction/stream_compaction/naive.cu index 35e8d38..dc8b82b 100644 --- a/Project2-Stream-Compaction/stream_compaction/naive.cu +++ b/Project2-Stream-Compaction/stream_compaction/naive.cu @@ -13,7 +13,6 @@ namespace StreamCompaction { static PerformanceTimer timer; return timer; } - // TODO: __global__ __global__ void prefixSum(int n, int d, int *odata, const int *idata) { int index = (blockDim.x*blockIdx.x) + threadIdx.x; @@ -37,18 +36,12 @@ namespace StreamCompaction { odata[index] = idata[index-1]; } - void printxxx(int n, const int *a) { - for (int i = 0; i < n; i++) { - printf("%d ", a[i]); - } - printf("\n\n\n"); - } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + int numThreads = 128; int numBlocks = (n + numThreads - 1) / numThreads; @@ -68,10 +61,13 @@ namespace StreamCompaction { cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); cudaMemcpy(dev_odata1, idata, n * sizeof(int), cudaMemcpyHostToDevice); + int *out1 = dev_idata; int *out2 = dev_odata1; + timer().startGpuTimer(); + for (int d = 1; d <= d_max; d++) { prefixSum<<>>(n, d, out2, out1); cudaMemcpy(out1, out2, n * sizeof(int), cudaMemcpyDeviceToDevice); @@ -79,6 +75,8 @@ namespace StreamCompaction { shiftRight<<>>(n, out2, out1); + timer().endGpuTimer(); + cudaMemcpy(odata, out2, n * sizeof(int), cudaMemcpyDeviceToHost); @@ -86,7 +84,7 @@ namespace StreamCompaction { cudaFree(dev_odata1); cudaFree(dev_odata2); - timer().endGpuTimer(); + } } } diff --git a/Project2-Stream-Compaction/stream_compaction/thrust.cu b/Project2-Stream-Compaction/stream_compaction/thrust.cu index f18f724..f6c5fc3 100644 --- a/Project2-Stream-Compaction/stream_compaction/thrust.cu +++ b/Project2-Stream-Compaction/stream_compaction/thrust.cu @@ -20,7 +20,7 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + // 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()); @@ -36,12 +36,16 @@ namespace StreamCompaction { thrust::device_ptr dev_idataItr(dev_idata); thrust::device_ptr dev_odataItr(dev_odata); + timer().startGpuTimer(); + thrust::exclusive_scan(dev_idataItr, dev_idataItr + n, dev_odataItr); + timer().endGpuTimer(); + cudaMemcpy(odata, dev_odata, n * sizeof(int), cudaMemcpyDeviceToHost); - timer().endGpuTimer(); + } } } From 02c8934a2402e9114649f2992e4f9cc32c82c38c Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 14:51:16 -0400 Subject: [PATCH 10/26] update readme. --- Project2-Character-Recognition/README.md | 100 ++++++++++++++++-- .../data/white_noise.png | Bin 0 -> 35295 bytes 2 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 Project2-Character-Recognition/data/white_noise.png diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index 4503fac..af872c6 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -1,14 +1,100 @@ CUDA Character Recognition ====================== -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** +**University of Pennsylvania, CIS 565: GPU Programming and Architecture, +Project 2 - Character Recognition** -* (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) +* Srinath Rajagopalan + * [LinkedIn](https://www.linkedin.com/in/srinath-rajagopalan-07a43155), [twitter](https://twitter.com/srinath132) +* Tested on: Windows 10, i7-6700 @ 3.4GHz 16GB, Nvidia Quadro P1000 4GB (Moore 100B Lab) -### (TODO: Your README) +### Neural Networks in CUDA for Character Recognition + + +![](data-set/01out.bmp) ![](data-set/07out.bmp) ![](data-set/37out.bmp) ![](data-set/52out.bmp) + +In this we project, I have implemented a feedforward neural network, from scratch, in C++ and CUDA to make efficient use of the GPU to parallelize the matrix operations. + +The network is trained to achieve a 100% accuracy on the provide character recognition dataset of 52 images with a resolution of 101x101. This network will NOT generalize to unseen test samples and that is not the focus of this project. The focus is on the performance and scalability comparisons and appreciating the role of the GPU. + +The network architecture is fixed to have one hidden layer but one can configure the image size, number of images, and the number of hidden neurons. + +## Computation Graph + +The setup is a 2-layer neural network with the following configuration +* Input - `X` of dimension `52 x 10201` +* Hidden - `X2` of dimension `52 x 10` with `ReLU` non-linearity and `10` hidden neurons +* Output - `probs` of dimension `52x52` predicted probability for each sample over 52 classes + +In effectively 2 matrix multiply operations, we compute the predictions for ALL `N` examples at the same time. It is helpful to think of the operations being performed on the matrices themselves and utilizing matrix differentiation to calculate the gradients for the backward pass. + +The following Python-Numpy, borrowed from [CS231n: CNNs for Visual Recognition](https://cs231n.github.io/neural-networks-case-study/), illustrates how we can vectorize the forward pass and backward pass computation. + +``` +### Forward Pass +# evaluate class scores with a 2-layer Neural Network +hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation +scores = np.dot(hidden_layer, W2) + b2 +exp_scores = np.exp(scores) +probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) + +### Evaluate Loss +correct_logprobs = -np.log(probs[range(num_examples),y]) +data_loss = np.sum(correct_logprobs)/num_examples + + +### Backward Pass +dscores = probs +dscores[range(num_examples),y] -= 1 +dscores /= num_examples # derivative wrt output layer + +dW2 = np.dot(hidden_layer.T, dscores) # derivative wrt W2 +db2 = np.sum(dscores, axis=0, keepdims=True) + +dhidden = np.dot(dscores, W2.T) # derivative wrt hidden layer +dhidden[hidden_layer <= 0] = 0 # derivative through ReLU + +dW = np.dot(X.T, dhidden) # derivative wrt W +db = np.sum(dhidden, axis=0, keepdims=True) +``` + +The above code is treated as an API one can adhere to. It's easy to see how easily all the above operations can be individually parallelized. To achieve this the following CUDA Kernels have been implemented + +* Matrix multiply on any two matrices of any size +* Slicing operations to fetch the scores of the correct class for each example +* ReLU activation +* Softmax activation +* Matrix addition +* Element-wise matrix multiplication +* Filter to zero-out negative elements in a matrix + +## Training Curves + +The training curve is plotted for the toy XOR operation and for the character recognition dataset. + +![](data/training_curve_XOR.png) + +![](data/training_curve.png) + + +Predictions on character-recognition is given below + +![](data/final_predictions.png) + +## Performance Analysis on Memorizing Random Images + +With 52 examples, and enough number of hidden neurons, we can achieve 100% accuracy on random images (101x101 resolution)! + +![](data/white_noise.png) + +The performance analysis is based on generating random images of different resolutions (changing `d`) and different number of hidden neurons (changing `h`) + +![](data/perf_image.png) + +![](data/perf_neurons.png) + + +## Extra Credit +The forward and backward pass is computed for all `N` exmaples simultaneously in one-shot using matrix-multiply and slicing operations. -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) diff --git a/Project2-Character-Recognition/data/white_noise.png b/Project2-Character-Recognition/data/white_noise.png new file mode 100644 index 0000000000000000000000000000000000000000..aefe249db04bcde4133b0625e5ce4a896f196a5a GIT binary patch literal 35295 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 literal 0 HcmV?d00001 From ad1e4cd07af7f75a0c824b84499ed199df531fdc Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 14:53:46 -0400 Subject: [PATCH 11/26] update readme. --- Project2-Stream-Compaction/README.md | 51 ++++++++++++++++++++++++---- README.md | 17 ++++------ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 0e38ddb..5f6b52c 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -1,14 +1,51 @@ CUDA Stream Compaction ====================== -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** +**University of Pennsylvania, CIS 565: GPU Programming and Architecture, +Project 2 - Stream Compaction** -* (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) +* Srinath Rajagopalan + * [LinkedIn](https://www.linkedin.com/in/srinath-rajagopalan-07a43155), [twitter](https://twitter.com/srinath132) +* Tested on: Windows 10, i7-6700 @ 3.4GHz 16GB, Nvidia Quadro P1000 4GB (Moore 100B Lab) -### (TODO: Your README) +### Scan and Stream Compaction -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +In this we project, I have implemented the exclusive scan and stream compaction algorithms for different configuratins +1) CPU scan and CPU stream compaction +2) Naive GPU scan +3) Work-Efficient GPU scan and stream compaction +5) Scan from Thrust (to benchmark) +Scan and Compaction can be best understood with the following example: + +* scan: + - goal: produce a prefix sum array of a given array (we only care about exclusive scan here) + - input + - [1 5 0 1 2 0 3] + - output + - [0 1 6 6 7 9 9] +* compact: + - goal: closely and neatly packed the elements != 0 + - input + - [1 5 0 1 2 0 3] + - output + - [1 5 1 2 3] + +### Performance Analysis +1) A block size that worked well for each of the configurations was `128`. I experimented with different size options from 256 to 512 and the performance remained similar. Decreasing the block size below `64` led to a drop in performance for all the GPU implementations. Thus, all the comparisons are benchmarked by fixing block size as 128. + +2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`. + + + ![](data/scan_perf_15.png) + + ![](data/scan_perf_20.png) + + ![](data/scan_perf_25.png) + + + ![](data/compact_perf_15.png) + + ![](data/compact_perf_20.png) + + ![](data/compact_perf_25.png) diff --git a/README.md b/README.md index 3a0b2fe..397690c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ CUDA Number Algorithms ====================== -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** +**University of Pennsylvania, CIS 565: GPU Programming and Architecture, +Project 2 - Number Algorithms** -* (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) +* Srinath Rajagopalan + * [LinkedIn](https://www.linkedin.com/in/srinath-rajagopalan-07a43155), [twitter](https://twitter.com/srinath132) +* Tested on: Windows 10, i7-6700 @ 3.4GHz 16GB, Nvidia Quadro P1000 4GB (Moore 100B Lab) -### (TODO: Your README) - -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.) +1) [Project-Stream Compaction](Project2-Stream-Compaction/README.md) +2) [Project-Character-Recognition](Project2-Character-Recognition/REAME.md) From f5d79c6fa2b6592c719c1685ea93e83591e8f0d1 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 14:55:18 -0400 Subject: [PATCH 12/26] fix readme. --- Project2-Character-Recognition/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index af872c6..4ba854b 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -72,7 +72,7 @@ The above code is treated as an API one can adhere to. It's easy to see how easi The training curve is plotted for the toy XOR operation and for the character recognition dataset. -![](data/training_curve_XOR.png) +![](data/training_curve_xor.png) ![](data/training_curve.png) From f73ad96382c82233ebcb5139856008c67e694691 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 14:57:32 -0400 Subject: [PATCH 13/26] fix readme. --- Project2-Character-Recognition/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index 4ba854b..89a574b 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -87,7 +87,7 @@ With 52 examples, and enough number of hidden neurons, we can achieve 100% accur ![](data/white_noise.png) -The performance analysis is based on generating random images of different resolutions (changing `d`) and different number of hidden neurons (changing `h`) +The performance analysis is based on generating random images of different resolutions (changing `d`) and different number of hidden neurons (changing `h`). Resolution could be scaled to a maximum of 1 million pixels before running out of memory. Number of hidden neurons could be scaled to a maximum of 10K neurons before running out of memeory. ![](data/perf_image.png) From 8e187a6013b71700a8c6a3e1c25dfc6755e7e163 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:34:45 -0400 Subject: [PATCH 14/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 5f6b52c..ae37711 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -31,6 +31,18 @@ Scan and Compaction can be best understood with the following example: - output - [1 5 1 2 3] +## GPU Implementation +### Naive Scan +Though exclusive scan looks like a sequential operation, we can parallelize by dividing the computation to `log(n)` passes through the array. In the first pass, we do `a[i] = a[i] + a[i-1]`. The invariant we maintain is that by the `k`th pass, we have the correct values in the array upto to the index `2^(k-1)` (excluded), and we perform the two-sum addition only for `index >= 2^(k-1)`. After `log(n)` passes, the array will contain an inclsuive scan which we convert to exclusive by a shift-right operation. By utilizing `n` threads, each pass requires `O(1)` time and we have `log(n)` passes. So totally the time complexity is `O(log(n))`. However, what about the total number of adds? A sequential scan will only perform `n` adds. The naive parallel implementation odes `O(n)` adds for each pass and total number of adds is therefore `O(nlogn)`. + +### Work-efficient Scan +Work-efficient implementation aims to bring down the total number of addition operations to `O(n)`. This approach uses a balanced-binary tree view to structure the computation. In the up-sweep phase, we go from the leaf nodes to the root and keep building partial-sums in-place. In the down-sweep phase, we traverse back from the root to make use of the partial-sum from before and build the scan in-place. This approach requires the array to be a power of 2, so when the array size is not one we pad it to the next 2-power. + +### Work-efficient Compaction + +In compaction, we to remove all elements from the array which do not satisfy a certain criterion. We first create a boolean array indicating which all elements satisfy the criteria. After this, we call the work-efficient scan on the _boolean array_ to build the prefix-sum. For all the elements which are going to be included in the final array, the exclusive scan gives at what _index_ they must be writtten to. This is done by a parallel scatter operation. + + ### Performance Analysis 1) A block size that worked well for each of the configurations was `128`. I experimented with different size options from 256 to 512 and the performance remained similar. Decreasing the block size below `64` led to a drop in performance for all the GPU implementations. Thus, all the comparisons are benchmarked by fixing block size as 128. @@ -49,3 +61,11 @@ Scan and Compaction can be best understood with the following example: ![](data/compact_perf_20.png) ![](data/compact_perf_25.png) + + +*What do the above graphs mean?* + +For smaller inputs, the CPU implementation is significantly faster than the GPU one. The GPU naive scan invokes a kernel `log(n)` times. There is a lot of overhead incurred while invoking a kernel withtin a loop. For small sizes it doesn't justify the cost. The overhead is compounded in work-efficient scan. So for small array sizes we are better off with the CPU. However, the CPU scan _also_ performs better than the naive one for large array sizes. Why? I am theorizing this to be because of the additional number of adds required in the parallel approach. This is also reflected in the fact that the the work-efficient scan is faster than the CPU implementation for the larger array size. Thrust, unsurprisingly, performs better than everything I have implemented. This is probably because, though the algorithmic complexity might still be the same, Thrust is making better utilizattion of hardware resources: using shared memory and reducing global memory calls, and also avoiding issues like bank conflicts (when accessing shared memory). + +Work-efficient compaction is significantly slower if use the same implementation as work-efficient scan one. This is because of additional `cudaMemcpy` calls from Host to Device and Device to Host. We can improve this by minimizing the CPU-GPU transfers by maintaining all the intermediate arrays in the GPU itself. But scan updates the array in place and we require the boolean array while performing scatter, so we have to copy it to another memory location on the GPU. This is a better alternative compared as it's a Device to Device transfer as opposed tot Device To Host. + From 9a1c0e05a36919c11e719147b9684e3daca75d8c Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:35:41 -0400 Subject: [PATCH 15/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index ae37711..6065d1a 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -69,3 +69,59 @@ For smaller inputs, the CPU implementation is significantly faster than the GPU Work-efficient compaction is significantly slower if use the same implementation as work-efficient scan one. This is because of additional `cudaMemcpy` calls from Host to Device and Device to Host. We can improve this by minimizing the CPU-GPU transfers by maintaining all the intermediate arrays in the GPU itself. But scan updates the array in place and we require the boolean array while performing scatter, so we have to copy it to another memory location on the GPU. This is a better alternative compared as it's a Device to Device transfer as opposed tot Device To Host. +### Tests output + +``` +**************** +** SCAN TESTS ** +**************** + [ 8 18 36 48 30 4 0 24 16 19 40 47 23 ... 23 0 ] +==== cpu scan, power-of-two ==== + elapsed time: 0.0017ms (std::chrono Measured) + [ 0 8 26 62 110 140 144 144 168 184 203 243 290 ... 25535 25558 ] +==== cpu scan, non-power-of-two ==== + elapsed time: 0.0016ms (std::chrono Measured) + [ 0 8 26 62 110 140 144 144 168 184 203 243 290 ... 25468 25509 ] + passed +==== naive scan, power-of-two ==== + elapsed time: 0.251904ms (CUDA Measured) + passed +==== naive scan, non-power-of-two ==== + elapsed time: 0.058368ms (CUDA Measured) + passed +==== work-efficient scan, power-of-two ==== + elapsed time: 0.099328ms (CUDA Measured) + passed +==== work-efficient scan, non-power-of-two ==== + elapsed time: 0.142368ms (CUDA Measured) + passed +==== thrust scan, power-of-two ==== + elapsed time: 0.05264ms (CUDA Measured) + passed +==== thrust scan, non-power-of-two ==== + elapsed time: 0.082112ms (CUDA Measured) + passed + +***************************** +** STREAM COMPACTION TESTS ** +***************************** + [ 2 2 2 2 0 0 0 2 0 1 0 3 3 ... 1 0 ] +==== cpu compact without scan, power-of-two ==== + elapsed time: 0.0026ms (std::chrono Measured) + [ 2 2 2 2 2 1 3 3 2 2 1 3 2 ... 1 1 ] + passed +==== cpu compact without scan, non-power-of-two ==== + elapsed time: 0.0164ms (std::chrono Measured) + [ 2 2 2 2 2 1 3 3 2 2 1 3 2 ... 3 1 ] + passed +==== cpu compact with scan ==== + elapsed time: 0.124ms (std::chrono Measured) + [ 2 2 2 2 2 1 3 3 2 2 1 3 2 ... 1 1 ] + passed +==== work-efficient compact, power-of-two ==== + elapsed time: 0.459776ms (CUDA Measured) + passed +==== work-efficient compact, non-power-of-two ==== + elapsed time: 0.529408ms (CUDA Measured) + passed``` + From ec7662a36e5966233d727b0c53488268c100e116 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:36:32 -0400 Subject: [PATCH 16/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 6065d1a..0f4d9e5 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -63,7 +63,7 @@ In compaction, we to remove all elements from the array which do not satisfy a c ![](data/compact_perf_25.png) -*What do the above graphs mean?* +**What do the above graphs mean?** For smaller inputs, the CPU implementation is significantly faster than the GPU one. The GPU naive scan invokes a kernel `log(n)` times. There is a lot of overhead incurred while invoking a kernel withtin a loop. For small sizes it doesn't justify the cost. The overhead is compounded in work-efficient scan. So for small array sizes we are better off with the CPU. However, the CPU scan _also_ performs better than the naive one for large array sizes. Why? I am theorizing this to be because of the additional number of adds required in the parallel approach. This is also reflected in the fact that the the work-efficient scan is faster than the CPU implementation for the larger array size. Thrust, unsurprisingly, performs better than everything I have implemented. This is probably because, though the algorithmic complexity might still be the same, Thrust is making better utilizattion of hardware resources: using shared memory and reducing global memory calls, and also avoiding issues like bank conflicts (when accessing shared memory). From 99cc684086f683ec954527a65aea36217f3783c1 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:41:45 -0400 Subject: [PATCH 17/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 0f4d9e5..a9e0a68 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -49,18 +49,11 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`. - ![](data/scan_perf_15.png) + - ![](data/scan_perf_20.png) + - ![](data/scan_perf_25.png) - - - ![](data/compact_perf_15.png) - - ![](data/compact_perf_20.png) - - ![](data/compact_perf_25.png) + **What do the above graphs mean?** From 74fae9f0e67cf5e576eeadb35d0720fcaf14f173 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:42:30 -0400 Subject: [PATCH 18/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index a9e0a68..3960114 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -49,7 +49,7 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`. - + From 6e491b8074c9bf5337abbab84dd42b0f911e2e2b Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:43:37 -0400 Subject: [PATCH 19/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 3960114..d09fe4a 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -48,9 +48,9 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`. - +

- +

From af663cddf673d73710e98cab1bb29b8a5f0265e4 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:43:58 -0400 Subject: [PATCH 20/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index d09fe4a..d974d8e 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -49,7 +49,7 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`.

- +

From 842b6f1a91298d87efb75003ef3615cc2e8eaf4f Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:45:16 -0400 Subject: [PATCH 21/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index d974d8e..311dd75 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -49,7 +49,7 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`.

- +

From ce43da687b8e70391aadc47c973a668e63de742c Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:45:37 -0400 Subject: [PATCH 22/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 311dd75..7a808cd 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -49,7 +49,7 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`.

- +

From ca473869a6705c57bad2b858001a485266aa4912 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:47:17 -0400 Subject: [PATCH 23/26] Updated readme for compactiton. --- Project2-Stream-Compaction/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Project2-Stream-Compaction/README.md b/Project2-Stream-Compaction/README.md index 7a808cd..d3ce611 100644 --- a/Project2-Stream-Compaction/README.md +++ b/Project2-Stream-Compaction/README.md @@ -48,13 +48,15 @@ In compaction, we to remove all elements from the array which do not satisfy a c 2) Performannce graphs comparing the differnet implementations are included below. For work-efficient compaction, I implemented two versions, one of which is inefficient but passes all the test cases for array sizes upto 2^25. This implementation calls the work-efficient scan function implemented as a part of the scan setup. But to stick to the API, it also does several `cudaMemcpy` from device to host and host to device which is not required if the interemediate scan buffers are never going to be needed on the CPU. However, the more efficient version is not passing the test case for not-power of two for array size `2^25`. -

+

- - - - +

+ +

+

+ +

**What do the above graphs mean?** From 69bc0a113166b0213bbdf4a55d14dd15039ad828 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:54:14 -0400 Subject: [PATCH 24/26] Update readme for character recognition. --- Project2-Character-Recognition/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Project2-Character-Recognition/README.md b/Project2-Character-Recognition/README.md index 89a574b..6351544 100644 --- a/Project2-Character-Recognition/README.md +++ b/Project2-Character-Recognition/README.md @@ -94,6 +94,12 @@ The performance analysis is based on generating random images of different resol ![](data/perf_neurons.png) +As we scale to bigger models and higher resolution images, memory is definitely a main bottleneck. Fitting the enitre dataset into the GPU global memory will be impossible. So we have to work the CPU hard disk. From a computatiton perspective, there can be several improvements + +1) Efficient matrix-multiply operations by the tiling approach using shared memory. As of now, I am a doing a naive matrix multiplication which is doing many redundant global memory access. + +2) Loss calculation can be parallelized by using parallel-reduction written in stream-compaction. The tricky part is to tweak the implementation to work access the index in a 2D view. + ## Extra Credit The forward and backward pass is computed for all `N` exmaples simultaneously in one-shot using matrix-multiply and slicing operations. From a71857d89f6f09f03e696b3ad69d96a73df18012 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Tue, 17 Sep 2019 21:55:05 -0400 Subject: [PATCH 25/26] update readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 397690c..eceb145 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,4 @@ Project 2 - Number Algorithms** 1) [Project-Stream Compaction](Project2-Stream-Compaction/README.md) -2) [Project-Character-Recognition](Project2-Character-Recognition/REAME.md) +2) [Project-Character-Recognition](Project2-Character-Recognition/README.md) From 77f7cdd764668db6ff689290714c7213f4ee3b13 Mon Sep 17 00:00:00 2001 From: Srinath Rajagopalan Date: Sun, 13 Oct 2019 02:53:00 -0400 Subject: [PATCH 26/26] Update main.cpp --- Project2-Character-Recognition/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project2-Character-Recognition/src/main.cpp b/Project2-Character-Recognition/src/main.cpp index cd5d146..0f0c2ab 100644 --- a/Project2-Character-Recognition/src/main.cpp +++ b/Project2-Character-Recognition/src/main.cpp @@ -61,10 +61,10 @@ void fillImage(float *X, int *y) { for (int i = 1; i <= 52; i++) { std::string fileName; if (i <= 9) { - fileName = "C:\\\\Users\\sri07\\Desktop\\Project2-Number-Algorithms\\Project2-Character-Recognition\\data-set\\0" + std::to_string(i) + "info.txt"; + fileName = "../data-set/0" + std::to_string(i) + "info.txt"; } else { - fileName = "C:\\\\Users\\sri07\\Desktop\\Project2-Number-Algorithms\\Project2-Character-Recognition\\data-set\\" + std::to_string(i) + "info.txt"; + fileName = "../data-set\\" + std::to_string(i) + "info.txt"; } std::ifstream myfile(fileName);