Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project 2: Helena Zhang #24

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 88 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,94 @@ 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)
* Helena Zhang
* Tested on: Windows 11, i7-10750 @ 2.6GHz 16GB, Geforce RTX 2060 6GB

### (TODO: Your README)
### Performance analysis

Include analysis, etc. (Remember, this is public, so don't put
anything here that you don't want to share with the world.)
First up, I compared the runtimes of different GPU implementations (naive scan, work efficient scan, and stream compaction) on an array size of 2^20 and took the average of three runs each to find the optimal block size on my GPU:

![](img/blocksize.jpg)

Although the runtimes past 64 threads per block were similar, 64 threads per block narrowly beat out the larger blocksizes.

Next, using the optimal blocksize of 64, I compared the runtimes of the base CPU exclusive scan, and the three GPU scan implementations: naive, work efficient, and thrust. I ran each implementation on each array size 5 times and took the average to account for any potential outliers.

![](img/runtime.jpg)

The CPU runtime scaled linearly with the increase in array size, so the trendline appeared to be linear on a chart with log-scaled runtime on log-scaled array size. The GPU implementation all ran slower than the CPU implementation on small array sizes, and only the naive implementation showed a large spike in runtime at the end. The work efficient implementation runtime increased slowly towards the end, and the thrust runtime remained the fastest at the end.

In regards to the optimized work efficient GPU implementation, I managed to minimize the number of blocks needed at small numbers of threads. During the upstream / downstream summation, there were only a few additions done at the top levels of the tree. Instead of taking the 2^d th thread to run in the kernel, I scaled each thread with a factor of 2^(d-1), so that only k threads are needed. In addition, instead of launching approximately N / blocksize blocks at each level of summation, I launched only **1 << (ilogceil(N) - d - log(blocksize) - 1)** blocks, which is significantly fewer blocks. For any d, only every other 2^d numbers will get incremented, meaning there should be at most **1 << (ilogceil(N) - d - 1)** threads. Since the max number of threads in each block is **blocksize**, we will scale down the number of threads by a factor of that, leaving each level only launching **1 << (ilogceil(N) - d - log(blocksize) - 1)** blocks, and every thread in those blocks are used.

For the fastest implementation, **thrust::exclusive_scan**, I've observed its execution in Nsight:

![](img/nsight.jpg)

Assuming **cudeEventRecord** are the GPU timer operations, there are 5 operations within exclusive scan: **cudaMalloc**, **DeviceScanInitKernel**, **DeviceScanKernel**, **cudaStreamSynchronize**, **cudaFree**. Since **idata** was copied into device memory prior to starting the GPU timer, **cudaMalloc** was likely an operation to store temporary data within the operation since that memory was freed at the end of this function. The two scan operations were likely performing the addition, and they took the least time, meaning the **cudaMalloc** most likely allocated some shared memory for these operations to execute quickly.

Based on the increased runtimes of all the implementations, a large bottleneck seems to be caused by excessive yet unproductive blocks, hence reducing and packing blocks enhanced the runtime.

Finally, an overview of all the runtimes:
```

****************
** SCAN TESTS **
****************
[ 1 6 1 30 17 6 13 30 37 3 40 40 21 ... 29 12 ]
==== cpu scan, power-of-two ====
elapsed time: 14.2981ms (std::chrono Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838503 410838532 ]
==== cpu scan, non-power-of-two ====
elapsed time: 13.4163ms (std::chrono Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838422 410838469 ]
passed
==== naive scan, power-of-two ====
elapsed time: 16.1973ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838503 410838532 ]
passed
==== naive scan, non-power-of-two ====
elapsed time: 16.1932ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 0 0 ]
passed
==== work-efficient scan, power-of-two ====
elapsed time: 7.42416ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838503 410838532 ]
passed
==== work-efficient scan, non-power-of-two ====
elapsed time: 7.27318ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838422 410838469 ]
passed
==== thrust scan, power-of-two ====
elapsed time: 1.18771ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838503 410838532 ]
passed
==== thrust scan, non-power-of-two ====
elapsed time: 1.35674ms (CUDA Measured)
[ 0 1 7 8 38 55 61 74 104 141 144 184 224 ... 410838422 410838469 ]
passed

*****************************
** STREAM COMPACTION TESTS **
*****************************
[ 0 0 3 1 2 3 0 1 2 1 0 0 0 ... 2 2 ]
==== cpu compact without scan, power-of-two ====
elapsed time: 36.814ms (std::chrono Measured)
[ 3 1 2 3 1 2 1 2 2 3 2 2 3 ... 2 2 ]
passed
==== cpu compact without scan, non-power-of-two ====
elapsed time: 37.0357ms (std::chrono Measured)
[ 3 1 2 3 1 2 1 2 2 3 2 2 3 ... 1 2 ]
passed
==== cpu compact with scan ====
elapsed time: 77.7415ms (std::chrono Measured)
[ 3 1 2 3 1 2 1 2 2 3 2 2 3 ... 2 2 ]
passed
==== work-efficient compact, power-of-two ====
elapsed time: 14.5762ms (CUDA Measured)
[ 3 1 2 3 1 2 1 2 2 3 2 2 3 ... 2 2 ]
passed
==== work-efficient compact, non-power-of-two ====
elapsed time: 14.472ms (CUDA Measured)
[ 3 1 2 3 1 2 1 2 2 3 2 2 3 ... 1 2 ]
passed
```
Binary file added img/blocksize.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/nsight.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/runtime.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 13 additions & 13 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#include <stream_compaction/thrust.h>
#include "testing_helpers.hpp"

const int SIZE = 1 << 8; // feel free to change the size of array
const int SIZE = 1 << 24; // 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];
Expand All @@ -27,8 +27,8 @@ int main(int argc, char* argv[]) {
printf("** SCAN TESTS **\n");
printf("****************\n");

genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case
a[SIZE - 1] = 0;
genArray(SIZE, a, 50); // Leave a 0 at the end to test that edge case
// a[SIZE - 1] = 0;
printArray(SIZE, a, true);

// initialize b using StreamCompaction::CPU::scan you implement
Expand All @@ -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
Expand All @@ -64,35 +64,35 @@ 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);
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");
Expand All @@ -102,8 +102,8 @@ int main(int argc, char* argv[]) {

// Compaction tests

genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case
a[SIZE - 1] = 0;
genArray(SIZE, a, 4); // Leave a 0 at the end to test that edge case
// a[SIZE - 1] = 0;
printArray(SIZE, a, true);

int count, expectedCount, expectedNPOT;
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions stream_compaction/common.cu
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ namespace StreamCompaction {
* which map to 0 will be removed, and elements which map to 1 will be kept.
*/
__global__ void kernMapToBoolean(int n, int *bools, const int *idata) {
// TODO
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
idata[i] > 0 ? bools[i] = 1 : bools[i] = 0;
}
}

/**
Expand All @@ -32,7 +35,12 @@ namespace StreamCompaction {
*/
__global__ void kernScatter(int n, int *odata,
const int *idata, const int *bools, const int *indices) {
// TODO
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
if (bools[i]) {
odata[indices[i]] = idata[i];
}
}
}

}
Expand Down
39 changes: 34 additions & 5 deletions stream_compaction/cpu.cu
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ namespace StreamCompaction {
*/
void scan(int n, int *odata, const int *idata) {
timer().startCpuTimer();
// TODO
int sum = 0;
for (int i = 0; i < n; i++) {
odata[i] = sum;
sum += idata[i];

}
timer().endCpuTimer();
}

Expand All @@ -30,9 +35,15 @@ namespace StreamCompaction {
*/
int compactWithoutScan(int n, int *odata, const int *idata) {
timer().startCpuTimer();
// TODO
int length = 0;
for (int i = 0; i < n; i++) {
if (idata[i] != 0) {
odata[length] = idata[i];
length++;
}
}
timer().endCpuTimer();
return -1;
return length;
}

/**
Expand All @@ -42,9 +53,27 @@ namespace StreamCompaction {
*/
int compactWithScan(int n, int *odata, const int *idata) {
timer().startCpuTimer();
// TODO
int* temp = new int[n];
int* tempScanned = new int[n];
for (int i = 0; i < n; i++) {
idata[i] > 0 ? temp[i] = 1 : temp[i] = 0;
}

int sum = 0;
for (int i = 0; i < n; i++) {
tempScanned[i] = sum;
sum += temp[i];
}

int count = 0;
for (int i = 0; i < n; i++) {
if (temp[i] == 1) {
odata[tempScanned[i]] = idata[i];
count++;
}
}
timer().endCpuTimer();
return -1;
return count;
}
}
}
Loading