From e4fabe9eb8c44b03e803f21cd0a1b68220c44c96 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 14:02:37 -0400 Subject: [PATCH 01/31] Working for 256 size --- src/main.cpp | 49 ++++++----- stream_compaction/common.cu | 11 +++ stream_compaction/cpu.cu | 60 +++++++++++++- stream_compaction/efficient.cu | 147 ++++++++++++++++++++++++++++++++- stream_compaction/naive.cu | 87 ++++++++++++++++++- stream_compaction/thrust.cu | 20 ++++- 6 files changed, 344 insertions(+), 30 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3d5c8820..b0b84d8d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,11 +13,11 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 8; // feel free to change the size of array +const int SIZE = 1 << 9; // 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]; +int* a = new int[SIZE]; +int* b = new int[SIZE]; +int* c = new int[SIZE]; int main(int argc, char* argv[]) { // Scan tests @@ -29,7 +29,7 @@ int main(int argc, char* argv[]) { genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; - printArray(SIZE, a, true); + // 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. @@ -38,20 +38,20 @@ int main(int argc, char* argv[]) { printDesc("cpu scan, power-of-two"); StreamCompaction::CPU::scan(SIZE, b, a); printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(SIZE, b, true); + 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, c, true); + printArray(NPOT, c, 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); + printArray(SIZE, c, true); printCmpResult(SIZE, b, c); /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan @@ -64,35 +64,44 @@ int main(int argc, char* argv[]) { printDesc("naive scan, non-power-of-two"); StreamCompaction::Naive::scan(NPOT, c, a); printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); + 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); + 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); + int* myTest = new int[SIZE] {0, 1, 2, 3, 4, 5, 6, 7}; + int* myResult = new int[SIZE] {0, 1, 2, 6, 4, 9, 6, 28}; + printDesc("work-efficient scan, upSweep test"); + StreamCompaction::Efficient::scan(SIZE, c, myTest); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + printArray(SIZE, c, true); + printCmpResult(SIZE, myResult, 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"); @@ -104,7 +113,7 @@ int main(int argc, char* argv[]) { genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; - printArray(SIZE, a, true); + // printArray(SIZE, a, true); int count, expectedCount, expectedNPOT; @@ -115,7 +124,7 @@ int main(int argc, char* argv[]) { count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); expectedCount = count; - printArray(count, b, true); + // printArray(count, b, true); printCmpLenResult(count, expectedCount, b, b); zeroArray(SIZE, c); @@ -123,32 +132,32 @@ int main(int argc, char* argv[]) { count = StreamCompaction::CPU::compactWithoutScan(NPOT, c, a); printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); expectedNPOT = count; - printArray(count, c, true); + // printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); zeroArray(SIZE, c); printDesc("cpu compact with scan"); count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(count, c, true); + // printArray(count, c, true); printCmpLenResult(count, expectedCount, b, c); zeroArray(SIZE, c); printDesc("work-efficient compact, power-of-two"); count = StreamCompaction::Efficient::compact(SIZE, c, a); printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); + // 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 delete[] a; delete[] b; delete[] c; -} +} \ No newline at end of file diff --git a/stream_compaction/common.cu b/stream_compaction/common.cu index 2ed6d630..01b41c98 100644 --- a/stream_compaction/common.cu +++ b/stream_compaction/common.cu @@ -1,4 +1,5 @@ #include "common.h" +#include void checkCUDAErrorFn(const char *msg, const char *file, int line) { cudaError_t err = cudaGetLastError(); @@ -24,6 +25,11 @@ namespace StreamCompaction { */ __global__ void kernMapToBoolean(int n, int *bools, const int *idata) { // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index < n) { + bools[index] = (idata[index] != 0) ? 1 : 0; + } + } /** @@ -33,6 +39,11 @@ 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 && bools[index]) { + odata[indices[index]] = idata[index]; + } + } } diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index 719fa115..7e6b717c 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -20,6 +20,14 @@ 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 +39,16 @@ 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]; + count++; + } + } + timer().endCpuTimer(); - return -1; + return count; } /** @@ -42,9 +58,49 @@ namespace StreamCompaction { */ int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); + // TODO + + int count = 0; + int* temp = new int[n]; + int* scanned = new int[n]; + + for (int i = 0; i < n; i++) { + if (idata[i] != 0) { + odata[i] = 1; + } + else { + odata[i] = 0; + } + + + } + + + scanned[0] = 0; + for (int i = 1; i < n; i++) { + + scanned[i] = scanned[i - 1] + odata[i - 1]; + } + + + + for (int i = 0; i < n; i++) { + if (odata[i] == 1) { + + temp[scanned[i]] = idata[i]; + count++; + } + } + + + + for (int i = 0; i < count; i++) { + odata[i] = temp[i]; + } + timer().endCpuTimer(); - return -1; + return count; } } } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 2db346ee..608c6dee 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -2,6 +2,7 @@ #include #include "common.h" #include "efficient.h" +#include namespace StreamCompaction { namespace Efficient { @@ -12,13 +13,125 @@ namespace StreamCompaction { return timer; } + + #define blockSize 128 + int* obuffer; + int* ibuffer; + + + __global__ void upSweep(int n, int* idata, int layer) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + + int skip = powf(2, layer + 1); // 1 << (layer + 1); + + int i = (index) * skip; + if (i >= n) { + return; + } + + + idata[int(i + skip - 1)] += idata[int(i + (skip >> 1) - 1)]; + + + } + + __global__ void downSweep(int n, int* idata, int layer) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + + + int skip = powf(2, layer + 1); + int i = index * skip; + if (i >= n) { + return; + } + + + + int t = idata[int(i + (skip >> 1) - 1)]; + + + idata[int(i + (skip >> 1) - 1)] = idata[int(i + skip - 1)]; + idata[int(i + skip - 1)] += t; + } + /** * 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 size = 1 << ilog2ceil(n); + + int* padded = new int[size]; + + for (int i = 0; i < size; i++) { + if (i < n) { + padded[i] = idata[i]; + } + else { + padded[i] = 0; + } + + } + + int* obuffer; + int* ibuffer; + cudaMalloc((void**)&obuffer, size * sizeof(int)); + cudaMalloc((void**)&ibuffer, size * sizeof(int)); + + + cudaMemcpy(obuffer, padded, size * sizeof(int), cudaMemcpyHostToDevice); + cudaMemcpy(ibuffer, padded, size * sizeof(int), cudaMemcpyHostToDevice); + cudaDeviceSynchronize(); + + + + + + // up sweep + + int numBlocks; + + + for (int layer = 0; layer <= ilog2ceil(size) - 1; layer++) { + int numThreads = size / int(powf(2, layer + 1)); + numBlocks = (numThreads + blockSize - 1) / blockSize; + upSweep<<<(numBlocks, blockSize)>>>(size, ibuffer, layer); + cudaDeviceSynchronize(); + + + } + + + cudaMemset(ibuffer + size - 1, 0, sizeof(int)); + + + + // down sweep + + + for (int layer = ilog2ceil(size) - 1; layer >= 0; layer--) { + int numThreads = size / int(powf(2, layer + 1)); + numBlocks = (numThreads + blockSize - 1) / blockSize; + downSweep<<<(numBlocks, blockSize)>>>(size, ibuffer, layer); + cudaDeviceSynchronize(); + + + } timer().endGpuTimer(); + + + + + cudaMemcpy(odata, ibuffer, n * sizeof(int), cudaMemcpyDeviceToHost); + + + delete[] padded; + padded = nullptr; + cudaFree(ibuffer); + cudaFree(obuffer); + } /** @@ -31,10 +144,38 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { - timer().startGpuTimer(); + // timer().startGpuTimer(); // TODO - timer().endGpuTimer(); - return -1; + int count = 0; + int* flags = new int[n]; + int* scanned = new int[n]; + int* temp = new int[n]; + for (int i = 0; i < n; i++) { + if (idata[i] != 0) { + flags[i] = 1; + } + else { + flags[i] = 0; + } + } + + scan(n, scanned, flags); + + + for (int i = 0; i < n; i++) { + if (flags[i] == 1) { + temp[scanned[i]] = idata[i]; + count++; + } + + } + for (int i = 0; i < count; i++) { + odata[i] = temp[i]; + } + + + // timer().endGpuTimer(); + return count; } } } diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 43088769..c8113170 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -2,6 +2,8 @@ #include #include "common.h" #include "naive.h" +#include "cmath" +#include namespace StreamCompaction { namespace Naive { @@ -10,8 +12,47 @@ namespace StreamCompaction { { static PerformanceTimer timer; return timer; - } - // TODO: __global__ + } + + #define blockSize 128 + int* obuffer; + int* ibuffer; + + __global__ void shiftRight(int n, int* odata, const int* idata) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + if (index >= n) { + return; + } + if (index == 0) { + odata[0] = 0; + } + else { + odata[index] = idata[index - 1]; + } + } + + __global__ void kernNaiveScan(int n, int* odata, const int* idata, int offset) { + int index = threadIdx.x + (blockIdx.x * blockDim.x); + + if (index >= n) { + return; + } + + + if (index >= offset) { + odata[index] = idata[(index - offset)] + idata[index]; + + } + else { + odata[index] = idata[index]; + } + + + + + + + } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. @@ -19,7 +60,49 @@ namespace StreamCompaction { void scan(int n, int *odata, const int *idata) { timer().startGpuTimer(); // TODO + + cudaMalloc((void**)&obuffer, n * sizeof(int)); + cudaMalloc((void**)&ibuffer, n * sizeof(int)); + cudaMemcpy(obuffer, odata, n * sizeof(int), cudaMemcpyHostToDevice); + cudaMemcpy(ibuffer, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + int numBlocks = (n + blockSize - 1) / blockSize; + dim3 fullBlocksPerGrid(numBlocks); + + + + + int count = 0; + for (int layer = 1; layer <= ilog2ceil(n); layer++) { + + int offset = powf(2, layer - 1); + + kernNaiveScan<<>>(n, obuffer, ibuffer, offset); + + cudaDeviceSynchronize(); + count++; + int* temp = ibuffer; + ibuffer = obuffer; + obuffer = temp; + + + + + } + shiftRight << > > (n, obuffer, ibuffer); + cudaDeviceSynchronize(); + + + cudaMemcpy(odata, obuffer, n * sizeof(int), cudaMemcpyDeviceToHost); + + cudaFree(obuffer); + cudaFree(ibuffer); + + odata[0] = 0; + + timer().endGpuTimer(); + } } } diff --git a/stream_compaction/thrust.cu b/stream_compaction/thrust.cu index 1def45e7..ad462ec5 100644 --- a/stream_compaction/thrust.cu +++ b/stream_compaction/thrust.cu @@ -6,6 +6,7 @@ #include "common.h" #include "thrust.h" + namespace StreamCompaction { namespace Thrust { using StreamCompaction::Common::PerformanceTimer; @@ -18,11 +19,24 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + int* obuffer; + int* ibuffer; + cudaMalloc(&obuffer, n * sizeof(int)); + cudaMalloc(&ibuffer, n * sizeof(int)); + + cudaMemcpy(ibuffer, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + thrust::device_ptr dev_in = thrust::device_pointer_cast(ibuffer); + thrust::device_ptr dev_out = thrust::device_pointer_cast(obuffer); + 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, dev_in + n, dev_out); timer().endGpuTimer(); + + cudaMemcpy(odata, obuffer, n * sizeof(int), cudaMemcpyDeviceToHost); + + cudaFree(obuffer); + cudaFree(ibuffer); } } } From cef4fbdf23dbf9f7c33db969e69ae557308b5c2d Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 15:25:01 -0400 Subject: [PATCH 02/31] Kernal Launch Bug FIX --- stream_compaction/efficient.cu | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 608c6dee..ec6b3a64 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -22,10 +22,10 @@ namespace StreamCompaction { __global__ void upSweep(int n, int* idata, int layer) { int index = threadIdx.x + (blockIdx.x * blockDim.x); - int skip = powf(2, layer + 1); // 1 << (layer + 1); + int skip = 1 << (layer + 1); // powf(2, layer + 1); int i = (index) * skip; - if (i >= n) { + if (i + skip - 1 >= n) { return; } @@ -39,9 +39,9 @@ namespace StreamCompaction { int index = threadIdx.x + (blockIdx.x * blockDim.x); - int skip = powf(2, layer + 1); + int skip = 1 << (layer + 1); // powf(2, layer + 1); int i = index * skip; - if (i >= n) { + if (i + skip - 1 >= n) { return; } @@ -96,8 +96,10 @@ namespace StreamCompaction { for (int layer = 0; layer <= ilog2ceil(size) - 1; layer++) { int numThreads = size / int(powf(2, layer + 1)); + if (numThreads == 0) continue; + numBlocks = (numThreads + blockSize - 1) / blockSize; - upSweep<<<(numBlocks, blockSize)>>>(size, ibuffer, layer); + upSweep<<>>(size, ibuffer, layer); cudaDeviceSynchronize(); @@ -113,8 +115,10 @@ namespace StreamCompaction { for (int layer = ilog2ceil(size) - 1; layer >= 0; layer--) { int numThreads = size / int(powf(2, layer + 1)); + if (numThreads == 0) continue; + numBlocks = (numThreads + blockSize - 1) / blockSize; - downSweep<<<(numBlocks, blockSize)>>>(size, ibuffer, layer); + downSweep<<>>(size, ibuffer, layer); cudaDeviceSynchronize(); From 45bce5cebd50f7b28601af712cd75b746e46257c Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 15:49:35 -0400 Subject: [PATCH 03/31] CUDA memset instead of loop --- src/main.cpp | 2 +- stream_compaction/common.cu | 2 +- stream_compaction/cpu.cu | 7 +++--- stream_compaction/efficient.cu | 44 ++++++++++------------------------ stream_compaction/naive.cu | 5 ++-- 5 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b0b84d8d..a42b9e8f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 9; // feel free to change the size of array +const int SIZE = 1 << 22; // 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/stream_compaction/common.cu b/stream_compaction/common.cu index 01b41c98..f3c40dc0 100644 --- a/stream_compaction/common.cu +++ b/stream_compaction/common.cu @@ -38,7 +38,7 @@ 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 && bools[index]) { odata[indices[index]] = idata[index]; diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index 7e6b717c..fb47d248 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -19,8 +19,7 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO - + odata[0] = 0; for (int i = 1; i < n; i++) { @@ -38,7 +37,7 @@ 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) { @@ -59,7 +58,7 @@ namespace StreamCompaction { int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO + int count = 0; int* temp = new int[n]; diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index ec6b3a64..592692e5 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -35,6 +35,7 @@ namespace StreamCompaction { } + __global__ void downSweep(int n, int* idata, int layer) { int index = threadIdx.x + (blockIdx.x * blockDim.x); @@ -59,45 +60,28 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { timer().startGpuTimer(); - // TODO + int size = 1 << ilog2ceil(n); - int* padded = new int[size]; - - for (int i = 0; i < size; i++) { - if (i < n) { - padded[i] = idata[i]; - } - else { - padded[i] = 0; - } - - } - + //Init Buffers int* obuffer; int* ibuffer; cudaMalloc((void**)&obuffer, size * sizeof(int)); cudaMalloc((void**)&ibuffer, size * sizeof(int)); + cudaMemset(ibuffer, 0, (size) * sizeof(int)); - - cudaMemcpy(obuffer, padded, size * sizeof(int), cudaMemcpyHostToDevice); - cudaMemcpy(ibuffer, padded, size * sizeof(int), cudaMemcpyHostToDevice); - cudaDeviceSynchronize(); - - + cudaMemcpy(obuffer, odata, size * sizeof(int), cudaMemcpyHostToDevice); + cudaMemcpy(ibuffer, idata, size * sizeof(int), cudaMemcpyHostToDevice); - + cudaDeviceSynchronize(); // up sweep - int numBlocks; for (int layer = 0; layer <= ilog2ceil(size) - 1; layer++) { int numThreads = size / int(powf(2, layer + 1)); - if (numThreads == 0) continue; - numBlocks = (numThreads + blockSize - 1) / blockSize; upSweep<<>>(size, ibuffer, layer); cudaDeviceSynchronize(); @@ -105,17 +89,15 @@ namespace StreamCompaction { } - + //Set ibuffer[n - 1] = 0 cudaMemset(ibuffer + size - 1, 0, sizeof(int)); // down sweep - - for (int layer = ilog2ceil(size) - 1; layer >= 0; layer--) { int numThreads = size / int(powf(2, layer + 1)); - if (numThreads == 0) continue; + numBlocks = (numThreads + blockSize - 1) / blockSize; downSweep<<>>(size, ibuffer, layer); @@ -131,8 +113,7 @@ namespace StreamCompaction { cudaMemcpy(odata, ibuffer, n * sizeof(int), cudaMemcpyDeviceToHost); - delete[] padded; - padded = nullptr; + cudaFree(ibuffer); cudaFree(obuffer); @@ -149,7 +130,7 @@ namespace StreamCompaction { */ int compact(int n, int *odata, const int *idata) { // timer().startGpuTimer(); - // TODO + int count = 0; int* flags = new int[n]; int* scanned = new int[n]; @@ -163,9 +144,10 @@ namespace StreamCompaction { } } + //Produce scan scan(n, scanned, flags); - + //Use scan for indices for (int i = 0; i < n; i++) { if (flags[i] == 1) { temp[scanned[i]] = idata[i]; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index c8113170..c51b9f82 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -18,6 +18,8 @@ namespace StreamCompaction { int* obuffer; int* ibuffer; + + //Shift right for exclusive scan __global__ void shiftRight(int n, int* odata, const int* idata) { int index = threadIdx.x + (blockIdx.x * blockDim.x); if (index >= n) { @@ -59,8 +61,7 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { timer().startGpuTimer(); - // TODO - + cudaMalloc((void**)&obuffer, n * sizeof(int)); cudaMalloc((void**)&ibuffer, n * sizeof(int)); cudaMemcpy(obuffer, odata, n * sizeof(int), cudaMemcpyHostToDevice); From 3108892343fee064983962ee55c2f75e2270c11e Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:34:56 -0400 Subject: [PATCH 04/31] Update README.md --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0e38ddb1..444eb65a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,66 @@ -CUDA Stream Compaction -====================== +# University of Pennsylvania, CIS 5650: GPU Programming and Architecture +## Project 2 - Stream Compaction -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** +* Zwe Tun + * LinkedIn: https://www.linkedin.com/in/zwe-tun-6b7191256/ +* Tested on: Intel(R) i7-14700HX, 2100 Mhz, RTX 5060 Laptop -* (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) +## Overview +Stream compaction is a widely used algorithm with applications in areas such as image compression, data filtering, and parallel computing. This project explores four different implementations of stream compaction: -### (TODO: Your README) +- CPU compaction without scan -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +- CPU compaction with scan +- GPU-based compaction + +Each method demonstrates a different approach to optimizing performance and leveraging hardware capabilities. The goal is to analyze the performance trade-offs and efficiency of each implementation. + +## Implementation + +### CPU compaction without scan +This implementation uses an iterative approach that incrementally places non-zero elements from the input array into an output array. An index counter tracks the next available position in the output. + +### CPU compaction with scan +In this approach, an exclusive scan is introduced to calculate the output indices of the non-zero elements. The scan is performed using an iterative loop based on the following formula: + +x[i] = x[i - 1] + input[i - 1] + +Once the scan array is generated, we pass over the input. For each element i, if input[i] is non-zero, we place it into the output array at the position defined by scan[i]. + +### GPU-based compaction +The GPU version builds on the scan-based CPU approach. Its performance depends on two scan implementations: + +Naive Scan: +Uses GPU threads to compute the scan in multiple layers. At each layer, threads read from specific indices and write to new indices, all in place, gradually building up the scan. + +Work-Efficient Scan: +Uses two phases: + +Up-sweep: Performs a parallel reduction to build a balanced binary tree of partial sums. + +Down-sweep: Traverses back down the tree to compute the exclusive scan in-place. At each pass, a node passes its value to its left child, and sets the right child to the sum of the previous left child’s value and its value. + +Once the scan is complete, the result is copied back to the host (CPU), where the compaction is performed + +## Performance Analysis + +## Questions +Roughly optimize the block sizes of each of your implementations for minimal run time on your GPU. + +(You shouldn't compare unoptimized implementations to each other!) +Compare all of these GPU Scan implementations (Naive, Work-Efficient, and Thrust) to the serial CPU version of Scan. Plot a graph of the comparison (with array size on the independent axis). + +We wrapped up both CPU and GPU timing functions as a performance timer class for you to conveniently measure the time cost. +We use std::chrono to provide CPU high-precision timing and CUDA event to measure the CUDA performance. +For CPU, put your CPU code between timer().startCpuTimer() and timer().endCpuTimer(). +For GPU, put your CUDA code between timer().startGpuTimer() and timer().endGpuTimer(). Be sure not to include any initial/final memory operations (cudaMalloc, cudaMemcpy) in your performance measurements, for comparability. +Don't mix up CpuTimer and GpuTimer. +To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. +Write a brief explanation of the phenomena you see here. + +Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? +Paste the output of the test program into a triple-backtick block in your README. + +If you add your own tests (e.g. for radix sort or to test additional corner cases), be sure to mention it explicitly. +These questions should help guide you in performance analysis on future assignments, as well. From 52ed5174ef2fc2ccb0347014d1b213f9f686bb56 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 19:56:02 -0400 Subject: [PATCH 05/31] GPU compact --- src/main.cpp | 2 +- stream_compaction/common.cu | 2 +- stream_compaction/efficient.cu | 78 ++++++++++++++++++++++------------ 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a42b9e8f..3e869846 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 22; // 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]; diff --git a/stream_compaction/common.cu b/stream_compaction/common.cu index f3c40dc0..205faa81 100644 --- a/stream_compaction/common.cu +++ b/stream_compaction/common.cu @@ -24,7 +24,7 @@ namespace StreamCompaction { * which map to 0 will be removed, and elements which map to 1 will be kept. */ __global__ void kernMapToBoolean(int n, int *bools, const int *idata) { - // TODO + int index = threadIdx.x + (blockIdx.x * blockDim.x); if (index < n) { bools[index] = (idata[index] != 0) ? 1 : 0; diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 592692e5..34f8ec12 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -129,39 +129,63 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { + // timer().startGpuTimer(); - - int count = 0; - int* flags = new int[n]; - int* scanned = new int[n]; - int* temp = new int[n]; - for (int i = 0; i < n; i++) { - if (idata[i] != 0) { - flags[i] = 1; - } - else { - flags[i] = 0; - } - } - //Produce scan - scan(n, scanned, flags); + int* boolBuffer; + int* obuffer; + int* ibuffer; + int* scanBuffer = new int[n]; + int* scanBufferDevice; + cudaMalloc((void**)&scanBufferDevice, n * sizeof(int)); + cudaMalloc((void**)&boolBuffer, n * sizeof(int)); + cudaMalloc((void**)&ibuffer, n * sizeof(int)); + cudaMalloc((void**)&obuffer, n * sizeof(int)); + + cudaMemcpy(ibuffer, idata, n * sizeof(int), cudaMemcpyHostToDevice); + + + int numBlocks = (n + blockSize - 1) / blockSize; + dim3 fullBlocksPerGrid(numBlocks); + //Map to boolean + Common::kernMapToBoolean<<>>(n, boolBuffer, ibuffer); + + int* boolHost = new int[n]; + cudaMemcpy(boolHost, boolBuffer, n * sizeof(int), cudaMemcpyDeviceToHost); - //Use scan for indices - for (int i = 0; i < n; i++) { - if (flags[i] == 1) { - temp[scanned[i]] = idata[i]; - count++; - } + + //Scan boolean array + scan(n, scanBuffer, boolHost); + - } - for (int i = 0; i < count; i++) { - odata[i] = temp[i]; - } + //cudaDeviceSynchronize(); + cudaMemcpy(scanBufferDevice, scanBuffer, n * sizeof(int), cudaMemcpyHostToDevice); + + Common::kernScatter <<> > (n, obuffer, ibuffer, boolBuffer, scanBufferDevice); + ////Scatter results + + + // timer().endGpuTimer(); - // timer().endGpuTimer(); - return count; + //Get count by looking at last element of scan + last element of bool + //Last element of scan holds number of elements up to n-1, so + //add bool[n-1] to get the full count + int count; + cudaMemcpy(&count, scanBufferDevice + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + int lastBool; + cudaMemcpy(&lastBool, boolBuffer + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + count += lastBool; + cudaFree(boolBuffer); + cudaFree(scanBuffer); + cudaMemcpy(odata, obuffer, n * sizeof(int), cudaMemcpyDeviceToHost); + + + + return count; + + + } } } From 9a84b1ecfc3b591982a44f2ae3df138cee5106b1 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:59:24 -0400 Subject: [PATCH 06/31] Update README.md --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 444eb65a..71eed811 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ Down-sweep: Traverses back down the tree to compute the exclusive scan in-place. Once the scan is complete, the result is copied back to the host (CPU), where the compaction is performed ## Performance Analysis +To evaluate and compare each implementation, a benchmark is used based on the Thrust API, specifically leveraging thrust::exclusive_scan. Performance is measured for both CPU and GPU executions by wrapping each in a performance timer class. The CPU execution time is recorded using std::chrono for high-precision timing, while GPU timing is measured using CUDA events. + +Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. + +The benchmarking will also explore performance across various GPU block sizes to identify the optimal configuration. + ## Questions Roughly optimize the block sizes of each of your implementations for minimal run time on your GPU. @@ -51,11 +57,8 @@ Roughly optimize the block sizes of each of your implementations for minimal run (You shouldn't compare unoptimized implementations to each other!) Compare all of these GPU Scan implementations (Naive, Work-Efficient, and Thrust) to the serial CPU version of Scan. Plot a graph of the comparison (with array size on the independent axis). -We wrapped up both CPU and GPU timing functions as a performance timer class for you to conveniently measure the time cost. -We use std::chrono to provide CPU high-precision timing and CUDA event to measure the CUDA performance. -For CPU, put your CPU code between timer().startCpuTimer() and timer().endCpuTimer(). -For GPU, put your CUDA code between timer().startGpuTimer() and timer().endGpuTimer(). Be sure not to include any initial/final memory operations (cudaMalloc, cudaMemcpy) in your performance measurements, for comparability. -Don't mix up CpuTimer and GpuTimer. + + To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. From 0be3a20e7a229041302251cd00ce3427a2ed5b35 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 22:00:04 -0400 Subject: [PATCH 07/31] image --- ... vs Work-Efficient Scan vs Thrust Scan.png | Bin 0 -> 14304 bytes stream_compaction/efficient.cu | 26 ++++++++---------- stream_compaction/naive.cu | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 img/Naive Scan vs Work-Efficient Scan vs Thrust Scan.png diff --git a/img/Naive Scan vs Work-Efficient Scan vs Thrust Scan.png b/img/Naive Scan vs Work-Efficient Scan vs Thrust Scan.png new file mode 100644 index 0000000000000000000000000000000000000000..c317a752bcce8bfa8e82e3ebf2de7d2f7f05e8a9 GIT binary patch literal 14304 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 literal 0 HcmV?d00001 diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 34f8ec12..0a43277e 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { } - #define blockSize 128 + #define blockSize 64 int* obuffer; int* ibuffer; @@ -131,41 +131,39 @@ namespace StreamCompaction { int compact(int n, int *odata, const int *idata) { // timer().startGpuTimer(); - - int* boolBuffer; + //Init Buffers + int* boolBufferDevice; int* obuffer; int* ibuffer; int* scanBuffer = new int[n]; int* scanBufferDevice; + cudaMalloc((void**)&scanBufferDevice, n * sizeof(int)); - cudaMalloc((void**)&boolBuffer, n * sizeof(int)); + cudaMalloc((void**)&boolBufferDevice, n * sizeof(int)); cudaMalloc((void**)&ibuffer, n * sizeof(int)); cudaMalloc((void**)&obuffer, n * sizeof(int)); cudaMemcpy(ibuffer, idata, n * sizeof(int), cudaMemcpyHostToDevice); - int numBlocks = (n + blockSize - 1) / blockSize; dim3 fullBlocksPerGrid(numBlocks); //Map to boolean - Common::kernMapToBoolean<<>>(n, boolBuffer, ibuffer); + Common::kernMapToBoolean<<>>(n, boolBufferDevice, ibuffer); - int* boolHost = new int[n]; - cudaMemcpy(boolHost, boolBuffer, n * sizeof(int), cudaMemcpyDeviceToHost); + int* boolBuffer = new int[n]; + cudaMemcpy(boolBuffer, boolBufferDevice, n * sizeof(int), cudaMemcpyDeviceToHost); //Scan boolean array - scan(n, scanBuffer, boolHost); + scan(n, scanBuffer, boolBuffer); - //cudaDeviceSynchronize(); cudaMemcpy(scanBufferDevice, scanBuffer, n * sizeof(int), cudaMemcpyHostToDevice); - Common::kernScatter <<> > (n, obuffer, ibuffer, boolBuffer, scanBufferDevice); + Common::kernScatter <<> > (n, obuffer, ibuffer, boolBufferDevice, scanBufferDevice); ////Scatter results - - + // timer().endGpuTimer(); //Get count by looking at last element of scan + last element of bool @@ -174,7 +172,7 @@ namespace StreamCompaction { int count; cudaMemcpy(&count, scanBufferDevice + n - 1, sizeof(int), cudaMemcpyDeviceToHost); int lastBool; - cudaMemcpy(&lastBool, boolBuffer + n - 1, sizeof(int), cudaMemcpyDeviceToHost); + cudaMemcpy(&lastBool, boolBufferDevice + n - 1, sizeof(int), cudaMemcpyDeviceToHost); count += lastBool; cudaFree(boolBuffer); cudaFree(scanBuffer); diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index c51b9f82..fad4d375 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { return timer; } - #define blockSize 128 + #define blockSize 64 int* obuffer; int* ibuffer; From 445b9bf9bc88acbc8ab783318f4af3ebc93288cd Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:01:54 -0400 Subject: [PATCH 08/31] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71eed811..1cfead55 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. -The benchmarking will also explore performance across various GPU block sizes to identify the optimal configuration. +Performance across various GPU block sizes + ## Questions From edd6b0cd7469cddd2eed225f82b49eddc66bc037 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 22:02:47 -0400 Subject: [PATCH 09/31] Rename --- ...-Efficient Scan vs Thrust Scan.png => block.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename img/{Naive Scan vs Work-Efficient Scan vs Thrust Scan.png => block.png} (100%) diff --git a/img/Naive Scan vs Work-Efficient Scan vs Thrust Scan.png b/img/block.png similarity index 100% rename from img/Naive Scan vs Work-Efficient Scan vs Thrust Scan.png rename to img/block.png From bf6345a54f836773ee11f3ff369f303fc13008e9 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:03:30 -0400 Subject: [PATCH 10/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cfead55..5a6bafa8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. Performance across various GPU block sizes - +![Stream Compaction](imag/block.png) ## Questions From 91ca0f88c1c046627da7067e815376b7c6939599 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:03:41 -0400 Subject: [PATCH 11/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a6bafa8..e08c0322 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. Performance across various GPU block sizes -![Stream Compaction](imag/block.png) +![Stream Compaction](img/block.png) ## Questions From c0a33d8b060d2662e64ba9080b8f8b9e610853cc Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:17:52 -0400 Subject: [PATCH 12/31] Update README.md --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e08c0322..539fbc80 100644 --- a/README.md +++ b/README.md @@ -48,23 +48,18 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. -Performance across various GPU block sizes ![Stream Compaction](img/block.png) - -## Questions -Roughly optimize the block sizes of each of your implementations for minimal run time on your GPU. - -(You shouldn't compare unoptimized implementations to each other!) -Compare all of these GPU Scan implementations (Naive, Work-Efficient, and Thrust) to the serial CPU version of Scan. Plot a graph of the comparison (with array size on the independent axis). - +Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests. +## Questions To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. +Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? Paste the output of the test program into a triple-backtick block in your README. -If you add your own tests (e.g. for radix sort or to test additional corner cases), be sure to mention it explicitly. -These questions should help guide you in performance analysis on future assignments, as well. + + From 1a4b6b1c3a945f71c4a9a9da840b64a596e45e2b Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 22:35:53 -0400 Subject: [PATCH 13/31] Images add --- img/Scan.png | Bin 0 -> 78337 bytes img/compact.png | Bin 0 -> 47937 bytes src/main.cpp | 16 ++++++++-------- stream_compaction/efficient.cu | 2 +- stream_compaction/naive.cu | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 img/Scan.png create mode 100644 img/compact.png diff --git a/img/Scan.png b/img/Scan.png new file mode 100644 index 0000000000000000000000000000000000000000..5918c31f1bad0b07afc49e75adde70bfe40288b6 GIT binary patch literal 78337 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 i2@e}a4kcyn=;(L8+(j*_ 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 literal 0 HcmV?d00001 diff --git a/img/compact.png b/img/compact.png new file mode 100644 index 0000000000000000000000000000000000000000..73a3418223f72f611d1e9f494b89f2fd1b3656f5 GIT binary patch literal 47937 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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 << 23; // 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]; @@ -38,20 +38,20 @@ int main(int argc, char* argv[]) { printDesc("cpu scan, power-of-two"); StreamCompaction::CPU::scan(SIZE, b, a); printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(SIZE, b, true); + 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, c, true); + //printArray(NPOT, c, 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); + //printArray(SIZE, c, true); printCmpResult(SIZE, b, c); /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan @@ -64,21 +64,21 @@ int main(int argc, char* argv[]) { printDesc("naive scan, non-power-of-two"); StreamCompaction::Naive::scan(NPOT, c, a); printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - printArray(SIZE, c, true); + //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); + //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); @@ -153,7 +153,7 @@ int main(int argc, char* argv[]) { 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/efficient.cu b/stream_compaction/efficient.cu index 0a43277e..dff4b152 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { } - #define blockSize 64 + #define blockSize 128 int* obuffer; int* ibuffer; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index fad4d375..c51b9f82 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { return timer; } - #define blockSize 64 + #define blockSize 128 int* obuffer; int* ibuffer; From 214cda3e85a3130d06e8f537b1f331c0f2103ca2 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:45:50 -0400 Subject: [PATCH 14/31] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 539fbc80..c30dee27 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests. +![Stream Compaction](img/scan.png) +![Stream Compaction](img/compact.png) + +Performance results for 8,388,608 size array, 128 block size. ## Questions To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. @@ -59,7 +63,4 @@ Write a brief explanation of the phenomena you see here. Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? -Paste the output of the test program into a triple-backtick block in your README. - - - +For the CPU implementation, the bottleneck is predominantly computation based on the size of the array. Since the CPU executes sequentially main cost is due to math operations. For the GPU, memory I/O will make up the majority of the cost. Computation is parallelized through threads so global memory accesses and copies will be limiting. From 0eb2ee01bdeaa160dfdabe767035da552e0f908e Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:46:14 -0400 Subject: [PATCH 15/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c30dee27..28a38ca0 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests. -![Stream Compaction](img/scan.png) +![Stream Compaction](img/Scan.png) ![Stream Compaction](img/compact.png) Performance results for 8,388,608 size array, 128 block size. From 716512d2056615f355e7e6bb24af0f940bd7e863 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:46:51 -0400 Subject: [PATCH 16/31] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28a38ca0..e5c21426 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,10 @@ Performance across various GPU block sizes for 256 array size, 128 block size us Performance results for 8,388,608 size array, 128 block size. ## Questions -To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. +# To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. + Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. -Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? +# Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? For the CPU implementation, the bottleneck is predominantly computation based on the size of the array. Since the CPU executes sequentially main cost is due to math operations. For the GPU, memory I/O will make up the majority of the cost. Computation is parallelized through threads so global memory accesses and copies will be limiting. From 70ac31c337d64892795f1ef00d28b84618bc5886 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:47:07 -0400 Subject: [PATCH 17/31] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5c21426..7a242910 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,10 @@ Performance across various GPU block sizes for 256 array size, 128 block size us Performance results for 8,388,608 size array, 128 block size. ## Questions -# To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. +### To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. -# Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? +### Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? For the CPU implementation, the bottleneck is predominantly computation based on the size of the array. Since the CPU executes sequentially main cost is due to math operations. For the GPU, memory I/O will make up the majority of the cost. Computation is parallelized through threads so global memory accesses and copies will be limiting. From 1c752e7def7e793d57bf78503aa33d82ddaa0828 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:47:48 -0400 Subject: [PATCH 18/31] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a242910..3f937027 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@ Each method demonstrates a different approach to optimizing performance and leve ## Implementation -### CPU compaction without scan +### CPU Compaction Without Scan This implementation uses an iterative approach that incrementally places non-zero elements from the input array into an output array. An index counter tracks the next available position in the output. -### CPU compaction with scan +### CPU Compaction With Scan In this approach, an exclusive scan is introduced to calculate the output indices of the non-zero elements. The scan is performed using an iterative loop based on the following formula: x[i] = x[i - 1] + input[i - 1] Once the scan array is generated, we pass over the input. For each element i, if input[i] is non-zero, we place it into the output array at the position defined by scan[i]. -### GPU-based compaction +### GPU-based Compaction The GPU version builds on the scan-based CPU approach. Its performance depends on two scan implementations: Naive Scan: From 56a74af7d109f1289409be8e2cf8a6fb37c0653a Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:48:03 -0400 Subject: [PATCH 19/31] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f937027..4eb9902f 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,7 @@ Performance across various GPU block sizes for 256 array size, 128 block size us Performance results for 8,388,608 size array, 128 block size. ## Questions -### To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. -Write a brief explanation of the phenomena you see here. +### To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. From f11713039f3f29b7b63cb5350537878b1334b948 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:49:44 -0400 Subject: [PATCH 20/31] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4eb9902f..f7f98738 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,11 @@ Uses GPU threads to compute the scan in multiple layers. At each layer, threads Work-Efficient Scan: Uses two phases: -Up-sweep: Performs a parallel reduction to build a balanced binary tree of partial sums. +Up-sweep: +Performs a parallel reduction to build a balanced binary tree of partial sums. -Down-sweep: Traverses back down the tree to compute the exclusive scan in-place. At each pass, a node passes its value to its left child, and sets the right child to the sum of the previous left child’s value and its value. +Down-sweep: +Traverses back down the tree to compute the exclusive scan in-place. At each pass, a node passes its value to its left child, and sets the right child to the sum of the previous left child’s value and its value. Once the scan is complete, the result is copied back to the host (CPU), where the compaction is performed From 35a544c2374ee02afb3d90214dbdd7a34d1598b8 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 23:06:10 -0400 Subject: [PATCH 21/31] img size --- img/size.png | Bin 0 -> 21919 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/size.png diff --git a/img/size.png b/img/size.png new file mode 100644 index 0000000000000000000000000000000000000000..d61d8c174fb637f2dc828a7c1c3d7a9c42b408b4 GIT binary patch literal 21919 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

