From 5af4492467e89abc781d06c340c22363503ce1e9 Mon Sep 17 00:00:00 2001 From: Shineng Tang <45440408+jaAcKrABbit@users.noreply.github.com> Date: Sun, 18 Sep 2022 12:04:29 -0400 Subject: [PATCH 1/5] finished optimizing block counts --- src/main.cpp | 16 ++-- stream_compaction/common.cu | 14 ++++ stream_compaction/common.h | 1 + stream_compaction/cpu.cu | 32 +++++++- stream_compaction/efficient.cu | 136 ++++++++++++++++++++++++++++++++- stream_compaction/naive.cu | 43 ++++++++++- stream_compaction/thrust.cu | 6 ++ 7 files changed, 233 insertions(+), 15 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 896ac2b..7ea8a56 100644 --- a/src/main.cpp +++ b/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 << 11; // 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]; @@ -51,7 +51,7 @@ int main(int argc, char* argv[]) { printDesc("naive scan, power-of-two"); StreamCompaction::Naive::scan(SIZE, c, a); printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); + printArray(SIZE, c, true); printCmpResult(SIZE, b, c); /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan @@ -71,28 +71,28 @@ int main(int argc, char* argv[]) { printDesc("work-efficient scan, power-of-two"); StreamCompaction::Efficient::scan(SIZE, c, a); printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); + 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); + 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); + 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); + printArray(NPOT, c, true); printCmpResult(NPOT, b, c); printf("\n"); @@ -137,14 +137,14 @@ int main(int argc, char* argv[]) { 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); + printArray(count, c, true); printCmpLenResult(count, expectedCount, b, c); zeroArray(SIZE, c); printDesc("work-efficient compact, non-power-of-two"); count = StreamCompaction::Efficient::compact(NPOT, c, a); printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); + printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); system("pause"); // stop Win32 console from closing on exit diff --git a/stream_compaction/common.cu b/stream_compaction/common.cu index 2ed6d63..d845250 100644 --- a/stream_compaction/common.cu +++ b/stream_compaction/common.cu @@ -24,6 +24,13 @@ namespace StreamCompaction { */ __global__ void kernMapToBoolean(int n, int *bools, const int *idata) { // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + + bools[index] = idata[index] == 0 ? 0 : 1; + } /** @@ -33,6 +40,13 @@ namespace StreamCompaction { __global__ void kernScatter(int n, int *odata, const int *idata, const int *bools, const int *indices) { // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + if (bools[index] == 1) { + odata[indices[index]] = idata[index]; + } } } diff --git a/stream_compaction/common.h b/stream_compaction/common.h index d2c1fed..57f97ec 100644 --- a/stream_compaction/common.h +++ b/stream_compaction/common.h @@ -9,6 +9,7 @@ #include #include #include +#include #define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) #define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index 719fa11..eb5d13f 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -20,6 +20,10 @@ namespace StreamCompaction { void scan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + odata[0] = 0; + for (int i = 1; i < n; i++) { + odata[i] = odata[i - 1] + idata[i - 1]; + } timer().endCpuTimer(); } @@ -31,8 +35,14 @@ namespace StreamCompaction { int compactWithoutScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + int count = 0; + for (int i = 0; i < n; i++) { + if (idata[i] != 0) { + odata[count++] = idata[i]; + } + } timer().endCpuTimer(); - return -1; + return count; } /** @@ -43,8 +53,26 @@ namespace StreamCompaction { int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); // TODO + int* temp = new int[n]; + for (int i = 0; i < n; i++) { + temp[i] = idata[i] == 0 ? 0 : 1; + } + //scan result + odata[0] = 0; + for (int i = 1; i < n; i++) { + odata[i] = odata[i - 1] + temp[i - 1]; + } + //int count = odata[n - 1]; + int count = 0; + for (int i = 0; i < n; i++) { + if (idata[i] != 0) { + odata[odata[i]] = idata[i]; + count++; + } + } timer().endCpuTimer(); - return -1; + delete temp; + return count; } } } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 2db346e..ace1118 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -3,6 +3,10 @@ #include "common.h" #include "efficient.h" + +#define blockSize 128 +#define THREAD_OPTIMIZATION 1; + namespace StreamCompaction { namespace Efficient { using StreamCompaction::Common::PerformanceTimer; @@ -12,13 +16,101 @@ namespace StreamCompaction { return timer; } - /** + __global__ void kernUpSweep(int n, int d, int* data){ + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k >= n) { + return; + } + +#if THREAD_OPTIMIZATION + int stride = 1 << (d + 1); + k = (k + 1) * stride - 1; + data[k] += data[k - (stride >> 1)]; +#else + int stride = 1 << (d + 1); + if (k % stride == 0) { + data[k + stride - 1] += data[k + (1 << d) - 1]; + } +#endif + + + + + } + + __global__ void kernDownSweep(int n, int d, int* data) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k >= n) { + return; + } + + +#if THREAD_OPTIMIZATION + int stride = 1 << (d + 1); + k = (k + 1) * stride - 1; + int t = data[k - (stride >> 1)]; + data[k - (stride >> 1)] = data[k]; + data[k] += t; +#else + int stride = 1 << (d + 1); + int pow_2 = 1 << d; + if (k % stride == 0) { + int t = data[k + pow_2 - 1]; + data[k + pow_2 - 1] = data[k + stride - 1]; + data[k + stride - 1] = t + data[k + stride - 1]; + } +#endif + } + + void perfixSumScan(int size, int* idata) { + dim3 fullBlocksPerGrid((size + blockSize - 1) / blockSize); + int threads = size; + for (int d = 0; d < ilog2ceil(size); d++) { +#if THREAD_OPTIMIZATION + dim3 blocksPerGrid((threads + blockSize - 1) / blockSize); + threads /= 2; + kernUpSweep <<< blocksPerGrid, blockSize >> > (threads, d, idata); + +#else + kernUpSweep << < fullBlocksPerGrid, blockSize >> > (size, d, idata); +#endif + } + //set last element to 0 + + int val = 0; + cudaMemcpy(&idata[size - 1], &val, sizeof(int), cudaMemcpyHostToDevice); + + //threads is already 1 + for (int d = ilog2ceil(size) - 1; d >= 0; d--) { +#if THREAD_OPTIMIZATION + dim3 blocksPerGrid((threads + blockSize - 1) / blockSize); + kernDownSweep << < blocksPerGrid, blockSize >> > (threads, d, idata); + threads *= 2; +#else + kernDownSweep << < fullBlocksPerGrid, blockSize >> > (size, d, idata); +#endif + } + } + /**uyb * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + int* dev_data; + + //for non-pow2 + int size = 1 << ilog2ceil(n); + cudaMalloc((void**)&dev_data, size * sizeof(int)); + cudaMemcpy(dev_data, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + + timer().startGpuTimer(); // TODO + perfixSumScan(size, dev_data); timer().endGpuTimer(); + + cudaMemcpy(odata, dev_data, sizeof(int) * n, cudaMemcpyDeviceToHost); + + cudaFree(dev_data); } /** @@ -31,10 +123,48 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + int* dev_bool; + int* dev_idata; + int* dev_odata; + int* dev_scanResult; + + int count = 0; + int lastBool = 0; + + int size = 1 << ilog2ceil(n); + + cudaMalloc((void**)&dev_bool, n * sizeof(int)); + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + cudaMalloc((void**)&dev_odata, n * sizeof(int)); + cudaMalloc((void**)&dev_scanResult, size * sizeof(int)); + + + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + timer().startGpuTimer(); // TODO + Common::kernMapToBoolean << > > (n, dev_bool, dev_idata); + cudaMemcpy(dev_scanResult, dev_bool, n * sizeof(int), cudaMemcpyDeviceToDevice); + perfixSumScan(size, dev_scanResult); + Common::kernScatter << > > (n, dev_odata, dev_idata, dev_bool, dev_scanResult); timer().endGpuTimer(); - return -1; + + //last boolean is not counted in exclusive scan, if last bool is 1, need to take this into account + //This is not shown in the slides + cudaMemcpy(&count, dev_scanResult + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + cudaMemcpy(&lastBool, dev_bool + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + + int length = count + lastBool; + cudaMemcpy(odata, dev_odata, sizeof(int) * length, cudaMemcpyDeviceToHost); + + cudaFree(dev_bool); + cudaFree(dev_idata); + cudaFree(dev_odata); + cudaFree(dev_scanResult); + return length; } } } diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 4308876..4299dbf 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -3,6 +3,10 @@ #include "common.h" #include "naive.h" + + +#define blockSize 128 + namespace StreamCompaction { namespace Naive { using StreamCompaction::Common::PerformanceTimer; @@ -12,14 +16,49 @@ namespace StreamCompaction { return timer; } // TODO: __global__ - + __global__ void kernScan(int n, int d, int* odata, const int* idata) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k >= n) { + return; + } + int stride = 1 << (d - 1); + if (k >= stride) { + odata[k] = idata[k - stride] + idata[k]; + } + else { + odata[k] = idata[k]; + } + } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. - */ + */ void scan(int n, int *odata, const int *idata) { + int* dev_idata; + int* dev_odata; + + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + cudaMalloc((void**)&dev_odata, n * sizeof(int)); + + cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + // cudaMemcpy(dev_odata, odata, sizeof(int) * n, cudaMemcpyHostToDevice); + timer().startGpuTimer(); // TODO + for (int d = 1; d <= ilog2ceil(n); d++) { + kernScan << > > (n, d, dev_odata, dev_idata); + std::swap(dev_odata, dev_idata); + cudaMemcpy(odata , dev_idata, sizeof(int) * (n), cudaMemcpyDeviceToHost); + } timer().endGpuTimer(); + //convert from inclusive scan to exclusive scan + odata[0] = 0; + cudaMemcpy(odata + 1, dev_idata, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); + + cudaFree(dev_idata); + cudaFree(dev_odata); + } } } diff --git a/stream_compaction/thrust.cu b/stream_compaction/thrust.cu index 1def45e..ed16d1e 100644 --- a/stream_compaction/thrust.cu +++ b/stream_compaction/thrust.cu @@ -18,11 +18,17 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + thrust::host_vector host_data(idata, idata + n); + thrust::device_vector dev_in(host_data); + thrust::device_vector dev_out(host_data); 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()); + thrust::exclusive_scan(dev_in.begin(), dev_in.end(), dev_out.begin()); timer().endGpuTimer(); + + cudaMemcpy(odata, dev_out.data().get(), sizeof(int) * n, cudaMemcpyDeviceToHost); } } } From 84da4e8c442a5b6e64daf5ca3976687da0dfd4f9 Mon Sep 17 00:00:00 2001 From: Shineng Tang <45440408+jaAcKrABbit@users.noreply.github.com> Date: Sun, 18 Sep 2022 18:48:41 -0400 Subject: [PATCH 2/5] implemented radix sort --- src/main.cpp | 36 ++++++++++++++- stream_compaction/cpu.cu | 6 +++ stream_compaction/cpu.h | 2 + stream_compaction/efficient.cu | 81 +++++++++++++++++++++++++++++++++- stream_compaction/efficient.h | 2 + 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 7ea8a56..595fa22 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,9 +11,10 @@ #include #include #include +#include #include "testing_helpers.hpp" -const int SIZE = 1 << 11; // feel free to change the size of array +const int SIZE = 1 << 21; // 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]; @@ -147,6 +148,39 @@ int main(int argc, char* argv[]) { printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); + printf("\n"); + printf("*****************************\n"); + printf("** RADIX SORT TESTS **\n"); + printf("*****************************\n"); + + genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case + a[SIZE - 1] = 0; + memcpy(b, a, SIZE * sizeof(int)); + /*for (int i = 0; i < SIZE; i++) { + b[i] = a[i]; + }*/ + // printArray(SIZE, a, true); + zeroArray(SIZE, c); + printDesc("radix sort, power-of-two"); + StreamCompaction::Efficient::radixSort(SIZE, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(SIZE, c, true); + StreamCompaction::CPU::sort(SIZE, b); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + zeroArray(SIZE, b); + memcpy(b, a, NPOT * sizeof(int)); + printDesc("radix sort, non-power-of-two"); + StreamCompaction::Efficient::radixSort(NPOT, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(NPOT, c, true); + StreamCompaction::CPU::sort(NPOT, b); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printCmpResult(NPOT, b, c); + + system("pause"); // stop Win32 console from closing on exit delete[] a; delete[] b; diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index eb5d13f..fcbb505 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -74,5 +74,11 @@ namespace StreamCompaction { delete temp; return count; } + + void sort(int n, int* idata) { + timer().startCpuTimer(); + std::sort(idata, idata + n); + timer().endCpuTimer(); + } } } diff --git a/stream_compaction/cpu.h b/stream_compaction/cpu.h index 873c047..9ca5664 100644 --- a/stream_compaction/cpu.h +++ b/stream_compaction/cpu.h @@ -11,5 +11,7 @@ namespace StreamCompaction { int compactWithoutScan(int n, int *odata, const int *idata); int compactWithScan(int n, int *odata, const int *idata); + + void sort(int n, int* idata); } } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index ace1118..a5030f9 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -1,5 +1,6 @@ #include #include +#include #include "common.h" #include "efficient.h" @@ -140,8 +141,6 @@ namespace StreamCompaction { cudaMalloc((void**)&dev_odata, n * sizeof(int)); cudaMalloc((void**)&dev_scanResult, size * sizeof(int)); - - cudaMemcpy(dev_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); timer().startGpuTimer(); @@ -164,7 +163,85 @@ namespace StreamCompaction { cudaFree(dev_idata); cudaFree(dev_odata); cudaFree(dev_scanResult); + return length; } + + + __global__ void kernNegate(int n, int k, int* data, int* bools) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + + int curBit = (data[index] & (1 << k)) >> k; + bools[index] = curBit == 1 ? 0 : 1; + + } + + __global__ void kernSplit(int n, int k, int totalFalses, int* scan, int* idata, int* odata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + + int cur = idata[index]; + int curBit = (cur & (1 << k)) >> k; + int scanIdx = scan[index]; + // odata[index] = curBit == 0 ? idata[scanIdx] : idata[index - scanIdx + totalFalses]; + if (curBit == 0) { + odata[scanIdx] = cur; + } else { + odata[index - scanIdx + totalFalses] = cur; + } + + } + + + void radixSort(int n, int* odata, const int* idata) { + + int* dev_idata; + int* dev_odata; + int* dev_scanResult; + int* dev_bool; + + int size = 1 << ilog2ceil(n); + int lastBool = 0; + int lastCount = 0; + int totalFalses = 0; + + dim3 fullBlocksPerGrid((n + blockSize - 1) / blockSize); + + cudaMalloc((void**)&dev_idata, n * sizeof(int)); + cudaMalloc((void**)&dev_odata, n * sizeof(int)); + cudaMalloc((void**)&dev_bool, n * sizeof(int)); + cudaMalloc((void**)&dev_scanResult, size * sizeof(int)); + + cudaMemcpy(dev_idata, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + timer().startGpuTimer(); + + int max = *std::max_element(idata, idata + n); + int numBits = ilog2ceil(max); + + for (int i = 0; i < numBits; i++) { + kernNegate << > > (n, i, dev_idata, dev_bool); + cudaMemcpy(&lastBool, dev_bool + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + cudaMemcpy(dev_scanResult, dev_bool, n * sizeof(int), cudaMemcpyDeviceToDevice); + perfixSumScan(size, dev_scanResult); + cudaMemcpy(&lastCount, dev_scanResult + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + totalFalses = lastBool + lastCount; + kernSplit << > > (n, i, totalFalses, dev_scanResult, dev_idata, dev_odata); + std::swap(dev_idata, dev_odata); + } + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata, n * sizeof(int), cudaMemcpyDeviceToHost); + + cudaFree(dev_idata); + cudaFree(dev_odata); + cudaFree(dev_bool); + cudaFree(dev_scanResult); + } } } diff --git a/stream_compaction/efficient.h b/stream_compaction/efficient.h index 803cb4f..8efb4f4 100644 --- a/stream_compaction/efficient.h +++ b/stream_compaction/efficient.h @@ -9,5 +9,7 @@ namespace StreamCompaction { void scan(int n, int *odata, const int *idata); int compact(int n, int *odata, const int *idata); + + void radixSort(int n, int* odata, const int* idata); } } From 4c549ed21dfedb6d3acd59ab91fef53c04df46e1 Mon Sep 17 00:00:00 2001 From: Shineng Tang <45440408+jaAcKrABbit@users.noreply.github.com> Date: Sun, 18 Sep 2022 22:22:21 -0400 Subject: [PATCH 3/5] Update naive.cu --- stream_compaction/naive.cu | 1 - 1 file changed, 1 deletion(-) diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 4299dbf..77f5a02 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -49,7 +49,6 @@ namespace StreamCompaction { for (int d = 1; d <= ilog2ceil(n); d++) { kernScan << > > (n, d, dev_odata, dev_idata); std::swap(dev_odata, dev_idata); - cudaMemcpy(odata , dev_idata, sizeof(int) * (n), cudaMemcpyDeviceToHost); } timer().endGpuTimer(); //convert from inclusive scan to exclusive scan From cbecc91f35bdacb1128eed353527525ad1c756c6 Mon Sep 17 00:00:00 2001 From: Shineng Tang <45440408+jaAcKrABbit@users.noreply.github.com> Date: Sun, 18 Sep 2022 23:03:56 -0400 Subject: [PATCH 4/5] update images --- img/Stream-compaction.png | Bin 0 -> 22696 bytes img/comparison1.png | Bin 0 -> 21686 bytes img/comparison2.png | Bin 0 -> 21612 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/Stream-compaction.png create mode 100644 img/comparison1.png create mode 100644 img/comparison2.png diff --git a/img/Stream-compaction.png b/img/Stream-compaction.png new file mode 100644 index 0000000000000000000000000000000000000000..61fc07ad9423c2cfbfc4254d9e7217cbe3d588c9 GIT binary patch literal 22696 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�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 L;Z%P*;MRWt0>uzN literal 0 HcmV?d00001 diff --git a/img/comparison2.png b/img/comparison2.png new file mode 100644 index 0000000000000000000000000000000000000000..592d11aea85bfae816f372ba64c1b81720bda6a8 GIT binary patch literal 21612 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 Date: Sun, 18 Sep 2022 23:04:18 -0400 Subject: [PATCH 5/5] update readme --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0e38ddb..59f0a2a 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,116 @@ CUDA Stream Compaction **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** -* (TODO) YOUR NAME HERE - * (TODO) [LinkedIn](), [personal website](), [twitter](), etc. -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +* Shineng Tang + * [LinkedIn](https://www.linkedin.com/in/shineng-t-224192195/) +* Tested on: Windows 11, i9-10900k @3.7GHz 32GB, RTX 3090 24GB -### (TODO: Your README) -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +# Project Description +This project implements a few different versions of the **Scan** (_Prefix Sum_) algorithm and stream compaction in CUDA. +![](img/Stream-compaction.png) + + +## Main Features: +* `StreamCompaction::CPU::scan`: compute an exclusive prefix sum. +* `StreamCompaction::CPU::compactWithoutScan`: stream compaction without using + the `scan` function. +* `StreamCompaction::CPU::compactWithScan`: stream compaction using the `scan` + function. +* `StreamCompaction::Naive::scan`: A naive parallel GPU scan +* `StreamCompaction::Efficient::scan`: A **work-efficient** parallel GPU scan using _upsweep_ and _downsweep_ +* `StreamCompaction::Thrust::scan`: A short function which wraps a call to the **Thrust** library +* `StreamCompaction::Efficient::compact`: A string compaction funtion in CUDA + +## Extra Credit Features: +* `StreamCompaction::Efficient::radixSort`: I implemented CUDA based radix sort. It is noticably faster than `std::sort` when dealing with large size array. The test cases are shown at the bottom of the **Test Outputs** below. +* Threads optimization: By rearranging the usage of the threads, and reducing the blockcount when doing upsweep and downsweep, the performance increases dramatically. I set a macro to toggle the thread-optimization mode. + + +# Test Outputs +``` + +**************** +** SCAN TESTS ** +**************** + [ 47 21 28 13 2 26 1 14 4 49 25 20 43 ... 15 0 ] +==== cpu scan, power-of-two ==== + elapsed time: 2.729ms (std::chrono Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332490 51332505 ] +==== cpu scan, non-power-of-two ==== + elapsed time: 2.7305ms (std::chrono Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332413 51332419 ] + passed +==== naive scan, power-of-two ==== + elapsed time: 19.9673ms (CUDA Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332490 51332505 ] + passed +==== naive scan, non-power-of-two ==== + elapsed time: 18.4846ms (CUDA Measured) + passed +==== work-efficient scan, power-of-two ==== + elapsed time: 0.527424ms (CUDA Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332490 51332505 ] + passed +==== work-efficient scan, non-power-of-two ==== + elapsed time: 0.519136ms (CUDA Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332413 51332419 ] + passed +==== thrust scan, power-of-two ==== + elapsed time: 0.51376ms (CUDA Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332490 51332505 ] + passed +==== thrust scan, non-power-of-two ==== + elapsed time: 0.469344ms (CUDA Measured) + [ 0 47 68 96 109 111 137 138 152 156 205 230 250 ... 51332413 51332419 ] + passed + +***************************** +** STREAM COMPACTION TESTS ** +***************************** + [ 1 3 0 1 0 2 1 0 0 1 1 0 1 ... 3 0 ] +==== cpu compact without scan, power-of-two ==== + elapsed time: 4.1334ms (std::chrono Measured) + [ 1 3 1 2 1 1 1 1 3 3 2 2 2 ... 2 3 ] + passed +==== cpu compact without scan, non-power-of-two ==== + elapsed time: 4.115ms (std::chrono Measured) + [ 1 3 1 2 1 1 1 1 3 3 2 2 2 ... 2 1 ] + passed +==== cpu compact with scan ==== + elapsed time: 6.5739ms (std::chrono Measured) + [ 1 3 1 2 1 1 1 1 3 3 2 2 2 ... 2 3 ] + passed +==== work-efficient compact, power-of-two ==== + elapsed time: 0.602784ms (CUDA Measured) + [ 1 3 1 2 1 1 1 1 3 3 2 2 2 ... 2 3 ] + passed +==== work-efficient compact, non-power-of-two ==== + elapsed time: 0.604032ms (CUDA Measured) + [ 1 3 1 2 1 1 1 1 3 3 2 2 2 ... 2 1 ] + passed + +***************************** +** RADIX SORT TESTS ** +***************************** +==== radix sort, power-of-two ==== + elapsed time: 6.89866ms (CUDA Measured) + [ 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 49 49 ] + elapsed time: 34.7704ms (std::chrono Measured) + passed +==== radix sort, non-power-of-two ==== + elapsed time: 6.59078ms (CUDA Measured) + [ 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 49 49 ] + elapsed time: 35.2076ms (std::chrono Measured) + passed +``` +# Performance Analysis + +![](img/comparison1.png) + +For the **scan algorithm**, I notice that when dealing with relatively small-sized arrays, the cpu version is slightly faster than any gpu implementation, even Thrust. When I increase the size of the array, for example, at array size 2^14, the supposedly faster implementation is slower than any other ones. However, when the array size reaches a bigger number 2^22, the performance of the work-efficent scan is already fairly close to the thrust function. Another thing I notice is that the naive GPU scan does not surpass the CPU scan until approximately 2^19. This is because of the usage of global memory and no threads optimization which leads to divergency. + +![](img/comparison2.png) + +For the **stream compaction**, the pattern of the chart is similar to the scan function. When dealing with large-sized data, GPU is always faster. \ No newline at end of file