From 130dd4203805f6c94f75ec7ba7a736477b9dece1 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 12:54:25 -0400 Subject: [PATCH 01/22] Finished part 1, CPU scan and stream --- src/main.cpp | 1 + stream_compaction/cpu.cu | 45 ++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 675da35..465a0f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,4 +120,5 @@ int main(int argc, char* argv[]) { count = StreamCompaction::Efficient::compact(NPOT, c, a); //printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); + printf("done\n"); } diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index e600c29..5133cd6 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -8,8 +8,11 @@ namespace CPU { * CPU scan (prefix sum). */ void scan(int n, int *odata, const int *idata) { - // TODO - printf("TODO\n"); + // Implement exclusive serial scan on CPU + odata[0] = 0; + for (int i = 1; i < n; i++) { + odata[i] = odata[i - 1] + idata[i - 1]; + } } /** @@ -18,8 +21,16 @@ void scan(int n, int *odata, const int *idata) { * @returns the number of elements remaining after compaction. */ int compactWithoutScan(int n, int *odata, const int *idata) { - // TODO - return -1; + // remove all 0s from the array of ints + int odataIndex = 0; + for (int i = 0; i < n; i++) { + if (idata[i] == 0) { + continue; + } + odata[odataIndex] = idata[i]; + odataIndex++; + } + return odataIndex; } /** @@ -28,8 +39,30 @@ int compactWithoutScan(int n, int *odata, const int *idata) { * @returns the number of elements remaining after compaction. */ int compactWithScan(int n, int *odata, const int *idata) { - // TODO - return -1; + // Step 1: Compute temporary values in odata + int *trueArray = new int[n]; + for (int i = 0; i < n; i++) { + if (idata[i] == 0) { + trueArray[i] = 0; + } + else { + trueArray[i] = 1; + } + } + // Step 2: Run exclusive scan on temporary array + int *trueScan = new int[n]; + scan(n, trueScan, trueArray); + + // Step 3: Scatter + for (int i = 0; i < n; i++) { + if (trueArray[i]) { + odata[trueScan[i]] = idata[i]; + } + } + delete trueArray; + int numRemaining = trueScan[n - 1]; + delete trueScan; + return numRemaining; } } From 0fa4b2a58eca3e395dc9481c6ab799491dc94143 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 15:14:48 -0400 Subject: [PATCH 02/22] finished part 2, naive gpu scan. hopefully. --- stream_compaction/naive.cu | 52 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 3d86b60..0441673 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -8,12 +8,60 @@ namespace Naive { // TODO: __global__ +__global__ void naive_scan_step(int d, int *x, int *x_next) { + int i = threadIdx.x + (blockIdx.x * blockDim.x); + int offset = powf(2, d - 1); + if (i >= offset) { + x_next[i] = x[i - offset] + x[i]; + } + else { + x_next[i] = x[i]; + } +} + +__global__ void parallel_copy(int *data, int *copy) { + int i = threadIdx.x + (blockIdx.x * blockDim.x); + copy[i] = data[i]; +} + +__global__ void parallel_shift(int *inclusive, int *exclusive) { + int i = threadIdx.x + (blockIdx.x * blockDim.x); + if (i == 0) { + exclusive[i] = 0; + return; + } + exclusive[i] = inclusive[i - 1]; +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - // TODO - printf("TODO\n"); + // copy everything in idata over to the GPU + dim3 dimBlock(n); + dim3 dimGrid(1); + int *dev_x; + int *dev_x_next; + int *dev_exclusive; + cudaMalloc((void**)&dev_x, sizeof(int) * n); + cudaMalloc((void**)&dev_x_next, sizeof(int) * n); + cudaMalloc((void**)&dev_exclusive, sizeof(int) * n); + + cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + // run steps. + int logn = ilog2ceil(n); + for (int d = 1; d <= logn; d++) { + naive_scan_step <<>>(d, dev_x, dev_x_next); + parallel_copy <<>>(dev_x_next, dev_x); + } + + parallel_shift << > >(dev_x, dev_exclusive); + cudaFree(dev_x); + cudaFree(dev_x_next); + + cudaMemcpy(odata, dev_exclusive, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaFree(dev_exclusive); } } From d26f32edeb6bf9a1e51c8e2d6c0570a91d353640 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 16:29:47 -0400 Subject: [PATCH 03/22] added more tests, efficient stream compaction largely done. TODO: address power of 2 problems --- src/main.cpp | 23 ++++++++++++++++ stream_compaction/efficient.cu | 48 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 465a0f6..276b880 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,14 @@ int main(int argc, char* argv[]) { const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; + // the case in the slides, as a smaller test. + int small[8]; + for (int i = 0; i < 8; i++) { + small[i] = i; + } + int smallScan[8] = { 0, 0, 1, 3, 6, 10, 15, 21 }; + + // Scan tests printf("\n"); @@ -40,6 +48,11 @@ int main(int argc, char* argv[]) { printArray(NPOT, b, true); printCmpResult(NPOT, b, c); + zeroArray(SIZE, c); + printDesc("small cpu scan test."); + StreamCompaction::CPU::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("naive scan, power-of-two"); StreamCompaction::Naive::scan(SIZE, c, a); @@ -52,12 +65,22 @@ int main(int argc, char* argv[]) { //printArray(SIZE, c, true); printCmpResult(NPOT, b, c); + zeroArray(SIZE, c); + printDesc("small naive scan test."); + StreamCompaction::Naive::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, power-of-two"); StreamCompaction::Efficient::scan(SIZE, c, a); //printArray(SIZE, c, true); printCmpResult(SIZE, b, c); + zeroArray(SIZE, c); + printDesc("small work efficient scan test."); + StreamCompaction::Efficient::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, non-power-of-two"); StreamCompaction::Efficient::scan(NPOT, c, a); diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index b2f739b..615515d 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -8,12 +8,56 @@ namespace Efficient { // TODO: __global__ +__global__ void upsweep_step(int d, int *x) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k % (int) powf(2, d + 1)) { + return; + } + x[k + (int) powf(2, d + 1) - 1] += x[k + (int) powf(2, d) - 1]; +} + +__global__ void downsweep_step(int d, int *x) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k % (int)powf(2, d + 1)) { + return; + } + int t = x[k + (int) powf(2, d) - 1]; + x[k + (int) powf(2, d) - 1] = x[k + (int) powf(2, d + 1) - 1]; + x[k + (int) powf(2, d + 1) - 1] += t; +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - // TODO - printf("TODO\n"); + // copy everything in idata over to the GPU + dim3 dimBlock(n); + dim3 dimGrid(1); + int *dev_x; + cudaMalloc((void**)&dev_x, sizeof(int) * n); + + cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + // Up Sweep + int logn = ilog2ceil(n); + for (int d = 0; d < logn; d++) { + upsweep_step <<>>(d, dev_x); + } + + //debug: peek at the array after upsweep + //int peek[8]; + //cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); + + // Down-Sweep + int zero[1]; + zero[0] = 0; + cudaMemcpy(&dev_x[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + for (int d = logn - 1; d >= 0; d--) { + downsweep_step <<>>(d, dev_x); + } + + cudaMemcpy(odata, dev_x, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaFree(dev_x); } /** From 6671b7a716fde2e1d10b1e743ba33c8a294a870b Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 16:29:47 -0400 Subject: [PATCH 04/22] added more tests, efficient scan largely done. TODO: address power of 2 problems --- src/main.cpp | 23 ++++++++++++++++ stream_compaction/efficient.cu | 48 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 465a0f6..276b880 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,14 @@ int main(int argc, char* argv[]) { const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; + // the case in the slides, as a smaller test. + int small[8]; + for (int i = 0; i < 8; i++) { + small[i] = i; + } + int smallScan[8] = { 0, 0, 1, 3, 6, 10, 15, 21 }; + + // Scan tests printf("\n"); @@ -40,6 +48,11 @@ int main(int argc, char* argv[]) { printArray(NPOT, b, true); printCmpResult(NPOT, b, c); + zeroArray(SIZE, c); + printDesc("small cpu scan test."); + StreamCompaction::CPU::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("naive scan, power-of-two"); StreamCompaction::Naive::scan(SIZE, c, a); @@ -52,12 +65,22 @@ int main(int argc, char* argv[]) { //printArray(SIZE, c, true); printCmpResult(NPOT, b, c); + zeroArray(SIZE, c); + printDesc("small naive scan test."); + StreamCompaction::Naive::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, power-of-two"); StreamCompaction::Efficient::scan(SIZE, c, a); //printArray(SIZE, c, true); printCmpResult(SIZE, b, c); + zeroArray(SIZE, c); + printDesc("small work efficient scan test."); + StreamCompaction::Efficient::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, non-power-of-two"); StreamCompaction::Efficient::scan(NPOT, c, a); diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index b2f739b..615515d 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -8,12 +8,56 @@ namespace Efficient { // TODO: __global__ +__global__ void upsweep_step(int d, int *x) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k % (int) powf(2, d + 1)) { + return; + } + x[k + (int) powf(2, d + 1) - 1] += x[k + (int) powf(2, d) - 1]; +} + +__global__ void downsweep_step(int d, int *x) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (k % (int)powf(2, d + 1)) { + return; + } + int t = x[k + (int) powf(2, d) - 1]; + x[k + (int) powf(2, d) - 1] = x[k + (int) powf(2, d + 1) - 1]; + x[k + (int) powf(2, d + 1) - 1] += t; +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - // TODO - printf("TODO\n"); + // copy everything in idata over to the GPU + dim3 dimBlock(n); + dim3 dimGrid(1); + int *dev_x; + cudaMalloc((void**)&dev_x, sizeof(int) * n); + + cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + // Up Sweep + int logn = ilog2ceil(n); + for (int d = 0; d < logn; d++) { + upsweep_step <<>>(d, dev_x); + } + + //debug: peek at the array after upsweep + //int peek[8]; + //cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); + + // Down-Sweep + int zero[1]; + zero[0] = 0; + cudaMemcpy(&dev_x[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + for (int d = logn - 1; d >= 0; d--) { + downsweep_step <<>>(d, dev_x); + } + + cudaMemcpy(odata, dev_x, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaFree(dev_x); } /** From c26c59975edae6ee85485165a5882fe23e6bada0 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 16:45:58 -0400 Subject: [PATCH 05/22] addressed powers of 2 problems. I think. --- src/main.cpp | 5 +++++ stream_compaction/efficient.cu | 24 +++++++++++++++++------- stream_compaction/naive.cu | 2 ++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 276b880..e97a616 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -81,6 +81,11 @@ int main(int argc, char* argv[]) { StreamCompaction::Efficient::scan(8, c, small); printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); + printDesc("small work efficient scan test, non-power-of-two."); + StreamCompaction::Efficient::scan(7, c, small); + printCmpResult(7, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, non-power-of-two"); StreamCompaction::Efficient::scan(NPOT, c, a); diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 615515d..584933b 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -26,32 +26,42 @@ __global__ void downsweep_step(int d, int *x) { x[k + (int) powf(2, d + 1) - 1] += t; } +__global__ void fill_by_value(int val, int *x) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + x[k] = val; +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - // copy everything in idata over to the GPU - dim3 dimBlock(n); + + // copy everything in idata over to the GPU. + // we'll need to pad the device memory with 0s to get a power of 2 array size. + int logn = ilog2ceil(n); + int pow2 = (int)pow(2, logn); + + dim3 dimBlock(pow2); dim3 dimGrid(1); int *dev_x; - cudaMalloc((void**)&dev_x, sizeof(int) * n); + cudaMalloc((void**)&dev_x, sizeof(int) * pow2); + fill_by_value <<>>(0, dev_x); cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); // Up Sweep - int logn = ilog2ceil(n); for (int d = 0; d < logn; d++) { upsweep_step <<>>(d, dev_x); } //debug: peek at the array after upsweep - //int peek[8]; - //cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); + int peek[8]; + cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); // Down-Sweep int zero[1]; zero[0] = 0; - cudaMemcpy(&dev_x[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + cudaMemcpy(&dev_x[pow2 - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); for (int d = logn - 1; d >= 0; d--) { downsweep_step <<>>(d, dev_x); } diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 0441673..4fee645 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -50,6 +50,8 @@ void scan(int n, int *odata, const int *idata) { cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); // run steps. + // no need to pad with 0s to get a power of 2 array here, + // this can be an "unbalanced" binary tree of ops. int logn = ilog2ceil(n); for (int d = 1; d <= logn; d++) { naive_scan_step <<>>(d, dev_x, dev_x_next); From 10e1eea66259304b9eed42fcf5a2f98d73a5450f Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 17:03:53 -0400 Subject: [PATCH 06/22] dirty efficient compaction appears to be done --- stream_compaction/efficient.cu | 65 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 584933b..03b2e73 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -55,8 +55,8 @@ void scan(int n, int *odata, const int *idata) { } //debug: peek at the array after upsweep - int peek[8]; - cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); + //int peek[8]; + //cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); // Down-Sweep int zero[1]; @@ -70,6 +70,23 @@ void scan(int n, int *odata, const int *idata) { cudaFree(dev_x); } +__global__ void temporary_array(int *x, int *temp) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (x[k] != 0) { + temp[k] = 1; + } + else { + temp[k] = 0; + } +} + +__global__ void scatter(int *x, int *trueFalse, int* scan, int *out) { + int k = threadIdx.x + (blockIdx.x * blockDim.x); + if (trueFalse[k]) { + out[scan[k]] = x[k]; + } +} + /** * Performs stream compaction on idata, storing the result into odata. * All zeroes are discarded. @@ -80,8 +97,48 @@ void scan(int n, int *odata, const int *idata) { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { - // TODO - return -1; + dim3 dimBlock(n); + dim3 dimGrid(1); + int *dev_x; + int *dev_tmp; + cudaMalloc((void**)&dev_x, sizeof(int) * n); + cudaMalloc((void**)&dev_tmp, sizeof(int) * n); + + // copy everything in idata over to the GPU. + cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + // Step 1: compute temporary true/false array + temporary_array << > >(dev_x, dev_tmp); + + // Step 2: run efficient scan on the tmp array + // TODO: expose the CUDA relevant portions of efficient scan so we don't have to shunt around + int *trueArray = new int[n]; + int *scanArray = new int[n]; + cudaMemcpy(trueArray, dev_tmp, sizeof(int) * n, cudaMemcpyDeviceToHost); + scan(n, scanArray, trueArray); + + // Step 3: scatter + int *dev_scatter; + cudaMalloc((void**)&dev_scatter, sizeof(int) * n); + + int *dev_scan; + cudaMalloc((void**)&dev_scan, sizeof(int) * n); + cudaMemcpy(dev_scan, scanArray, sizeof(int) * n, cudaMemcpyHostToDevice); + + scatter << > >(dev_x, dev_tmp, dev_scan, dev_scatter); + + cudaMemcpy(odata, dev_scatter, sizeof(int) * n, cudaMemcpyDeviceToHost); + + int return_value = scanArray[n - 1]; + + delete trueArray; + delete scanArray; + cudaFree(dev_x); + cudaFree(dev_tmp); + cudaFree(dev_scan); + cudaFree(dev_scatter); + + return return_value; } } From bdfca8d53b333ed66d1680335917031d4cd0097c Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 17:56:56 -0400 Subject: [PATCH 07/22] finished thrust version --- src/main.cpp | 5 +++++ stream_compaction/thrust.cu | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index e97a616..f6a3a6b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -92,6 +92,11 @@ int main(int argc, char* argv[]) { //printArray(NPOT, c, true); printCmpResult(NPOT, b, c); + zeroArray(SIZE, c); + printDesc("small thrust scan."); + StreamCompaction::Thrust::scan(8, c, small); + printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); printDesc("thrust scan, power-of-two"); StreamCompaction::Thrust::scan(SIZE, c, a); diff --git a/stream_compaction/thrust.cu b/stream_compaction/thrust.cu index d8dbb32..91807d0 100644 --- a/stream_compaction/thrust.cu +++ b/stream_compaction/thrust.cu @@ -13,9 +13,21 @@ namespace Thrust { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { - // TODO use `thrust::exclusive_scan` + // 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()); + + // Create a thrust::device_vector from a thrust::host_vector + thrust::host_vector v_in(idata, idata + n); + thrust::device_vector device_v_in(v_in); + thrust::device_vector device_v_out(n); + thrust::exclusive_scan(device_v_in.begin(), device_v_in.end(), + device_v_out.begin()); + + // copy back over + for (int i = 0; i < n; i++) { + odata[i] = device_v_out[i]; + } } } From 8948799b7c5535914d740fd24c75bb352f5979f5 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 18:33:10 -0400 Subject: [PATCH 08/22] isolated upsweep and downsweep. I think it should be faster this way, less memcpy --- stream_compaction/efficient.cu | 68 +++++++++++++++++++--------------- stream_compaction/efficient.h | 2 + 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 03b2e73..fca0a57 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -49,9 +49,22 @@ void scan(int n, int *odata, const int *idata) { cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); - // Up Sweep + // up sweep and down sweep + up_sweep_down_sweep(pow2, dev_x); + + cudaMemcpy(odata, dev_x, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaFree(dev_x); +} + +// exposed up sweep and down sweep. expects powers of two! +void up_sweep_down_sweep(int n, int *dev_data) { + int logn = ilog2ceil(n); + dim3 dimBlock(n); + dim3 dimGrid(1); + + // Up Sweep for (int d = 0; d < logn; d++) { - upsweep_step <<>>(d, dev_x); + upsweep_step <<>>(d, dev_data); } //debug: peek at the array after upsweep @@ -61,13 +74,10 @@ void scan(int n, int *odata, const int *idata) { // Down-Sweep int zero[1]; zero[0] = 0; - cudaMemcpy(&dev_x[pow2 - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + cudaMemcpy(&dev_data[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); for (int d = logn - 1; d >= 0; d--) { - downsweep_step <<>>(d, dev_x); + downsweep_step << > >(d, dev_data); } - - cudaMemcpy(odata, dev_x, sizeof(int) * n, cudaMemcpyDeviceToHost); - cudaFree(dev_x); } __global__ void temporary_array(int *x, int *temp) { @@ -97,42 +107,42 @@ __global__ void scatter(int *x, int *trueFalse, int* scan, int *out) { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { - dim3 dimBlock(n); + int logn = ilog2ceil(n); + int pow2 = (int)pow(2, logn); + + dim3 dimBlock(pow2); dim3 dimGrid(1); int *dev_x; int *dev_tmp; - cudaMalloc((void**)&dev_x, sizeof(int) * n); - cudaMalloc((void**)&dev_tmp, sizeof(int) * n); + int *dev_scatter; + int *dev_scan; + cudaMalloc((void**)&dev_x, sizeof(int) * pow2); + cudaMalloc((void**)&dev_tmp, sizeof(int) * pow2); + cudaMalloc((void**)&dev_scan, sizeof(int) * pow2); + cudaMalloc((void**)&dev_scatter, sizeof(int) * pow2); + + // 0 pad up to a power of 2 array length. // copy everything in idata over to the GPU. - cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + fill_by_value << > >(0, dev_x); + cudaMemcpy(dev_x, idata, sizeof(int) * pow2, cudaMemcpyHostToDevice); // Step 1: compute temporary true/false array - temporary_array << > >(dev_x, dev_tmp); + temporary_array <<>>(dev_x, dev_tmp); // Step 2: run efficient scan on the tmp array - // TODO: expose the CUDA relevant portions of efficient scan so we don't have to shunt around - int *trueArray = new int[n]; - int *scanArray = new int[n]; - cudaMemcpy(trueArray, dev_tmp, sizeof(int) * n, cudaMemcpyDeviceToHost); - scan(n, scanArray, trueArray); + cudaMemcpy(dev_scan, dev_tmp, sizeof(int) * pow2, cudaMemcpyDeviceToDevice); + up_sweep_down_sweep(pow2, dev_scan); // Step 3: scatter - int *dev_scatter; - cudaMalloc((void**)&dev_scatter, sizeof(int) * n); - - int *dev_scan; - cudaMalloc((void**)&dev_scan, sizeof(int) * n); - cudaMemcpy(dev_scan, scanArray, sizeof(int) * n, cudaMemcpyHostToDevice); - - scatter << > >(dev_x, dev_tmp, dev_scan, dev_scatter); + scatter <<>>(dev_x, dev_tmp, dev_scan, dev_scatter); - cudaMemcpy(odata, dev_scatter, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaMemcpy(odata, dev_scatter, sizeof(int) * pow2, cudaMemcpyDeviceToHost); - int return_value = scanArray[n - 1]; + int return_value; + cudaMemcpy(&return_value, dev_scan + (n - 1), sizeof(int), + cudaMemcpyDeviceToHost); - delete trueArray; - delete scanArray; cudaFree(dev_x); cudaFree(dev_tmp); cudaFree(dev_scan); diff --git a/stream_compaction/efficient.h b/stream_compaction/efficient.h index 395ba10..a550771 100644 --- a/stream_compaction/efficient.h +++ b/stream_compaction/efficient.h @@ -4,6 +4,8 @@ namespace StreamCompaction { namespace Efficient { void scan(int n, int *odata, const int *idata); + void up_sweep_down_sweep(int n, int *dev_data); + int compact(int n, int *odata, const int *idata); } } From 420072401df29c933351ad5b8e04d59f1f5f8e3e Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 19:25:52 -0400 Subject: [PATCH 09/22] fixed a small problem with returned values in compact --- src/main.cpp | 35 ++++++++++++++++++++++++++++++++-- stream_compaction/cpu.cu | 2 +- stream_compaction/efficient.cu | 16 ++++++++++------ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index f6a3a6b..d41d25d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,6 +24,7 @@ int main(int argc, char* argv[]) { small[i] = i; } int smallScan[8] = { 0, 0, 1, 3, 6, 10, 15, 21 }; + int smallCompact[7] = { 1, 2, 3, 4, 5, 6, 7 }; // Scan tests @@ -122,6 +123,16 @@ int main(int argc, char* argv[]) { int count, expectedCount, expectedNPOT; + zeroArray(SIZE, c); + printDesc("small cpu compact without scan, power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(8, c, small); + printCmpLenResult(count, 7, smallCompact, c); + + zeroArray(SIZE, c); + printDesc("small cpu compact without scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(7, c, small); + printCmpLenResult(count, 6, smallCompact, c); + zeroArray(SIZE, b); printDesc("cpu compact without scan, power-of-two"); count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); @@ -136,22 +147,42 @@ int main(int argc, char* argv[]) { printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); + zeroArray(SIZE, c); + printDesc("small cpu compact with scan, power-of-two"); + count = StreamCompaction::CPU::compactWithScan(8, c, small); + printCmpLenResult(count, 7, smallCompact, c); + + zeroArray(SIZE, c); + printDesc("small cpu compact with scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithScan(7, c, small); + printCmpLenResult(count, 6, smallCompact, c); + zeroArray(SIZE, c); printDesc("cpu compact with scan"); count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); printArray(count, c, true); printCmpLenResult(count, expectedCount, b, c); + zeroArray(SIZE, c); + printDesc("small work-efficient compact with scan, power-of-two"); + count = StreamCompaction::Efficient::compact(8, c, small); + printCmpLenResult(count, 7, smallCompact, c); + + zeroArray(SIZE, c); + printDesc("small work-efficient compact with scan, non-power-of-two"); + count = StreamCompaction::Efficient::compact(7, c, small); + printCmpLenResult(count, 6, smallCompact, c); + zeroArray(SIZE, c); printDesc("work-efficient compact, power-of-two"); count = StreamCompaction::Efficient::compact(SIZE, c, a); - //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); - //printArray(count, c, true); + printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); printf("done\n"); } diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index 5133cd6..d354188 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -59,8 +59,8 @@ int compactWithScan(int n, int *odata, const int *idata) { odata[trueScan[i]] = idata[i]; } } + int numRemaining = trueScan[n - 1] + trueArray[n - 1]; delete trueArray; - int numRemaining = trueScan[n - 1]; delete trueScan; return numRemaining; } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index fca0a57..64b5d5b 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -125,22 +125,26 @@ int compact(int n, int *odata, const int *idata) { // 0 pad up to a power of 2 array length. // copy everything in idata over to the GPU. fill_by_value << > >(0, dev_x); - cudaMemcpy(dev_x, idata, sizeof(int) * pow2, cudaMemcpyHostToDevice); + cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); // Step 1: compute temporary true/false array temporary_array <<>>(dev_x, dev_tmp); // Step 2: run efficient scan on the tmp array cudaMemcpy(dev_scan, dev_tmp, sizeof(int) * pow2, cudaMemcpyDeviceToDevice); - up_sweep_down_sweep(pow2, dev_scan); + up_sweep_down_sweep(pow2, dev_scan); // Step 3: scatter scatter <<>>(dev_x, dev_tmp, dev_scan, dev_scatter); - cudaMemcpy(odata, dev_scatter, sizeof(int) * pow2, cudaMemcpyDeviceToHost); + cudaMemcpy(odata, dev_scatter, sizeof(int) * n, cudaMemcpyDeviceToHost); + + int last_index; + cudaMemcpy(&last_index, dev_scan + (n - 1), sizeof(int), + cudaMemcpyDeviceToHost); - int return_value; - cudaMemcpy(&return_value, dev_scan + (n - 1), sizeof(int), + int last_true_false; + cudaMemcpy(&last_true_false, dev_tmp + (n - 1), sizeof(int), cudaMemcpyDeviceToHost); cudaFree(dev_x); @@ -148,7 +152,7 @@ int compact(int n, int *odata, const int *idata) { cudaFree(dev_scan); cudaFree(dev_scatter); - return return_value; + return last_index + last_true_false; } } From 51af372fda409225ddfe465bc8175d65ba41422a Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 19:55:20 -0400 Subject: [PATCH 10/22] removed need for a parallel shifting kernel --- stream_compaction/naive.cu | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 4fee645..825797a 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -24,15 +24,6 @@ __global__ void parallel_copy(int *data, int *copy) { copy[i] = data[i]; } -__global__ void parallel_shift(int *inclusive, int *exclusive) { - int i = threadIdx.x + (blockIdx.x * blockDim.x); - if (i == 0) { - exclusive[i] = 0; - return; - } - exclusive[i] = inclusive[i - 1]; -} - /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ @@ -42,10 +33,8 @@ void scan(int n, int *odata, const int *idata) { dim3 dimGrid(1); int *dev_x; int *dev_x_next; - int *dev_exclusive; cudaMalloc((void**)&dev_x, sizeof(int) * n); cudaMalloc((void**)&dev_x_next, sizeof(int) * n); - cudaMalloc((void**)&dev_exclusive, sizeof(int) * n); cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); @@ -58,12 +47,11 @@ void scan(int n, int *odata, const int *idata) { parallel_copy <<>>(dev_x_next, dev_x); } - parallel_shift << > >(dev_x, dev_exclusive); + cudaMemcpy(odata + 1, dev_x, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); + odata[0] = 0; + cudaFree(dev_x); cudaFree(dev_x_next); - - cudaMemcpy(odata, dev_exclusive, sizeof(int) * n, cudaMemcpyDeviceToHost); - cudaFree(dev_exclusive); } } From 0bf441fbe5bf479dcde3d96069c6fa9f0c28fb5f Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 20:13:13 -0400 Subject: [PATCH 11/22] added ping ponging to get around excess memcpy in naive --- src/main.cpp | 7 ++++++- stream_compaction/naive.cu | 10 ++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index d41d25d..c3bc09e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "testing_helpers.hpp" int main(int argc, char* argv[]) { - const int SIZE = 1 << 8; + const int SIZE = 1 << 9; const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; @@ -71,6 +71,11 @@ int main(int argc, char* argv[]) { StreamCompaction::Naive::scan(8, c, small); printCmpResult(8, smallScan, c); + zeroArray(SIZE, c); + printDesc("small naive scan test, non-power-of-two."); + StreamCompaction::Naive::scan(7, c, small); + printCmpResult(7, smallScan, c); + zeroArray(SIZE, c); printDesc("work-efficient scan, power-of-two"); StreamCompaction::Efficient::scan(SIZE, c, a); diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 825797a..45cf67b 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -19,11 +19,6 @@ __global__ void naive_scan_step(int d, int *x, int *x_next) { } } -__global__ void parallel_copy(int *data, int *copy) { - int i = threadIdx.x + (blockIdx.x * blockDim.x); - copy[i] = data[i]; -} - /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ @@ -37,6 +32,7 @@ void scan(int n, int *odata, const int *idata) { cudaMalloc((void**)&dev_x_next, sizeof(int) * n); cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + cudaMemcpy(dev_x_next, dev_x, sizeof(int) * n, cudaMemcpyDeviceToDevice); // run steps. // no need to pad with 0s to get a power of 2 array here, @@ -44,7 +40,9 @@ void scan(int n, int *odata, const int *idata) { int logn = ilog2ceil(n); for (int d = 1; d <= logn; d++) { naive_scan_step <<>>(d, dev_x, dev_x_next); - parallel_copy <<>>(dev_x_next, dev_x); + int *temp = dev_x_next; + dev_x_next = dev_x; + dev_x = temp; } cudaMemcpy(odata + 1, dev_x, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); From bc9345ef86d1b599b1abaa4d02fbc49629d9c9e7 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 21:05:53 -0400 Subject: [PATCH 12/22] attempting to avoid race conditions is efficient, but had to resort to memcpy --- src/main.cpp | 12 +++++------ stream_compaction/efficient.cu | 37 +++++++++++++++++++++++----------- stream_compaction/naive.cu | 6 +++--- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c3bc09e..3256525 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -76,12 +76,6 @@ int main(int argc, char* argv[]) { StreamCompaction::Naive::scan(7, c, small); printCmpResult(7, smallScan, c); - zeroArray(SIZE, c); - printDesc("work-efficient scan, power-of-two"); - StreamCompaction::Efficient::scan(SIZE, c, a); - //printArray(SIZE, c, true); - printCmpResult(SIZE, b, c); - zeroArray(SIZE, c); printDesc("small work efficient scan test."); StreamCompaction::Efficient::scan(8, c, small); @@ -92,6 +86,12 @@ int main(int argc, char* argv[]) { StreamCompaction::Efficient::scan(7, c, small); printCmpResult(7, smallScan, c); + zeroArray(SIZE, c); + printDesc("work-efficient scan, power-of-two"); + StreamCompaction::Efficient::scan(SIZE, c, a); + //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); diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 64b5d5b..6e34018 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -8,22 +8,24 @@ namespace Efficient { // TODO: __global__ -__global__ void upsweep_step(int d, int *x) { +__global__ void upsweep_step(int d, int *x, int *out) { int k = threadIdx.x + (blockIdx.x * blockDim.x); if (k % (int) powf(2, d + 1)) { return; } - x[k + (int) powf(2, d + 1) - 1] += x[k + (int) powf(2, d) - 1]; + int out_index = k + (int)powf(2, d + 1) - 1; + out[out_index] = x[out_index] + x[k + (int)powf(2, d) - 1]; } -__global__ void downsweep_step(int d, int *x) { +__global__ void downsweep_step(int d, int *x, int *out) { int k = threadIdx.x + (blockIdx.x * blockDim.x); if (k % (int)powf(2, d + 1)) { return; } - int t = x[k + (int) powf(2, d) - 1]; - x[k + (int) powf(2, d) - 1] = x[k + (int) powf(2, d + 1) - 1]; - x[k + (int) powf(2, d + 1) - 1] += t; + int left_index = k + (int)powf(2, d) - 1; + int right_index = k + (int)powf(2, d + 1) - 1; + out[left_index] = x[right_index]; + out[right_index] = x[right_index] + x[left_index]; } __global__ void fill_by_value(int val, int *x) { @@ -57,27 +59,38 @@ void scan(int n, int *odata, const int *idata) { } // exposed up sweep and down sweep. expects powers of two! -void up_sweep_down_sweep(int n, int *dev_data) { +void up_sweep_down_sweep(int n, int *dev_data1) { int logn = ilog2ceil(n); dim3 dimBlock(n); dim3 dimGrid(1); + int *dev_data2; + cudaMalloc((void**)&dev_data2, sizeof(int) * n); + cudaMemcpy(dev_data2, dev_data1, sizeof(int) * n, cudaMemcpyDeviceToDevice); + // Up Sweep for (int d = 0; d < logn; d++) { - upsweep_step <<>>(d, dev_data); + upsweep_step <<>>(d, dev_data1, dev_data2); + cudaMemcpy(dev_data1, dev_data2, sizeof(int) * n, cudaMemcpyDeviceToDevice); } //debug: peek at the array after upsweep - //int peek[8]; - //cudaMemcpy(&peek, dev_x, sizeof(int) * 8, cudaMemcpyDeviceToHost); + //int peek1[8]; + //int peek2[8]; + //cudaMemcpy(&peek1, dev_data1, sizeof(int) * 8, cudaMemcpyDeviceToHost); + //cudaMemcpy(&peek2, dev_data2, sizeof(int) * 8, cudaMemcpyDeviceToHost); // Down-Sweep int zero[1]; zero[0] = 0; - cudaMemcpy(&dev_data[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + cudaMemcpy(&dev_data1[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + cudaMemcpy(dev_data2, dev_data1, sizeof(int) * n, cudaMemcpyDeviceToDevice); for (int d = logn - 1; d >= 0; d--) { - downsweep_step << > >(d, dev_data); + downsweep_step << > >(d, dev_data1, dev_data2); + cudaMemcpy(dev_data1, dev_data2, sizeof(int) * n, cudaMemcpyDeviceToDevice); } + + cudaFree(dev_data2); } __global__ void temporary_array(int *x, int *temp) { diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 45cf67b..e60ff7e 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -8,14 +8,14 @@ namespace Naive { // TODO: __global__ -__global__ void naive_scan_step(int d, int *x, int *x_next) { +__global__ void naive_scan_step(int d, int *x_1, int *x_2) { int i = threadIdx.x + (blockIdx.x * blockDim.x); int offset = powf(2, d - 1); if (i >= offset) { - x_next[i] = x[i - offset] + x[i]; + x_2[i] = x_1[i - offset] + x_1[i]; } else { - x_next[i] = x[i]; + x_2[i] = x_1[i]; } } From 02862f7b70d0ed77d38dfd75f0d8ffa3e31fcd86 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 21:29:29 -0400 Subject: [PATCH 13/22] things seem to work now for over 1024 --- src/main.cpp | 2 +- stream_compaction/efficient.cu | 42 ++++++++++++++++++++++++---------- stream_compaction/naive.cu | 17 ++++++++++++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3256525..eccfc75 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "testing_helpers.hpp" int main(int argc, char* argv[]) { - const int SIZE = 1 << 9; + const int SIZE = 1 << 11; const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 6e34018..8a3c921 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -33,6 +33,23 @@ __global__ void fill_by_value(int val, int *x) { x[k] = val; } +static void setup_dimms(dim3 &dimBlock, dim3 &dimGrid, int n) { + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, 0); + int tpb = deviceProp.maxThreadsPerBlock; + int blockWidth = fmin(n, tpb); + int blocks = 1; + if (blockWidth != n) { + blocks = n / tpb; + if (n % tpb) { + blocks ++; + } + } + + dimBlock = dim3(blockWidth); + dimGrid = dim3(blocks); +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ @@ -43,8 +60,10 @@ void scan(int n, int *odata, const int *idata) { int logn = ilog2ceil(n); int pow2 = (int)pow(2, logn); - dim3 dimBlock(pow2); - dim3 dimGrid(1); + dim3 dimBlock; + dim3 dimGrid; + setup_dimms(dimBlock, dimGrid, pow2); + int *dev_x; cudaMalloc((void**)&dev_x, sizeof(int) * pow2); fill_by_value <<>>(0, dev_x); @@ -61,8 +80,10 @@ void scan(int n, int *odata, const int *idata) { // exposed up sweep and down sweep. expects powers of two! void up_sweep_down_sweep(int n, int *dev_data1) { int logn = ilog2ceil(n); - dim3 dimBlock(n); - dim3 dimGrid(1); + + dim3 dimBlock; + dim3 dimGrid; + setup_dimms(dimBlock, dimGrid, n); int *dev_data2; cudaMalloc((void**)&dev_data2, sizeof(int) * n); @@ -95,12 +116,7 @@ void up_sweep_down_sweep(int n, int *dev_data1) { __global__ void temporary_array(int *x, int *temp) { int k = threadIdx.x + (blockIdx.x * blockDim.x); - if (x[k] != 0) { - temp[k] = 1; - } - else { - temp[k] = 0; - } + temp[k] = (x[k] != 0); } __global__ void scatter(int *x, int *trueFalse, int* scan, int *out) { @@ -123,8 +139,10 @@ int compact(int n, int *odata, const int *idata) { int logn = ilog2ceil(n); int pow2 = (int)pow(2, logn); - dim3 dimBlock(pow2); - dim3 dimGrid(1); + dim3 dimBlock; + dim3 dimGrid; + setup_dimms(dimBlock, dimGrid, pow2); + int *dev_x; int *dev_tmp; int *dev_scatter; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index e60ff7e..ceb2fd1 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -24,8 +24,21 @@ __global__ void naive_scan_step(int d, int *x_1, int *x_2) { */ void scan(int n, int *odata, const int *idata) { // copy everything in idata over to the GPU - dim3 dimBlock(n); - dim3 dimGrid(1); + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, 0); + int tpb = deviceProp.maxThreadsPerBlock; + int blockWidth = fmin(n, tpb); + int blocks = 1; + if (blockWidth != n) { + blocks = n / tpb; + if (n % tpb) { + blocks++; + } + } + + dim3 dimBlock(blockWidth); + dim3 dimGrid(blocks); + int *dev_x; int *dev_x_next; cudaMalloc((void**)&dev_x, sizeof(int) * n); From f7b2b8cd5f8a43d3883af35a862a3d10f2d7ff37 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 21:47:08 -0400 Subject: [PATCH 14/22] so it seems efficient can still do stuff in place without ill effects with many blocks? --- src/main.cpp | 2 +- stream_compaction/efficient.cu | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index eccfc75..5aaded2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "testing_helpers.hpp" int main(int argc, char* argv[]) { - const int SIZE = 1 << 11; + const int SIZE = 1 << 13; const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 8a3c921..a5c5c6a 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -8,24 +8,22 @@ namespace Efficient { // TODO: __global__ -__global__ void upsweep_step(int d, int *x, int *out) { +__global__ void upsweep_step(int d, int *x) { int k = threadIdx.x + (blockIdx.x * blockDim.x); if (k % (int) powf(2, d + 1)) { return; } - int out_index = k + (int)powf(2, d + 1) - 1; - out[out_index] = x[out_index] + x[k + (int)powf(2, d) - 1]; + x[k + (int)powf(2, d + 1) - 1] += x[k + (int)powf(2, d) - 1]; } -__global__ void downsweep_step(int d, int *x, int *out) { +__global__ void downsweep_step(int d, int *x) { int k = threadIdx.x + (blockIdx.x * blockDim.x); if (k % (int)powf(2, d + 1)) { return; } - int left_index = k + (int)powf(2, d) - 1; - int right_index = k + (int)powf(2, d + 1) - 1; - out[left_index] = x[right_index]; - out[right_index] = x[right_index] + x[left_index]; + int t = x[k + (int)powf(2, d) - 1]; + x[k + (int)powf(2, d) - 1] = x[k + (int)powf(2, d + 1) - 1]; + x[k + (int)powf(2, d + 1) - 1] += t; } __global__ void fill_by_value(int val, int *x) { @@ -85,33 +83,22 @@ void up_sweep_down_sweep(int n, int *dev_data1) { dim3 dimGrid; setup_dimms(dimBlock, dimGrid, n); - int *dev_data2; - cudaMalloc((void**)&dev_data2, sizeof(int) * n); - cudaMemcpy(dev_data2, dev_data1, sizeof(int) * n, cudaMemcpyDeviceToDevice); - // Up Sweep for (int d = 0; d < logn; d++) { - upsweep_step <<>>(d, dev_data1, dev_data2); - cudaMemcpy(dev_data1, dev_data2, sizeof(int) * n, cudaMemcpyDeviceToDevice); + upsweep_step << > >(d, dev_data1); } //debug: peek at the array after upsweep //int peek1[8]; - //int peek2[8]; //cudaMemcpy(&peek1, dev_data1, sizeof(int) * 8, cudaMemcpyDeviceToHost); - //cudaMemcpy(&peek2, dev_data2, sizeof(int) * 8, cudaMemcpyDeviceToHost); // Down-Sweep int zero[1]; zero[0] = 0; cudaMemcpy(&dev_data1[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); - cudaMemcpy(dev_data2, dev_data1, sizeof(int) * n, cudaMemcpyDeviceToDevice); for (int d = logn - 1; d >= 0; d--) { - downsweep_step << > >(d, dev_data1, dev_data2); - cudaMemcpy(dev_data1, dev_data2, sizeof(int) * n, cudaMemcpyDeviceToDevice); + downsweep_step << > >(d, dev_data1); } - - cudaFree(dev_data2); } __global__ void temporary_array(int *x, int *temp) { From efbf43829ab5462512084dea6c64c787ca9ef14d Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 22:50:25 -0400 Subject: [PATCH 15/22] added benchmark code --- src/main.cpp | 47 +++++++++++++++++++++++++++++++++- stream_compaction/common.h | 1 + stream_compaction/cpu.cu | 46 +++++++++++++++++++++++++++++++-- stream_compaction/cpu.h | 2 ++ stream_compaction/efficient.cu | 40 +++++++++++++++++++++++++++++ stream_compaction/naive.cu | 30 ++++++++++++++++++++++ stream_compaction/thrust.cu | 32 +++++++++++++++++++++++ 7 files changed, 195 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5aaded2..0017589 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "testing_helpers.hpp" int main(int argc, char* argv[]) { - const int SIZE = 1 << 13; + const int SIZE = 1 << 15; const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; @@ -26,6 +26,51 @@ int main(int argc, char* argv[]) { int smallScan[8] = { 0, 0, 1, 3, 6, 10, 15, 21 }; int smallCompact[7] = { 1, 2, 3, 4, 5, 6, 7 }; + // set "false" for standard tests + if (true) { + genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case + a[SIZE - 1] = 0; + printf("array size: %i\n", SIZE); + int count; + + zeroArray(SIZE, b); + printDesc("cpu scan, power-of-two"); + StreamCompaction::CPU::scan(SIZE, b, a); + printf("\n"); + + zeroArray(SIZE, c); + printDesc("naive scan, power-of-two"); + StreamCompaction::Naive::scan(SIZE, c, a); + printf("\n"); + + zeroArray(SIZE, c); + printDesc("work-efficient scan, power-of-two"); + StreamCompaction::Efficient::scan(SIZE, c, a); + printf("\n"); + + zeroArray(SIZE, c); + printDesc("thrust scan, power-of-two"); + StreamCompaction::Thrust::scan(SIZE, c, a); + printf("\n"); + + zeroArray(SIZE, b); + printDesc("cpu compact without scan, power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); + printf("\n"); + + zeroArray(SIZE, c); + printDesc("cpu compact with scan"); + count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); + printf("\n"); + + zeroArray(SIZE, c); + printDesc("work-efficient compact, power-of-two"); + count = StreamCompaction::Efficient::compact(SIZE, c, a); + printf("\n"); + + printf("benchmark tests done\n"); + return 0; + } // Scan tests diff --git a/stream_compaction/common.h b/stream_compaction/common.h index 4f52663..9ee943b 100644 --- a/stream_compaction/common.h +++ b/stream_compaction/common.h @@ -6,6 +6,7 @@ #define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) #define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) +#define BENCHMARK 1 /** * Check for CUDA errors; print and exit if there was a problem. diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index d354188..87f5fd7 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -1,5 +1,6 @@ #include #include "cpu.h" +#include "common.h" namespace StreamCompaction { namespace CPU { @@ -8,11 +9,24 @@ namespace CPU { * CPU scan (prefix sum). */ void scan(int n, int *odata, const int *idata) { + std::chrono::high_resolution_clock::time_point t1; + if (BENCHMARK) { + t1 = std::chrono::high_resolution_clock::now(); + } + + // Implement exclusive serial scan on CPU odata[0] = 0; for (int i = 1; i < n; i++) { odata[i] = odata[i - 1] + idata[i - 1]; } + + if (BENCHMARK) { + std::chrono::high_resolution_clock::time_point t2 = + std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(t2 - t1).count(); + std::cout << duration << " microseconds.\n"; + } } /** @@ -21,6 +35,11 @@ void scan(int n, int *odata, const int *idata) { * @returns the number of elements remaining after compaction. */ int compactWithoutScan(int n, int *odata, const int *idata) { + std::chrono::high_resolution_clock::time_point t1; + if (BENCHMARK) { + t1 = std::chrono::high_resolution_clock::now(); + } + // remove all 0s from the array of ints int odataIndex = 0; for (int i = 0; i < n; i++) { @@ -30,6 +49,14 @@ int compactWithoutScan(int n, int *odata, const int *idata) { odata[odataIndex] = idata[i]; odataIndex++; } + + if (BENCHMARK) { + std::chrono::high_resolution_clock::time_point t2 = + std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(t2 - t1).count(); + std::cout << duration << " microseconds.\n"; + } + return odataIndex; } @@ -39,8 +66,16 @@ int compactWithoutScan(int n, int *odata, const int *idata) { * @returns the number of elements remaining after compaction. */ int compactWithScan(int n, int *odata, const int *idata) { - // Step 1: Compute temporary values in odata int *trueArray = new int[n]; + int *trueScan = new int[n]; + + std::chrono::high_resolution_clock::time_point t1; + if (BENCHMARK) { + t1 = std::chrono::high_resolution_clock::now(); + } + + + // Step 1: Compute temporary values in odata for (int i = 0; i < n; i++) { if (idata[i] == 0) { trueArray[i] = 0; @@ -50,7 +85,6 @@ int compactWithScan(int n, int *odata, const int *idata) { } } // Step 2: Run exclusive scan on temporary array - int *trueScan = new int[n]; scan(n, trueScan, trueArray); // Step 3: Scatter @@ -60,6 +94,14 @@ int compactWithScan(int n, int *odata, const int *idata) { } } int numRemaining = trueScan[n - 1] + trueArray[n - 1]; + + if (BENCHMARK) { + std::chrono::high_resolution_clock::time_point t2 = + std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(t2 - t1).count(); + std::cout << duration << " microseconds.\n"; + } + delete trueArray; delete trueScan; return numRemaining; diff --git a/stream_compaction/cpu.h b/stream_compaction/cpu.h index 6348bf3..21cce3a 100644 --- a/stream_compaction/cpu.h +++ b/stream_compaction/cpu.h @@ -1,4 +1,6 @@ #pragma once +#include +#include namespace StreamCompaction { namespace CPU { diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index a5c5c6a..f08479a 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -6,6 +6,28 @@ namespace StreamCompaction { namespace Efficient { +cudaEvent_t start, stop; + +static void setup_timer_events() { + cudaEventCreate(&start); + cudaEventCreate(&stop); + + cudaEventRecord(start); +} + +static float teardown_timer_events() { + cudaEventRecord(stop); + + cudaEventSynchronize(stop); + float milliseconds = 0; + cudaEventElapsedTime(&milliseconds, start, stop); + + cudaEventDestroy(start); + cudaEventDestroy(stop); + + return milliseconds; +} + // TODO: __global__ __global__ void upsweep_step(int d, int *x) { @@ -68,9 +90,18 @@ void scan(int n, int *odata, const int *idata) { cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + if (BENCHMARK) { + setup_timer_events(); + } + // up sweep and down sweep up_sweep_down_sweep(pow2, dev_x); + if (BENCHMARK) { + printf("%f microseconds.\n", + teardown_timer_events() * 1000.0f); + } + cudaMemcpy(odata, dev_x, sizeof(int) * n, cudaMemcpyDeviceToHost); cudaFree(dev_x); } @@ -145,6 +176,10 @@ int compact(int n, int *odata, const int *idata) { fill_by_value << > >(0, dev_x); cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + if (BENCHMARK) { + setup_timer_events(); + } + // Step 1: compute temporary true/false array temporary_array <<>>(dev_x, dev_tmp); @@ -155,6 +190,11 @@ int compact(int n, int *odata, const int *idata) { // Step 3: scatter scatter <<>>(dev_x, dev_tmp, dev_scan, dev_scatter); + if (BENCHMARK) { + printf("%f microseconds.\n", + teardown_timer_events() * 1000.0f); + } + cudaMemcpy(odata, dev_scatter, sizeof(int) * n, cudaMemcpyDeviceToHost); int last_index; diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index ceb2fd1..4671b0a 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -6,6 +6,28 @@ namespace StreamCompaction { namespace Naive { +cudaEvent_t start, stop; + +static void setup_timer_events() { + cudaEventCreate(&start); + cudaEventCreate(&stop); + + cudaEventRecord(start); +} + +static float teardown_timer_events() { + cudaEventRecord(stop); + + cudaEventSynchronize(stop); + float milliseconds = 0; + cudaEventElapsedTime(&milliseconds, start, stop); + + cudaEventDestroy(start); + cudaEventDestroy(stop); + + return milliseconds; +} + // TODO: __global__ __global__ void naive_scan_step(int d, int *x_1, int *x_2) { @@ -47,6 +69,10 @@ void scan(int n, int *odata, const int *idata) { cudaMemcpy(dev_x, idata, sizeof(int) * n, cudaMemcpyHostToDevice); cudaMemcpy(dev_x_next, dev_x, sizeof(int) * n, cudaMemcpyDeviceToDevice); + if (BENCHMARK) { + setup_timer_events(); + } + // run steps. // no need to pad with 0s to get a power of 2 array here, // this can be an "unbalanced" binary tree of ops. @@ -57,6 +83,10 @@ void scan(int n, int *odata, const int *idata) { dev_x_next = dev_x; dev_x = temp; } + if (BENCHMARK) { + printf("%f microseconds.\n", + teardown_timer_events() * 1000.0f); + } cudaMemcpy(odata + 1, dev_x, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); odata[0] = 0; diff --git a/stream_compaction/thrust.cu b/stream_compaction/thrust.cu index 91807d0..be1200e 100644 --- a/stream_compaction/thrust.cu +++ b/stream_compaction/thrust.cu @@ -9,6 +9,28 @@ namespace StreamCompaction { namespace Thrust { +cudaEvent_t start, stop; + +static void setup_timer_events() { + cudaEventCreate(&start); + cudaEventCreate(&stop); + + cudaEventRecord(start); +} + +static float teardown_timer_events() { + cudaEventRecord(stop); + + cudaEventSynchronize(stop); + float milliseconds = 0; + cudaEventElapsedTime(&milliseconds, start, stop); + + cudaEventDestroy(start); + cudaEventDestroy(stop); + + return milliseconds; +} + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ @@ -21,9 +43,19 @@ void scan(int n, int *odata, const int *idata) { thrust::host_vector v_in(idata, idata + n); thrust::device_vector device_v_in(v_in); thrust::device_vector device_v_out(n); + + if (BENCHMARK) { + setup_timer_events(); + } + thrust::exclusive_scan(device_v_in.begin(), device_v_in.end(), device_v_out.begin()); + if (BENCHMARK) { + printf("%f microseconds.\n", + teardown_timer_events() * 1000.0f); + } + // copy back over for (int i = 0; i < n; i++) { odata[i] = device_v_out[i]; From 89c1f4b0a2b38240528ab7264f74c7d83d8765d8 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 23:21:55 -0400 Subject: [PATCH 16/22] added readme --- README.md | 313 +++++++++++++++-------------------------------- data.ods | Bin 0 -> 11110 bytes images/graph.png | Bin 0 -> 31606 bytes src/main.cpp | 3 +- 4 files changed, 98 insertions(+), 218 deletions(-) create mode 100644 data.ods create mode 100644 images/graph.png diff --git a/README.md b/README.md index 4535eea..4140c5d 100644 --- a/README.md +++ b/README.md @@ -3,220 +3,99 @@ CUDA Stream Compaction **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 2** -* (TODO) YOUR NAME HERE -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) - -### (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.) - -Instructions (delete me) -======================== - -This is due Sunday, September 13 at midnight. - -**Summary:** In this project, you'll implement GPU stream compaction in CUDA, -from scratch. This algorithm is widely used, and will be important for -accelerating your path tracer project. - -Your stream compaction implementations in this project will simply remove `0`s -from an array of `int`s. In the path tracer, you will remove terminated paths -from an array of rays. - -In addition to being useful for your path tracer, this project is meant to -reorient your algorithmic thinking to the way of the GPU. On GPUs, many -algorithms can benefit from massive parallelism and, in particular, data -parallelism: executing the same code many times simultaneously with different -data. - -You'll implement a few different versions of the *Scan* (*Prefix Sum*) -algorithm. First, you'll implement a CPU version of the algorithm to reinforce -your understanding. Then, you'll write a few GPU implementations: "naive" and -"work-efficient." Finally, you'll use some of these to implement GPU stream -compaction. - -**Algorithm overview & details:** There are two primary references for details -on the implementation of scan and stream compaction. - -* The [slides on Parallel Algorithms](https://github.com/CIS565-Fall-2015/cis565-fall-2015.github.io/raw/master/lectures/2-Parallel-Algorithms.pptx) - for Scan, Stream Compaction, and Work-Efficient Parallel Scan. -* GPU Gems 3, Chapter 39 - [Parallel Prefix Sum (Scan) with CUDA](http://http.developer.nvidia.com/GPUGems3/gpugems3_ch39.html). - -Your GPU stream compaction implementation will live inside of the -`stream_compaction` subproject. This way, you will be able to easily copy it -over for use in your GPU path tracer. - - -## Part 0: The Usual - -This project (and all other CUDA projects in this course) requires an NVIDIA -graphics card with CUDA capability. Any card with Compute Capability 2.0 -(`sm_20`) or greater will work. Check your GPU on this -[compatibility table](https://developer.nvidia.com/cuda-gpus). -If you do not have a personal machine with these specs, you may use those -computers in the Moore 100B/C which have supported GPUs. - -**HOWEVER**: If you need to use the lab computer for your development, you will -not presently be able to do GPU performance profiling. This will be very -important for debugging performance bottlenecks in your program. - -### Useful existing code - -* `stream_compaction/common.h` - * `checkCUDAError` macro: checks for CUDA errors and exits if there were any. - * `ilog2ceil(x)`: computes the ceiling of log2(x), as an integer. -* `main.cpp` - * Some testing code for your implementations. - -**Note 1:** The tests will simply compare against your CPU implementation -Do it first! - -**Note 2:** The tests default to an array of size 256. -Test with something larger (10,000?), too! - - -## Part 1: CPU Scan & Stream Compaction - -This stream compaction method will remove `0`s from an array of `int`s. - -Do this first, and double check the output! It will be used as the expected -value for the other tests. - -In `stream_compaction/cpu.cu`, implement: - -* `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. Map the input array to an array of 0s and 1s, scan it, and use - scatter to produce the output. You will need a **CPU** scatter implementation - for this (see slides or GPU Gems chapter for an explanation). - -These implementations should only be a few lines long. - - -## Part 2: Naive GPU Scan Algorithm - -In `stream_compaction/naive.cu`, implement `StreamCompaction::Naive::scan` - -This uses the "Naive" algorithm from GPU Gems 3, Section 39.2.1. We haven't yet -taught shared memory, and you **shouldn't use it yet**. Example 39-1 uses -shared memory, but is limited to operating on very small arrays! Instead, write -this using global memory only. As a result of this, you will have to do -`ilog2ceil(n)` separate kernel invocations. - -Beware of errors in Example 39-1 in the book; both the pseudocode and the CUDA -code in the online version of Chapter 39 are known to have a few small errors -(in superscripting, missing braces, bad indentation, etc.) - -Since the parallel scan algorithm operates on a binary tree structure, it works -best with arrays with power-of-two length. Make sure your implementation works -on non-power-of-two sized arrays (see `ilog2ceil`). This requires extra memory -- your intermediate array sizes will need to be rounded to the next power of -two. - - -## Part 3: Work-Efficient GPU Scan & Stream Compaction - -### 3.1. Scan - -In `stream_compaction/efficient.cu`, implement -`StreamCompaction::Efficient::scan` - -All of the text in Part 2 applies. - -* This uses the "Work-Efficient" algorithm from GPU Gems 3, Section 39.2.2. -* Beware of errors in Example 39-2. -* Test non-power-of-two sized arrays. - -### 3.2. Stream Compaction - -This stream compaction method will remove `0`s from an array of `int`s. - -In `stream_compaction/efficient.cu`, implement -`StreamCompaction::Efficient::compact` - -For compaction, you will also need to implement the scatter algorithm presented -in the slides and the GPU Gems chapter. - -In `stream_compaction/common.cu`, implement these for use in `compact`: - -* `StreamCompaction::Common::kernMapToBoolean` -* `StreamCompaction::Common::kernScatter` - - -## Part 4: Using Thrust's Implementation - -In `stream_compaction/thrust.cu`, implement: - -* `StreamCompaction::Thrust::scan` - -This should be a very short function which wraps a call to the Thrust library -function `thrust::exclusive_scan(first, last, result)`. - -To measure timing, be sure to exclude memory operations by passing -`exclusive_scan` a `thrust::device_vector` (which is already allocated on the -GPU). You can create a `thrust::device_vector` by creating a -`thrust::host_vector` from the given pointer, then casting it. - - -## Part 5: Radix Sort (Extra Credit) (+10) - -Add an additional module to the `stream_compaction` subproject. Implement radix -sort using one of your scan implementations. Add tests to check its correctness. - - -## Write-up - -1. Update all of the TODOs at the top of this README. -2. Add a description of this project including a list of its features. -3. Add your performance analysis (see below). - -All extra credit features must be documented in your README, explaining its -value (with performance comparison, if applicable!) and showing an example how -it works. For radix sort, show how it is called and an example of its output. - -Always profile with Release mode builds and run without debugging. - -### 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). - * You should use CUDA events for timing. Be sure **not** to include any - explicit memory operations in your performance measurements, for - comparability. - * To guess at what might be happening inside the Thrust implementation, take - a look at the Nsight timeline for its execution. - -* 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. - -## Submit - -If you have modified any of the `CMakeLists.txt` files at all (aside from the -list of `SOURCE_FILES`), you must test that your project can build in Moore -100B/C. Beware of any build issues discussed on the Google Group. - -1. Open a GitHub pull request so that we can see that you have finished. - The title should be "Submission: YOUR NAME". -2. Send an email to the TA (gmail: kainino1+cis565@) with: - * **Subject**: in the form of `[CIS565] Project 2: PENNKEY` - * Direct link to your pull request on GitHub - * In the form of a grade (0-100+) with comments, evaluate your own - performance on the project. - * Feedback on the project itself, if any. +* Kangning Li +* Tested on: Windows 10, i7-4790 @ 3.6GHz 16GB, GTX 970 4096MB (Personal) + +This repository contains HW2 for CIS 565 2015, GPU implementations of scan and compact. + +## Analysis + +![](images/graph.png) + +|array size | cpu scan | naive scan | efficient scan | thrust scan| cpu compact w/out scan| cpu compact w/scan | efficient compact +|-----------|----------|------------|----------------|------------|-----------------------|--------------------|------------------- +|32768 | 0 | 87 | 265 | 169 | 0 | 1002 | 308 +|16384 | 0 | 66 | 228 | 50 | 0 | 1002 | 233 +|8192 | 0 | 59 | 196 | 34 | 0 | 500 | 188 +|4096 | 0 | 51 | 182 | 25 | 0 | 499 | 173 +|2048 | 0 | 55 | 154 | 21 | 0 | 501 | 180 +|1024 | 0 | 42 | 137 | 18 | 0 | 500 | 181 +|512 | 0 | 36 | 143 | 19 | 0 | 0 | 153 +|256 | 0 | 32 | 133 | 18 | 0 | 0 | 123 + +The data tells us some obvious things, such as the fact that generally computation is faster when there is less data. However, the speed difference between teh CPU and GPU implementations indicate something suboptimal in the GPU code. The GPU code was timed without taking into account memory operations, so the difficulty may be in a lack of optimized register use or excess memory access besides explicit operations like copies, allocations, and frees. What is also interesting is that thrust scan, though faster than my implementation, is still slower than the CPU implementation. +Further analysis is required. It is also possible that the CPU implementation has not been timed correctly, or that the more expected benefits of GPU parallelization only become apparent with larger amounts of data than were measured. + +Another note is that this project implements efficient scan by modifying an array on the device in-place in both the upsweep and downsweep stages. +There were some concerns over race conditions when multiple blocks are needed, however, these did not arise. The project's commit history includes a version of efficient scan that uses an input and output array for the kernel but requires a memcpy to synchronize data in the two from the host in between passes. + +## Example Output + +``` +**************** +** SCAN TESTS ** +**************** + [ 38 19 38 37 5 47 15 35 0 12 3 0 42 ... 7 0 ] +==== cpu scan, power-of-two ==== + [ 0 38 57 95 132 137 184 199 234 234 246 249 249 ... 803684 803691 ] +==== cpu scan, non-power-of-two ==== + [ 0 38 57 95 132 137 184 199 234 234 246 249 249 ... 803630 803660 ] + passed +==== small cpu scan test. ==== + passed +==== naive scan, power-of-two ==== + passed +==== naive scan, non-power-of-two ==== + passed +==== small naive scan test. ==== + passed +==== small naive scan test, non-power-of-two. ==== + passed +==== small work efficient scan test. ==== + passed +==== small work efficient scan test, non-power-of-two. ==== + passed +==== work-efficient scan, power-of-two ==== + passed +==== work-efficient scan, non-power-of-two ==== + passed +==== small thrust scan. ==== + passed +==== thrust scan, power-of-two ==== + passed +==== thrust scan, non-power-of-two ==== + passed + +***************************** +** STREAM COMPACTION TESTS ** +***************************** + [ 2 3 2 1 3 1 1 1 2 0 1 0 2 ... 3 0 ] +==== small cpu compact without scan, power-of-two ==== + passed +==== small cpu compact without scan, non-power-of-two ==== + passed +==== cpu compact without scan, power-of-two ==== + [ 2 3 2 1 3 1 1 1 2 1 2 1 1 ... 2 3 ] + passed +==== cpu compact without scan, non-power-of-two ==== + [ 2 3 2 1 3 1 1 1 2 1 2 1 1 ... 2 2 ] + passed +==== small cpu compact with scan, power-of-two ==== + passed +==== small cpu compact with scan, non-power-of-two ==== + passed +==== cpu compact with scan ==== + [ 2 3 2 1 3 1 1 1 2 1 2 1 1 ... 2 3 ] + passed +==== small work-efficient compact with scan, power-of-two ==== + passed +==== small work-efficient compact with scan, non-power-of-two ==== + passed +==== work-efficient compact, power-of-two ==== + [ 2 3 2 1 3 1 1 1 2 1 2 1 1 ... 2 3 ] + passed +==== work-efficient compact, non-power-of-two ==== + [ 2 3 2 1 3 1 1 1 2 1 2 1 1 ... 2 2 ] + passed + +``` \ No newline at end of file diff --git a/data.ods b/data.ods new file mode 100644 index 0000000000000000000000000000000000000000..53773d45454d91adec5f3574b628dc397dd61dd1 GIT binary patch literal 11110 zcmd6Nby!@$d>`bjJX&fvKsjc+DreJC-YoMi}m4Uql(9(_?Y;6nFGX$Fefp)Tg!F+=G z!?2!80#=qrrpETRzoCKYX{@cR?X906>}mep59go!Y^{uKfncz{p6!3|#P|o#=gR)G z*FUurY^P^u|Nl^%os|{nS8Z?Jy!nmkKPY)Bkp4p(^$ZMvAm9^8R<<+-_O`aq6gydf zAOW%xub)Q-0P+0xWdBYY{?osIC)?CQ&lm`%;WxFj(6a{rU-D)IveL5y{wKEoiMQwe zTkBZ@LH|?RKQRFF)KsvYGYANN?&N}+hSeN1s>f2<+n5X!@9$9u4n+&o9p!o^-nC>d z&eMd1a`bZIsG@!GKW}lPK9m$?k?aLCK}QAN53clr*D~DP!Tkkwy_#>8LH9QwPq{R| zeta)fd&1*>Pw8~H1oCV&G8NZvmR8l=sE_cDAPx3=-RY;A{Qa}eiT*$YfxcEcPDoAY zRPk3oG*GHxoA_dp#!`R3hPsH^U248%iNjS8ZT!86W4H8qt_&T z<}xZJa|8P5P5U-Q4~6gVINczlZ>N%{w(3`J#%oY(frSatwx`VSjpRcZV8vK3F$XC} zBq14rvw2>8>Q2NI(SGdX>kpj5j1K6=HxVNM7Q`1w%Pla@A)97nSxH~~aNP@esO; z;EIhz4g`5+v>-7K130&jbfji5a#XdJrY6F0z$;m`FYn>uOFG4AYi2fU8T0)s2;RLQ zC-|n?U@2NhuM?PqW82+(m5~HHUI1ZVrJZM!Kz(1r#isBo&nQ?Qhln*--_?R54g0dc z>O;ec4hq>9m$Sv!io6m0YL&w&!(%<3zTza>cGgD2SL%gm+~2HUH2JS1z5XBwD~F6J z;5Kq+2Ww>_67mf#Qi4)Q{i1!naDCSt9qPZ$cj8csp1eXJCY ze9+E84MfA{N#wjf=*q94k&w{}4>W(O_f_#J(qDEn4n(et&m?7jD3X0uRX+?#7lZ%Z zdm#_eLq!OngpWoQZ|?c#pkh^H{7SO5 zg_%liA+hFOxA>I#l!Ktmqfab@((sin^!Gqi+1Yb`NcPmQ`c)ii@&YA#>}yp@Hg!&i0+;mg-H61IXqQT(V~i_6u_q`|Y-?dw8S z(=(w}h2xH7Ek*t{v;K)@G@c)u?Th7K4O}WE2_NT77TQznGh0*c_2N%7_pq~3XxXAd z)5QmHWukf9ugj6>fHbEwDzj@Ue3VtStz#qiIksh@&3-QU9fYkK!U{g7@5Sz6Ik!O$ ziHFC!a2&z>bZWrjx;he8rAJ^B?|rXMD%*v-*6JMI4V-d1{AP2+^r=>ZCOd6YwXKz4 zST1UMcTCqrk1ha~wHBj;OiY;IbyU79ULg@S-*`~zyNSoUu&x);px06q>w2d(J!-WBETG1xGzE06oytO zINu~Dx`X?rq=$0ji9xX?;@U$%2=Xb^WLl9n@S`o>X8&yFnQP*Bqd~A38$+2Y1S=)# zgVH!Dro(LetsImiLZ{8}A)>^@s;r%UEXeU5$bI#i*Ij;B>II14n;L{|%!(RY;nZJ~8`9Uvg+>0(MFw}*T?oMp#U!wYRY7Ms(iF%sy1|grFawc&lYrIGG zRHl-MoW}A%6WP78-xR$$DhK%$Cb~hwL6g{$0lnVT2VK33-+n} zN$G%reSW#QxyindFi8mPt-*awVLaUx{l>&gre&>nx~Jr)^g)h?Y_(K;W9r-mmH`)x zveJ+3cjUfY9E-r&s|$`%*f_28kLE)wlDK5S!zx&Ft|03u(OKVwEmAIlYGpI{9l`Bl z1Pge)1M74v+jL$XdIFxzn<@fzsI#25&zZ81f^q}=FY4Ny;%LjmGi!d@WCwKS4IDLf z$Gl_qzLl=mDoNsHxcsb7gEM$(eI5P%faiIuye!NN6@3K&%shn<|GicI)BXeZwEtM> zS(+LF!FJTPhDHO?1C~CtZ+(wA{eXkhyfjcywq$(`W?$Nw{8gc7PLq%qTf?H2<+2NR zzl^Vy3NtK)RVN?l;d~3J%>Prnq*)*z#?nJhA{{v`;T%mJ=|~ z*9xAgqAs$yMkNbchkwW+Gy;NVFP57%=hpRgASleBleu~)#Q)?4as1j6TuO#f>LF@Y z!4~Z(ANGgH9wQTs3{GMt@iSb5OtP1Gm^7@3ZCFb?st#;kFQ#7YC51PFkDY5fI0@Cx z-5<4j*+PHbXmXRVTx>;G@$kLU9Id1P)@()Pe^}b;COJ|UFPODs%kzy!z|adnM_w*d zE6J{pPFDF8i%D6?bQMgc$G7uVN2~A^3x4)ZWmb1yif7CIqt1hU@;#O7vD68od*r*z zTQ~PWjq7wE{a(g_i)aq6WT46?bUFT|vRQvE!8xfcHQ*;}g*BDBfTIWU0sQCDMGVjt z%6}SN<3C3C_ZtDTA*<3I1OVXqcn+ZzP3$f7E%i)6U>dvMi`3SZ#zC^uBJeO+&+!|) zn5dxq(`U=m@%ZKm5fGh73IqV2LRLvdApqbN4)jYzVir~gY9=No3Pu5TAt`oI4M9!@ z33eeR9$pE4b^~c1K4B3daVaq&Aqf!)83_RqNku7H8F6_H6-hoNX$dtc0aY0>MR7SL zIY~`fVNC^T1qDR~MGYl&O${Xl4P`Y=HDyh8O%X+)oU*=>hJk{biKdpmww9@!rnRyj zNJHOP!@ykq=|WWy)>oF(RZ%n4R5H+1H_=cu(^j)q7t+_(1L|sl^pwpEH0+Hv4D}84 z4b2QqLFPbxb3+pg6MZu?GedJbeH&+xwLJ*zVsCEf0yed?1B30I?OoiQ9l)+mc5ZHN zI_BQmw!VgzUZ(c`R_>8D_CEG*J~r+lZXRAXp3xp&9#&qVRzC6eUZL*ZKJGrD4u0{j zVObtg1>Tn00bm^;D+7NA6CYQwx4T=A2RO>jJm`~ijJIXXC%Y&=mqb6?q(J-B5SPr) z&UsO;-rioH{6qYL!h-xig$DYDhJ^Zs#)e0J4v9*N3-wJ1k4}mXNr;d44Nv!p%JhjV z@l7ZUiO&v8%nL~>@=vM=POXYg&I-${OUf!t$gj-^^U06*&q|5ONlh+Fjjl`yDacH$ z&HP-K7oVGxmsMPwS=v-uP@Y%OSXq==RZ`ecl37=tSzTVzT#?gKU0h#Z8(-3yTG^db z+ne7qQeN9$(fGBprN6nOwX|cfxMRG&qpzlCvaWx*d3d#>DDG=TN>_DGS8Y*eeQtL{ zQGZQle?xX(b8&A=^=M0OM|)#`d+os2#_^7#$==%K!G^WbmT%v`e;t_Y8lD>*9{({u zH!<*acxZTRbZ~y8Yi_J>WMXV$YG!fq|> z^y<#s&c*uFxApn4)1|({<)OW`sneB#la2A?joGuE`P=>ZhvUVy^|jUYo%Nl=-OZJQ z-Sz$b{nh=morB}U5HsPN{WP*&&)53MMf>yr_9VNftR6N_I%k-j=Ti)(J8SU#<+ z#_O$Uw!JDtkIugw8SiLx*;C7)F=shAN6z7}*_^m7xw+mt)a*Lcu(iPk?J?M!jqm(u z)hXkG1>J>PR;qfW=jnR&^XL`~;K_O9^q{?2?qbP$c-QJ4fa0OqW;O#FdPw>{d-`>z z1gY~*1CaJ3(55$)PD*1X##EdsmXadP4YREJDc8ez^^>JU)P866#g(0> z)wO^`l{}SLm8*eci5HKr84KjJ;_R+OL)#;;CV$f+ zdck6way&AACFwDplm$4mxr$zFlHnl7YKRefw!R5*k#e4rRr1VS#oo*x0Jo>XM@Rpb zMD@Ih-)bXgWXm0pCAtyb(%Y7{7X z_>|Z$?H(a1I}E#pL+qL7d&9MtT9t6cHV;*2adFxwIlHk zPP7v?v{l}{(C`#R9Jz9@aj5HzR$#4aUu<7&RL;?uBTGwLGA~eQx@#*!teGn#(}Yfv zAzabCQ%IaU$eUvF{+MozFA(&(gt$yh zHjUOIk|}7n`gKv(BwHvn_-278d0q^sm&jw#3Q}$udf6!LZ&8eATfN9R!Hd zI^*XXRY$=6Nnq%TJ1qSH`}KI`qxf*5LFy=^b`Ii4DJlgiF=K%V%H!LLm-WR0Jw_w} zF6o&_XnUcY%)JTKXT)n`eCO0ABW$a#ylyz$qaCy5!fo>blV=q5zT=33e8fet`O}wz zER+}K2oObOk6NCddxPhgT0Y#l%UkZ~$)~e7iO3F?Nzp|J1T+fMep`r|+QHBl{`PP# z_`6PwWsOwyfoY!LrYR8fm6OH{6hmES>1q+OGL_CmAiRA7wwDMR66skz?xyrf>Zo4d zRrVK*hg}8hxH-UNwSGQ*TqWErncjNU40gHsd%UZH{brtn*pD0^hla?cP_6 z6`u~N2(>Jq{=>^VaiZ;w)-H|VjNXNPK|DcQDuACm3TKpSJ3e}~`h@#QdI9h90Ib!w z0kD-xxcG3D*+h8%)nigp-Tmo!GlU`?W_&mR-yo!43rW&5|1iY5e7uXvUTQa8k5A1{ zo4+$uaB7xD0LomiUbjHttG3QK^x!qMM2P^}c~coWp)XVSPDB;oYc{AkTLmtspP4Hq zHQ!_tt+ZPD>(J%quUT2lkLDxB*1sSLK3>JFw^)E;_rDp@gdNb_B@y+z2JDng4|B(% z4^~FM1O24fKs&e@SldJbegI4~YTo4rk zG>rV`Z{5gOnzm4eTVfx>#M-X!!@VdGgCnnW{8Gp_#acQp@OL`Yf1GD7;Pn>Ch$XOk zpjGQOogz4uttSzvAEdH!yzX*xrS>CEv_9bD3U@9>JWJL)dbuU8A6HMIX$+wEqT_-J zya-*x4LMnfs$g>H5!iF349fE=so8h3f~5>%eGm=7OFO9DMLJsq)(x?iHDWcD6>Axf zA@&g&y4g=PE`0kD!`NIIEX-!S7bvp`}~shV2{>2pZnn zKBgpEv0igzr@?Qxg}8wnAMW*6X8g%$?KpC+3W_(no~8#Ly4$rZL90G1%y^VEWT=5r-ZD8eI2=8W{fbG2kbQK%l@- zN`=0vhB6#Yrbo6y==nrQB6fe0;`@9Lg2kB0iT;X~oVA4!&QiY4=A*U#X-c$f=d4d% z^S!8Nt79(AuPQ|)P&QmgWl}sMq-qiH>)9ck4ORPw5uitW_KCW_iDDcbYaMOZdkw4j z!Ufsn^dTy3NbsdlZ>ZEMC9Ab|QqMIJG}@u(Hn*+RA{cf5L zH0rw9u|^=f$=ilW@S|`dP*0H=*-hoc-hIMH(jo(aU~7~t;HF{ z2Qg6zm}peu)PR*LCJbF?k~1yZcOVNU?k>Bmi{)ZQF+Co5kgN~V%5&+2O}etzENtVB z&fibHb%f2_-?stW4YyXkX(}XpFX`8Eke6%r;i}$?t$vyxY?5ao#|4Ahi29 zBrtC;d>W{E)WGOj-4mUa+Xvt z8hqW#P!R|<=<@Jt<1iwol<4#+Aq50k2#S>2=;utf>R*wG$5r-%WQ3<`VJvTWed(|n zj?U1tFHmigQ|F^zrMt_A3p19Y+sA{BiPa7xU-e_~oT3fOC>gdvxewMzV{##reheI*a4}PQ*P)jwp z`~BzsVn;-t`4HC4_Zos9KFY_LA_q%$wGl=Pj%Utlsr&V#f1)Tw$COG`-?n~l8^X;`HO`E3Q~$7Yz1C)*4DY*y3YtVj}eA3&y-8 z{EBH(&b#JnX7w%Dxy1vAwNxH-TZ^W%n|W$SjZVFgAFzor`DE2usJKa+bpTkNIZbOs1;A6L0DLWoyPYB)e zujN#jb6miiZpN2{pg8>SBSafwg<>k7g9eHiP7xj>zr#M*_v(eh>Ef4BrCY2hO}C4D zN7S8;QRXBZ$HyNyv&E)eG_&(kUa?NSda0*@a%zE$DU#0sk)~x)r;C=V^f0w{hq&UO za}L-1YoXzH2C4)5`wZXqTNk`Xu-D&DfRrgss`YfkwpLw_@G&5Q@eQi!w zm>}qTT-8E&%d86<-lhZe?&MeqM5zI`WO9`Ce84Krl!a%oP^6F&W`0V?C0q17Xe1j| z6^G=j>;tiiXR0n{M=2S?eaf!vSvA2N1n>zDabxm$G zK>X%*e8%H`jM?>TwUh~5P0nEbvKrz5NOl4OSO-hj|Ar{{Js}b-%7wMg#I=L7qB~1- z{G=-ae3sqOHsk=oSo&L*;TTdGi9OOJ*o!lyQuS(Ku8CJNlOG{PcpGFnN)-_!!MOZ< z3-aH8Y**1J+(GGnhvpx+g&LC)uR15L#djw2L$1Jkkn%LXFV+aN#;9vfn0sA|4m#fn zugb=1FDkzgW$8p+psBCMrm3;~60+x#ijDgMGYV9BxiFKdCxUS?ojWOeB<~ z_Sw)eay_C=(rj>?3_T>)@SH)7P829>BFTbfejO%aWR@V^6PCOep_B)rQ`nMvIWvEtSThNbVr=RX68H;VgX!cFz>H1t&vVqC%5oy=-Wp=2;9$Hf0I0 zPiH zs&Yo(ka}jRgD{U%p*rPFgb$>vt^IPUS6q;eIaQe`lOj^W7RCfzuCJ+2AO%Zw1TplE znd!Y%R*4Jb_IaBUsqZedABlSy$fwm|JzcCL(4+4~LACE$weY~cgFYf^V(PuOxY!rr z;K#1R1DBmeZsya60e@#Y`S%0b6DGnpaV@QCMqeA8G0%5%FND!|;rEl)sC1BhU8<{u zPBu{&kB7~X-<4d&5j?b6aRs9A)lAH(skb_^q^5er!7Oo$2f0<8Nk>`BzD&^z9Xgbq zG0%TBBJB&5Xrag7whmn zLw3QH+^GnPT%4Nw080#r7lU^lyoZl%QwT#>Hd{w`Q_37hhd5hYkj)+2(AE|#82NjR zV9gDPd0q>|Db{<9eJpPus$_xKQp{2-z~Ss}&sRR-(ie;R`}Ot1HUYsnhC|8l6Q>bZ zIo_obRxX#(Y^A0E%k;6ft^sVXrrO38Hk9>ZxOVK&R6smShin-p#JUh2M*fOxed7+i zBFmD~A}=$%ae1k?t4F(*#)362B4z`ixnnwU`S!su-QnoA{;J9go5C(K($&Y<6Y2_8 za|L3)X5w>uH!;GpC9SR7JnjJgw*RJd1bBD-H0Ac-WyE8_X+K+wI_q8Xo1t4P!2oQ6 zaMJo#b3v~VQ2T8t7q!n{@N4bPZ`lcREv^Lz=8RoKLY)$B#qyiqOD# z71h4XIY*rt{n21oxsrhfeMk_g$e!|&0C$5817awi?XD_yT-9MCP0oX*dQ#jk%%a!E z4f#u2y6VhAhpuj3){P?KO;qV_QlHULGXK{l|0t+LhJp(1Jg|}RW69*;_m<)FVmZ8k z`%9564#*jqWQsRY)71{~C{zA%%sU{muWeaIu_dncn{ncey0qxLn;7ah>9hVW5w^h3 z>|2)x->TZb=oa)xw8`m|{1|Q--0N62xkueL)7rgN%kxxzw}c=%ojV`kdK{8VKh~s1j1R!dNo3pl;e(mQSW;#KV5! z1c9mXR!*&R3C>Jq`dyHc*hf0+3>kXIDTt^n-q`s#-|sLj6Le>gFLQtX*xGAAc!Z@; z*zRIpQCRn7=kw3>GD-ktSeALt?zQZ)9BaZ00>!f(xL~f~*2v%LIFbm5W(NrxV?mx% z-Z05GCPO-6e~b`&7ff7-L|Cw%*O*?Foxit#okki|B`+hgUrD)Ri#%u}oc$@YFo`z8 z`+^I25Kafn5TULaYy4n&bm6^I8c4%+j345dfg0&FGbL4~;*qbNez?8t=#4`5z_pPP z%Fgz&ASro-_1i_+aAe7lK~-#%V12;hYU{#J~pH|_WbWjZ&ppGU;4;;Ng9qg zNgPGN&l`QwYCVM(Fx6i(cD|C=r41lp%!co=W0TB$WT7VUb!~@3MI!nsY3#HPGS7b7 zI_EA2zNlgu`c-)s&^sudvs&#<`K8Jmt{~xKO9($g0Bd>7()BHz! zn*UNlihNXJ(!w-<Ed@q9qD^ss$W72 z&)=sm2tQpU7`E9XaJ)$+2uS7d%!ppaZBu)MV>3<2<*0i?_A7fS0gdN*C-MuxqdgO+ zsW&STN_PLvM{DhLBi)vjtk_Q#BA|0pddD#i=)zy%?8JnWcu+t6Pd4%j#u@&f? zs4PT4usDhnn=f|X&z_6r`WTxif~x3CEImB7lWp(A{!VLMrX1srG=2J-1<13Q!n_-t z_RS?fD@W&R!8J~D?nHWhPoJ;wxv!Zvj4NXNP97-FIWAuIZ&dI!4#nP{pcS|MCXY%; zB*g#ll-3e@`AqT)2voqoE8L!#{*(Q#cl*27pH2ZhYn*FIu_|wOfZx6TEt&j>69D?D`hQC;|Bmu|tN$E2J>PTxGBWJH+=2hy y@y~1SU%Tu2{;zS?-#!1lkUX!+zs!~RUsh;Y38<&=4gf%UdRafMKg%S~-~J0+nxwn{ literal 0 HcmV?d00001 diff --git a/images/graph.png b/images/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..b92563e9a204bff974e7419fab6623560d503dcf GIT binary patch literal 31606 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 AO#lD@ literal 0 HcmV?d00001 diff --git a/src/main.cpp b/src/main.cpp index 0017589..c7bb617 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,7 +26,8 @@ int main(int argc, char* argv[]) { int smallScan[8] = { 0, 0, 1, 3, 6, 10, 15, 21 }; int smallCompact[7] = { 1, 2, 3, 4, 5, 6, 7 }; - // set "false" for standard tests + // set "true" for timed tests + // also set BENCHMARK in common to 1 if (true) { genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; From c83240ad2c12716fdf49ead582e604b2cfd8088a Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 23:22:27 -0400 Subject: [PATCH 17/22] submission --- src/main.cpp | 2 +- stream_compaction/common.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c7bb617..adbb461 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) { // set "true" for timed tests // also set BENCHMARK in common to 1 - if (true) { + if (false) { genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; printf("array size: %i\n", SIZE); diff --git a/stream_compaction/common.h b/stream_compaction/common.h index 9ee943b..cc34b96 100644 --- a/stream_compaction/common.h +++ b/stream_compaction/common.h @@ -6,7 +6,7 @@ #define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) #define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) -#define BENCHMARK 1 +#define BENCHMARK 0 /** * Check for CUDA errors; print and exit if there was a problem. From 939058dcae4ddc7a92e1d9efd49cf3f3272b0f6f Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 23:24:24 -0400 Subject: [PATCH 18/22] updated readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4140c5d..4b43bb5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ Further analysis is required. It is also possible that the CPU implementation ha Another note is that this project implements efficient scan by modifying an array on the device in-place in both the upsweep and downsweep stages. There were some concerns over race conditions when multiple blocks are needed, however, these did not arise. The project's commit history includes a version of efficient scan that uses an input and output array for the kernel but requires a memcpy to synchronize data in the two from the host in between passes. +## Notes +I added an additional "small" case test for debugging use. +Efficient Scan also has disabled desting code for "peeking" at the results of up-sweep before down-sweep. + ## Example Output ``` From f7388b35b79e1f03ccd7fe5dd186beef06be8788 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 23:31:15 -0400 Subject: [PATCH 19/22] teh --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b43bb5..3f6659e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This repository contains HW2 for CIS 565 2015, GPU implementations of scan and c |512 | 0 | 36 | 143 | 19 | 0 | 0 | 153 |256 | 0 | 32 | 133 | 18 | 0 | 0 | 123 -The data tells us some obvious things, such as the fact that generally computation is faster when there is less data. However, the speed difference between teh CPU and GPU implementations indicate something suboptimal in the GPU code. The GPU code was timed without taking into account memory operations, so the difficulty may be in a lack of optimized register use or excess memory access besides explicit operations like copies, allocations, and frees. What is also interesting is that thrust scan, though faster than my implementation, is still slower than the CPU implementation. +The data tells us some obvious things, such as the fact that generally computation is faster when there is less data. However, the speed difference between the CPU and GPU implementations indicate something suboptimal in the GPU code. The GPU code was timed without taking into account memory operations, so the difficulty may be in a lack of optimized register use or excess memory access besides explicit operations like copies, allocations, and frees. What is also interesting is that thrust scan, though faster than my implementation, is still slower than the CPU implementation. Further analysis is required. It is also possible that the CPU implementation has not been timed correctly, or that the more expected benefits of GPU parallelization only become apparent with larger amounts of data than were measured. Another note is that this project implements efficient scan by modifying an array on the device in-place in both the upsweep and downsweep stages. From 5489a9a320e381d8f208551c35cce119d3888f80 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Sun, 13 Sep 2015 23:33:40 -0400 Subject: [PATCH 20/22] slight graph tweaks --- data.ods | Bin 11110 -> 20038 bytes images/graph.png | Bin 31606 -> 34982 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/data.ods b/data.ods index 53773d45454d91adec5f3574b628dc397dd61dd1..54d1333858fed0f20337b176e7d4a9f665553a82 100644 GIT binary patch delta 14703 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 z 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 LK|pFHU$6c@YztbC delta 5795 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 LbEie7`(OVDx|A@| diff --git a/images/graph.png b/images/graph.png index b92563e9a204bff974e7419fab6623560d503dcf..74a0067cb112c7781604af21e682464e52ddb391 100644 GIT binary patch literal 34982 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 F{{dm{V?O`@ literal 31606 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 AO#lD@ From e1478d30f1d3c0b1d0a7e03aab6d90acd2b29e72 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Mon, 14 Sep 2015 00:03:19 -0400 Subject: [PATCH 21/22] looks like things get MUCH faster when I do less power math and casting in the kernel --- src/main.cpp | 4 ++-- stream_compaction/common.h | 2 +- stream_compaction/efficient.cu | 24 ++++++++++++++---------- stream_compaction/naive.cu | 6 +++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index adbb461..565a985 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "testing_helpers.hpp" int main(int argc, char* argv[]) { - const int SIZE = 1 << 15; + const int SIZE = 1 << 8; const int NPOT = SIZE - 3; int a[SIZE], b[SIZE], c[SIZE]; @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) { // set "true" for timed tests // also set BENCHMARK in common to 1 - if (false) { + if (true) { genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; printf("array size: %i\n", SIZE); diff --git a/stream_compaction/common.h b/stream_compaction/common.h index cc34b96..9ee943b 100644 --- a/stream_compaction/common.h +++ b/stream_compaction/common.h @@ -6,7 +6,7 @@ #define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) #define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) -#define BENCHMARK 0 +#define BENCHMARK 1 /** * Check for CUDA errors; print and exit if there was a problem. diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index f08479a..1813244 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -30,22 +30,22 @@ static float teardown_timer_events() { // TODO: __global__ -__global__ void upsweep_step(int d, int *x) { +__global__ void upsweep_step(int d_offset_plus, int d_offset, int *x) { int k = threadIdx.x + (blockIdx.x * blockDim.x); - if (k % (int) powf(2, d + 1)) { + if (k % d_offset_plus) { return; } - x[k + (int)powf(2, d + 1) - 1] += x[k + (int)powf(2, d) - 1]; + x[k + d_offset_plus - 1] += x[k + d_offset - 1]; } -__global__ void downsweep_step(int d, int *x) { +__global__ void downsweep_step(int d_offset_plus, int d_offset, int *x) { int k = threadIdx.x + (blockIdx.x * blockDim.x); - if (k % (int)powf(2, d + 1)) { + if (k % d_offset_plus) { return; } - int t = x[k + (int)powf(2, d) - 1]; - x[k + (int)powf(2, d) - 1] = x[k + (int)powf(2, d + 1) - 1]; - x[k + (int)powf(2, d + 1) - 1] += t; + int t = x[k + d_offset - 1]; + x[k + d_offset - 1] = x[k + d_offset_plus - 1]; + x[k + d_offset_plus - 1] += t; } __global__ void fill_by_value(int val, int *x) { @@ -116,7 +116,9 @@ void up_sweep_down_sweep(int n, int *dev_data1) { // Up Sweep for (int d = 0; d < logn; d++) { - upsweep_step << > >(d, dev_data1); + int d_offset_plus = (int)pow(2, d + 1); + int d_offset = (int)pow(2, d); + upsweep_step << > >(d_offset_plus, d_offset, dev_data1); } //debug: peek at the array after upsweep @@ -128,7 +130,9 @@ void up_sweep_down_sweep(int n, int *dev_data1) { zero[0] = 0; cudaMemcpy(&dev_data1[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); for (int d = logn - 1; d >= 0; d--) { - downsweep_step << > >(d, dev_data1); + int d_offset_plus = (int)pow(2, d + 1); + int d_offset = (int)pow(2, d); + downsweep_step << > >(d_offset_plus, d_offset, dev_data1); } } diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 4671b0a..430d658 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -30,9 +30,8 @@ static float teardown_timer_events() { // TODO: __global__ -__global__ void naive_scan_step(int d, int *x_1, int *x_2) { +__global__ void naive_scan_step(int offset, int *x_1, int *x_2) { int i = threadIdx.x + (blockIdx.x * blockDim.x); - int offset = powf(2, d - 1); if (i >= offset) { x_2[i] = x_1[i - offset] + x_1[i]; } @@ -78,7 +77,8 @@ void scan(int n, int *odata, const int *idata) { // this can be an "unbalanced" binary tree of ops. int logn = ilog2ceil(n); for (int d = 1; d <= logn; d++) { - naive_scan_step <<>>(d, dev_x, dev_x_next); + int offset = powf(2, d - 1); + naive_scan_step <<>>(offset, dev_x, dev_x_next); int *temp = dev_x_next; dev_x_next = dev_x; dev_x = temp; From 8497e557e21eb3dc318124dd2fff4d1dc8727e3a Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Mon, 28 Sep 2015 02:37:53 -0400 Subject: [PATCH 22/22] using this as a testing platform for changes over in Project3's stream compaction. So far, tweaked efficient/memory inefficient. --- src/main.cpp | 2 +- stream_compaction/common.h | 2 +- stream_compaction/efficient.cu | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 565a985..85f4fbb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) { // set "true" for timed tests // also set BENCHMARK in common to 1 - if (true) { + if (false) { genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; printf("array size: %i\n", SIZE); diff --git a/stream_compaction/common.h b/stream_compaction/common.h index 9ee943b..cc34b96 100644 --- a/stream_compaction/common.h +++ b/stream_compaction/common.h @@ -6,7 +6,7 @@ #define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) #define checkCUDAError(msg) checkCUDAErrorFn(msg, FILENAME, __LINE__) -#define BENCHMARK 1 +#define BENCHMARK 0 /** * Check for CUDA errors; print and exit if there was a problem. diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 1813244..51e4acf 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -126,9 +126,10 @@ void up_sweep_down_sweep(int n, int *dev_data1) { //cudaMemcpy(&peek1, dev_data1, sizeof(int) * 8, cudaMemcpyDeviceToHost); // Down-Sweep - int zero[1]; - zero[0] = 0; - cudaMemcpy(&dev_data1[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + //int zero[1]; + //zero[0] = 0; + //cudaMemcpy(&dev_data1[n - 1], zero, sizeof(int) * 1, cudaMemcpyHostToDevice); + cudaMemset(&dev_data1[n - 1], 0, sizeof(int) * 1); for (int d = logn - 1; d >= 0; d--) { int d_offset_plus = (int)pow(2, d + 1); int d_offset = (int)pow(2, d);