5p<*!~9M%GAY^j4gcB!Tlh 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 Date: Wed, 17 Sep 2025 23:07:34 -0400 Subject: [PATCH 22/31] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7f98738..d58d9dd7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,12 @@ Performance across various GPU block sizes for 256 array size, 128 block size us ![Stream Compaction](img/Scan.png) ![Stream Compaction](img/compact.png) -Performance results for 8,388,608 size array, 128 block size. +Performance results for 8,388,608 (2^23) sized array, 128 block size. + +![Stream Compaction](img/size.png) + +Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size. + ## Questions ### To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. From 1c526e5fd57cd6505a11b1fa39c50403b14b2c2f Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:10:16 -0400 Subject: [PATCH 23/31] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d58d9dd7..cd44094d 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ Once the scan array is generated, we pass over the input. For each element i, if ### GPU-based Compaction The GPU version builds on the scan-based CPU approach. Its performance depends on two scan implementations: -Naive Scan: +** Naive Scan: ** Uses GPU threads to compute the scan in multiple layers. At each layer, threads read from specific indices and write to new indices, all in place, gradually building up the scan. -Work-Efficient Scan: +** Work-Efficient Scan: ** Uses two phases: Up-sweep: From 57518e26664fa8d0c64a13f024ce3a95fff7633b Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:11:32 -0400 Subject: [PATCH 24/31] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cd44094d..4db5d9d7 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ Once the scan array is generated, we pass over the input. For each element i, if ### GPU-based Compaction The GPU version builds on the scan-based CPU approach. Its performance depends on two scan implementations: -** Naive Scan: ** +**Naive Scan:** Uses GPU threads to compute the scan in multiple layers. At each layer, threads read from specific indices and write to new indices, all in place, gradually building up the scan. -** Work-Efficient Scan: ** +**Work-Efficient Scan:** Uses two phases: Up-sweep: @@ -52,16 +52,16 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a ![Stream Compaction](img/block.png) -Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests. +*Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests.* ![Stream Compaction](img/Scan.png) ![Stream Compaction](img/compact.png) -Performance results for 8,388,608 (2^23) sized array, 128 block size. +*Performance results for 8,388,608 (2^23) sized array, 128 block size.* ![Stream Compaction](img/size.png) -Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size. +*Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size.* ## Questions From 3271c473447f5c92bfb42a65cb60bf18baf9a09f Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:12:00 -0400 Subject: [PATCH 25/31] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4db5d9d7..858e7742 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,13 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a *Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests.* + ![Stream Compaction](img/Scan.png) ![Stream Compaction](img/compact.png) *Performance results for 8,388,608 (2^23) sized array, 128 block size.* + ![Stream Compaction](img/size.png) *Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size.* From c162218736d263eb6c10879005cc4d650a064301 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:12:22 -0400 Subject: [PATCH 26/31] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 858e7742..181fe7a5 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,24 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. + + + ![Stream Compaction](img/block.png) *Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests.* + + ![Stream Compaction](img/Scan.png) ![Stream Compaction](img/compact.png) *Performance results for 8,388,608 (2^23) sized array, 128 block size.* + + ![Stream Compaction](img/size.png) *Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size.* From 3ac2bcfff8a859c8014f2fb3d82af92742a0e409 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:14:16 -0400 Subject: [PATCH 27/31] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 181fe7a5..fc7cd56b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ To evaluate and compare each implementation, a benchmark is used based on the Th Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) are excluded from the timing results. +  ![Stream Compaction](img/block.png) @@ -58,6 +59,7 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a *Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests.* +  ![Stream Compaction](img/Scan.png) @@ -65,13 +67,14 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a *Performance results for 8,388,608 (2^23) sized array, 128 block size.* - +  ![Stream Compaction](img/size.png) *Performance results for 8,388,608 (2^23) non-power-two vs power-of-two sized array, 128 block size.* +  ## Questions ### To guess at what might be happening inside the Thrust implementation (e.g. allocation, memory copy), take a look at the Nsight timeline for its execution. Your analysis here doesn't have to be detailed, since you aren't even looking at the code for the implementation. Write a brief explanation of the phenomena you see here. From 77f7ee9d1901fe02febd24c5b401f25ef51e6202 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:17:09 -0400 Subject: [PATCH 28/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc7cd56b..3b1dd075 100644 --- a/README.md +++ b/README.md @@ -82,4 +82,4 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a Looking at the timeline, we can observe numerous memory operations and multiple kernel launches. These kernel launches appear to be synchronized and follow a pattern similar to the scan approaches used in this project's algorithm. ### Can you find the performance bottlenecks? Is it memory I/O? Computation? Is it different for each implementation? -For the CPU implementation, the bottleneck is predominantly computation based on the size of the array. Since the CPU executes sequentially main cost is due to math operations. For the GPU, memory I/O will make up the majority of the cost. Computation is parallelized through threads so global memory accesses and copies will be limiting. +For the CPU implementation, the bottleneck is predominantly computation based on the size of the array. Since the CPU executes sequentially, the main cost is due to math operations at each interation. For the GPU, memory I/O will make up the majority of the cost. Computation is parallelized through threads so global memory accesses and copies will be limiting. From 9beacee1442b5735bc4491ca51852533a386ea3a Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 23:40:38 -0400 Subject: [PATCH 29/31] Updated Graph --- img/block1.png | Bin 0 -> 15923 bytes src/main.cpp | 2 +- stream_compaction/efficient.cu | 2 +- stream_compaction/naive.cu | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 img/block1.png diff --git a/img/block1.png b/img/block1.png new file mode 100644 index 0000000000000000000000000000000000000000..b284724b8ec0daa5e2b297af4654b37cd3f9ed06 GIT binary patch literal 15923 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 literal 0 HcmV?d00001 diff --git a/src/main.cpp b/src/main.cpp index 1b7658ab..74a574bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 23; // 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]; diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index dff4b152..17cf7ace 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { } - #define blockSize 128 + #define blockSize 256 int* obuffer; int* ibuffer; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index c51b9f82..6e532d79 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { return timer; } - #define blockSize 128 + #define blockSize 256 int* obuffer; int* ibuffer; From 17a599593cc045fda65ca78f33f1273308e6d051 Mon Sep 17 00:00:00 2001 From: ZweTun <144613156+ZweTun@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:41:07 -0400 Subject: [PATCH 30/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b1dd075..13450701 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Note, memory allocation and transfer operations (cudaMalloc, cudaMemcpy, etc.) a   -![Stream Compaction](img/block.png) +![Stream Compaction](img/block1.png) *Performance across various GPU block sizes for 256 array size, 128 block size used for futher tests.* From 2fe21ba849fd313b05cbe1e31b0f5ab2553ac5a3 Mon Sep 17 00:00:00 2001 From: Zwe Date: Wed, 17 Sep 2025 23:43:18 -0400 Subject: [PATCH 31/31] Optimal block size --- stream_compaction/efficient.cu | 2 +- stream_compaction/naive.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 17cf7ace..dff4b152 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { } - #define blockSize 256 + #define blockSize 128 int* obuffer; int* ibuffer; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 6e532d79..c51b9f82 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -14,7 +14,7 @@ namespace StreamCompaction { return timer; } - #define blockSize 256 + #define blockSize 128 int* obuffer; int* ibuffer;