From a09073f8e7b54bff37254d98e3e84459bb7a812f Mon Sep 17 00:00:00 2001 From: PacosLelouch <276613158@qq.com> Date: Sat, 18 Sep 2021 02:17:12 -0400 Subject: [PATCH 1/5] Complete scan and stream compaction. --- src/main.cpp | 100 ++++++- stream_compaction/common.cu | 13 +- stream_compaction/cpu.cu | 41 ++- stream_compaction/efficient.cu | 503 ++++++++++++++++++++++++++++++++- stream_compaction/efficient.h | 9 + stream_compaction/naive.cu | 47 ++- stream_compaction/thrust.cu | 59 +++- stream_compaction/thrust.h | 4 + 8 files changed, 754 insertions(+), 22 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 896ac2b..ef32917 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ #include #include "testing_helpers.hpp" -const int SIZE = 1 << 8; // feel free to change the size of array +const int SIZE = 1 << 20;//1 << 24;//1 << 22;//1 << 20;//1 << 18;//1 << 16;//1 << 12;//1 << 3;//1 << 8; // feel free to change the size of array const int NPOT = SIZE - 3; // Non-Power-Of-Two int *a = new int[SIZE]; int *b = new int[SIZE]; @@ -23,9 +23,9 @@ int main(int argc, char* argv[]) { // Scan tests printf("\n"); - printf("****************\n"); - printf("** SCAN TESTS **\n"); - printf("****************\n"); + printf("***************************\n"); + printf("** SCAN TESTS %010d **\n", SIZE); + printf("***************************\n"); genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case a[SIZE - 1] = 0; @@ -71,7 +71,7 @@ int main(int argc, char* argv[]) { printDesc("work-efficient scan, power-of-two"); StreamCompaction::Efficient::scan(SIZE, c, a); printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); + //printArray(SIZE, c, true);// printCmpResult(SIZE, b, c); zeroArray(SIZE, c); @@ -85,7 +85,7 @@ int main(int argc, char* argv[]) { 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); @@ -95,6 +95,45 @@ int main(int argc, char* argv[]) { //printArray(NPOT, c, true); printCmpResult(NPOT, b, c); + ///////////////////////////////////////// + //if (SIZE <= (1 << 7)) { + // zeroArray(SIZE, c); + // printDesc("work-efficient-test scan, power-of-two"); + // StreamCompaction::EfficientTest::scan(SIZE, c, a); + // printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + // //printArray(SIZE, c, true);// + // printCmpResult(SIZE, b, c); + //} + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, power-of-two"); + //StreamCompaction::Efficient::scanBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(SIZE, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, non-power-of-two"); + //StreamCompaction::Efficient::scanBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-sh-mem-recursive scan, power-of-two"); + StreamCompaction::Efficient::scanBatch(SIZE, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-sh-mem-recursive scan, non-power-of-two"); + StreamCompaction::Efficient::scanBatch(NPOT, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(NPOT, b, c); + ///////////////////////////////////////// + printf("\n"); printf("*****************************\n"); printf("** STREAM COMPACTION TESTS **\n"); @@ -103,7 +142,7 @@ 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; + a[SIZE - 1] = 4;//0; printArray(SIZE, a, true); int count, expectedCount, expectedNPOT; @@ -147,7 +186,52 @@ int main(int argc, char* argv[]) { //printArray(count, c, true); printCmpLenResult(count, expectedNPOT, b, c); - system("pause"); // stop Win32 console from closing on exit + zeroArray(SIZE, c); + printDesc("thrust compact, power-of-two"); + count = StreamCompaction::Thrust::compact(SIZE, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + printDesc("thrust compact, non-power-of-two"); + count = StreamCompaction::Thrust::compact(NPOT, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + printCmpLenResult(count, expectedNPOT, b, c); + + ///////////////////////////////////////// + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, power-of-two"); + //StreamCompaction::Efficient::compactBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, non-power-of-two"); + //StreamCompaction::Efficient::compactBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-sh-mem-recursive compact, power-of-two"); + StreamCompaction::Efficient::compactBatch(SIZE, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-sh-mem-recursive compact, non-power-of-two"); + StreamCompaction::Efficient::compactBatch(NPOT, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpLenResult(count, expectedNPOT, b, c); + ///////////////////////////////////////// + + //system("pause"); // stop Win32 console from closing on exit delete[] a; delete[] b; delete[] c; diff --git a/stream_compaction/common.cu b/stream_compaction/common.cu index 2ed6d63..187697a 100644 --- a/stream_compaction/common.cu +++ b/stream_compaction/common.cu @@ -23,7 +23,12 @@ 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 + // DONE + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index < n) { + int nonZero = idata[index] != 0; + bools[index] = nonZero; + } } /** @@ -32,7 +37,11 @@ namespace StreamCompaction { */ __global__ void kernScatter(int n, int *odata, const int *idata, const int *bools, const int *indices) { - // TODO + // DONE + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index < n && bools[index] == 1) { + odata[indices[index]] = idata[index]; + } } } diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index 719fa11..d035dc4 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -19,7 +19,13 @@ namespace StreamCompaction { */ void scan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO + // DONE + // Exclusive scan includes 0 at first. + int sum = 0; + for (int i = 0; i < n; ++i) { + odata[i] = sum; + sum += idata[i]; + } timer().endCpuTimer(); } @@ -30,9 +36,15 @@ namespace StreamCompaction { */ int compactWithoutScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO + // DONE + int size = 0; + for (int i = 0; i < n; ++i) { + if (idata[i] != 0) { + odata[size++] = idata[i]; + } + } timer().endCpuTimer(); - return -1; + return size; } /** @@ -42,9 +54,28 @@ namespace StreamCompaction { */ int compactWithScan(int n, int *odata, const int *idata) { timer().startCpuTimer(); - // TODO + // DONE + + // Bit + for (int i = 0; i < n; ++i) { + odata[i] = idata[i] != 0 ? 1 : 0; + } + + // Scan + int size = 0; + for (int i = 0; i < n; ++i) { + int temp = odata[i]; + odata[i] = size; + size += temp; + } + + // Scatter. + for (int i = 0; i < n; ++i) { + odata[odata[i]] = idata[i]; + } + timer().endCpuTimer(); - return -1; + return size; } } } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 2db346e..34365d0 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -3,6 +3,8 @@ #include "common.h" #include "efficient.h" +//#define WRITE_EXC_WITH_INC 1 + namespace StreamCompaction { namespace Efficient { using StreamCompaction::Common::PerformanceTimer; @@ -12,13 +14,95 @@ namespace StreamCompaction { return timer; } + __global__ void kernUpSweepOnce(int n, int* odata, int stride) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + int base = index * stride; + int targetOffset = stride - 1; + int targetIdx = base + targetOffset; + if (targetIdx >= n) { + return; + } + int leftIdx = base + (targetOffset >> 1); + + odata[targetIdx] += odata[leftIdx]; + } + + __global__ void kernDownSweepOnce(int n, int* odata, int stride) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + int base = index * stride; + int targetOffset = stride - 1; + int targetIdx = base + targetOffset; + if (targetIdx >= n) { + return; + } + int leftIdx = base + (targetOffset >> 1); + + int prevLeftValue = odata[leftIdx]; + odata[leftIdx] = odata[targetIdx]; + odata[targetIdx] += prevLeftValue; + } + + void scanGpu(int newN, int logn, int* cuda_g_odata, int threadsPerBlock = 128) { + // 1 Up sweep. + int stride = 1; + int blockCount = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + int iToAddBlockBelow = logn; + for (int i = 0; i < logn; ++i) { + stride <<= 1; + if (blockCount == 1 && iToAddBlockBelow == logn) { + iToAddBlockBelow = i; + } + blockCount = std::max(1, blockCount >> 1); + //printf("UP blockCount:%d, stride:%d\n", blockCount, stride); + kernUpSweepOnce<<>>(newN, cuda_g_odata, stride); + } + + // 2 Down sweep. + cudaMemset(cuda_g_odata + newN - 1, 0, sizeof(int)); + + blockCount = 1; + for (int i = logn - 1; i >= 0; --i) { + //printf("DOWN blockCount:%d, stride:%d\n", blockCount, stride); + kernDownSweepOnce<<>>(newN, cuda_g_odata, stride); + stride = std::max(1, stride >> 1); + + if (i < iToAddBlockBelow) { + blockCount <<= 1; + } + } + } + /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + int threadsPerBlock = 128; + + int* cuda_g_odata = nullptr; + int logn = ilog2ceil(n); + size_t newN = 1i64 << logn; + size_t sizeNewN = sizeof(int) * newN; + cudaMalloc(&cuda_g_odata, sizeNewN); + cudaMemset(cuda_g_odata, 0, sizeNewN); + cudaMemcpy(cuda_g_odata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + timer().startGpuTimer(); - // TODO + // DONE + scanGpu(newN, logn, cuda_g_odata, threadsPerBlock); timer().endGpuTimer(); + + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_odata); + } + + void compactGpu(int newN, int logn, int* cuda_g_odata, int* cuda_g_bools, int* cuda_g_indices, const int* cuda_g_idata, int threadsPerBlock = 128) { + int blockCountSimplePara = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + Common::kernMapToBoolean<<>>(newN, cuda_g_bools, cuda_g_idata); + cudaMemcpy(cuda_g_indices, cuda_g_bools, sizeof(int) * newN, cudaMemcpyDeviceToDevice); + scanGpu(newN, logn, cuda_g_indices, threadsPerBlock); + Common::kernScatter<<>>(newN, cuda_g_odata, cuda_g_idata, cuda_g_bools, cuda_g_indices); } /** @@ -31,10 +115,423 @@ namespace StreamCompaction { * @returns The number of elements remaining after compaction. */ int compact(int n, int *odata, const int *idata) { + int threadsPerBlock = 128; + + int* cuda_g_odata = nullptr; + int* cuda_g_idata = nullptr; + int* cuda_g_bools = nullptr; + int* cuda_g_indices = nullptr; + int logn = ilog2ceil(n); + size_t newN = 1i64 << logn; + size_t sizeNewN = sizeof(int) * newN; + cudaMalloc(&cuda_g_odata, sizeNewN); + cudaMalloc(&cuda_g_idata, sizeNewN); + cudaMalloc(&cuda_g_bools, sizeNewN); + cudaMalloc(&cuda_g_indices, sizeNewN); + cudaMemset(cuda_g_odata, 0, sizeNewN); + cudaMemcpy(cuda_g_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + timer().startGpuTimer(); - // TODO + // DONE + compactGpu(newN, logn, cuda_g_odata, cuda_g_bools, cuda_g_indices, cuda_g_idata, threadsPerBlock); timer().endGpuTimer(); - return -1; + + int inclusivePrefixSum = 0, lastEle = 0; + cudaMemcpy(&inclusivePrefixSum, cuda_g_indices + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + cudaMemcpy(&lastEle, cuda_g_bools + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * newN, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_odata); + cudaFree(cuda_g_idata); + cudaFree(cuda_g_bools); + cudaFree(cuda_g_indices); + + return inclusivePrefixSum + lastEle; + } + + ////////////////////////////////////////////////////////////////// + + //__global__ void kernInclusiveScanNaiveInBlock(int n, int* odata) { + // extern __shared__ int sharedMemory[]; + // int* s_idata = sharedMemory; + // //int* s_idata2 = sharedMemory + threadIdx.x; + // int indexBaseInBlock = blockIdx.x * blockDim.x; + // int localIdx = threadIdx.x; + + // int totalIdx = indexBaseInBlock + localIdx; + // if (totalIdx >= n) { + // return; + // } + + // s_idata[localIdx] = odata[totalIdx]; + + // // Loop in kernel + // for(unsigned int stride = 1; stride <= n; stride <<= 1) { + // __syncthreads(); + // //int* tmp = s_idata2; + // //s_idata2 = s_idata; + // //s_idata = tmp; + // //printf("stride:%d, blockCount:%d, threadsPerBlock:%d\n", stride, gridDim.x, blockDim.x); + // int fromLocalIdx = localIdx - stride; + // //int prevOdata = fromLocalIdx < 0 ? 0 : s_idata2[fromLocalIdx]; + // int prevOdata = fromLocalIdx < 0 ? 0 : s_idata[fromLocalIdx]; + // __syncthreads(); + + // //s_idata[localIdx] = s_idata2[localIdx] + prevOdata; + // s_idata[localIdx] += prevOdata; + // //__syncthreads(); + // } + // odata[totalIdx] = s_idata[localIdx]; + + + // // Loop out of kernel + // //int indexBaseInBlock = blockIdx.x * blockDim.x; + // //int localIdx = threadIdx.x; + + // //int totalIdx = indexBaseInBlock + localIdx; + // //if (totalIdx >= n) { + // // return; + // //} + + // //s_idata[localIdx] = odata[totalIdx]; + // //__syncthreads(); + // //int fromLocalIdx = localIdx - stride; + // //int prevOdata = fromLocalIdx < 0 ? 0 : s_idata[fromLocalIdx]; + + // //odata[totalIdx] = s_idata[localIdx] + prevOdata; + //} + + __global__ void kernInclusiveScanWorkEfficientInBlock(int n, int* odata) { + extern __shared__ int sharedMemory[]; + int bkDim = blockDim.x; + int dblBkDim = blockDim.x << 1; + int tid = threadIdx.x; + int bid = blockIdx.x; + unsigned int loopLimit = (n > dblBkDim) ? dblBkDim : n; + unsigned int halfLoopLimit = loopLimit >> 1; + + int indexBaseInBlock = bid * dblBkDim; + int localIdx = ((tid + 1) << 1) - 1; + + int totalIdx = indexBaseInBlock + localIdx; + if (totalIdx >= n) { + return; + } + int* s_idata = sharedMemory; + int* s_odata = sharedMemory + dblBkDim; + + int localIdxToRead = tid; + int localIdxToRead2 = tid + halfLoopLimit; + + // Bank conflict? + s_idata[localIdxToRead] = odata[indexBaseInBlock + localIdxToRead]; + s_idata[localIdxToRead2] = odata[indexBaseInBlock + localIdxToRead2]; + s_odata[localIdxToRead] = s_idata[localIdxToRead]; + s_odata[localIdxToRead2] = s_idata[localIdxToRead2]; + //__syncthreads(); + + unsigned int stride = 2; + + // Up sweep + for (; stride <= loopLimit; stride <<= 1) { + localIdx = tid * stride + (stride - 1); + __syncthreads(); + if (localIdx < loopLimit) { + int leftLocalIdx = tid * stride + ((stride - 1) >> 1); + s_odata[localIdx] += s_odata[leftLocalIdx]; + } + } + //printf("tid:%d, stride:%d\n", tid, stride); + + stride >>= 1; + if (tid * stride + stride == loopLimit) { + s_odata[loopLimit - 1] = 0; + } + + // Down sweep + for (; stride > 1; stride >>= 1) { + localIdx = tid * stride + (stride - 1); + __syncthreads(); + if (localIdx < loopLimit) { + int leftLocalIdx = tid * stride + ((stride - 1) >> 1); + int tmp = s_odata[leftLocalIdx]; + s_odata[leftLocalIdx] = s_odata[localIdx]; + s_odata[localIdx] += tmp; + } + } + + // Exclusive to inclusive + __syncthreads(); + s_odata[localIdxToRead] += s_idata[localIdxToRead]; + s_odata[localIdxToRead2] += s_idata[localIdxToRead2]; + + odata[indexBaseInBlock + localIdxToRead] = s_odata[localIdxToRead]; + odata[indexBaseInBlock + localIdxToRead2] = s_odata[localIdxToRead2]; + } + + void inclusiveScanInBlock(int newN, int* cuda_g_odata, int threadsPerBlock = 128) { + //for(int stride = 1; stride <= n; stride <<= 1) { + // int blockCount = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + // //printf("stride:%d, blockCount:%d, threadsPerBlock:%d\n", stride, blockCount, threadsPerBlock); + // kernInclusiveScanInBlockOnce<<>>(newN, cuda_g_odata, stride); + //} + //int blockCount = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + //printf("stride:%d, blockCount:%d, threadsPerBlock:%d\n", stride, blockCount, threadsPerBlock); + //kernInclusiveScanInBlock<<>>(newN, cuda_g_odata); + //kernInclusiveScanNaiveInBlock<<>>(newN, cuda_g_odata); + + int blockCount = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + kernInclusiveScanWorkEfficientInBlock<<> 1, (threadsPerBlock << 1) * sizeof(int)>>>(newN, cuda_g_odata); + } + + __global__ void kernGenBlockSums(int newN, int* blockSums, const int* idata) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index >= newN) { + return; + } + int sumIdx = index * blockDim.x + blockDim.x - 1; + blockSums[index] = idata[sumIdx]; + } + + //__global__ void kernGenBlockIncrementsToExclusive(int newN, int* blockIncrements, const int* idata, const int* blockSums) { + // int index = (blockIdx.x * blockDim.x) + threadIdx.x; + // if (index >= newN) { + // return; + // } + // int bkIdx = blockIdx.x; + // int notSetZero = (index + 1 < newN); + // blockIncrements[notSetZero * index + notSetZero] = notSetZero * (blockSums[bkIdx] + idata[index]); + //} + + __global__ void kernGenBlockIncrementsToInclusive(int newN, int* blockIncrements, const int* idata, const int* blockSums) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index >= newN) { + return; + } + int bkIdx = blockIdx.x; + blockIncrements[index] = blockSums[bkIdx] + idata[index]; + } + +#if WRITE_EXC_WITH_INC + __global__ void kernGenBlockIncrementsToIncExc(int newN, int* blockIncrements, int* odata, const int* blockSums) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index >= newN) { + return; + } + int bkIdx = blockIdx.x; + + int idataAtIdx = odata[index]; + + int blkIncAtIdx = blockSums[bkIdx] + idataAtIdx; + __syncthreads();//Bug, maybe when ptr not in one block? + + blockIncrements[index] = blkIncAtIdx; + int nonZeroIdx = (index + 1 < newN); + odata[nonZeroIdx * (index + 1)] = nonZeroIdx * blkIncAtIdx; + } +#endif // WRITE_EXC_WITH_INC + + //__global__ void kernIncToExc(int newN, int* odata, const int* idata) { + // int index = (blockIdx.x * blockDim.x) + threadIdx.x; + // if (index >= newN) { + // return; + // } + // int bkIdx = blockIdx.x; + // int notSetZero = index + 1 < newN; + // odata[notSetZero * index + notSetZero] = notSetZero * idata[index]; + //} + + void scanGpuBatch(int newN, int logn, int* cuda_g_odata, int* cuda_g_blockSums, int* cuda_g_blockIncrements, int threadsPerBlock = 128, + int depth = 0, bool splitOnce = true) { + int batch = (newN + threadsPerBlock - 1) / threadsPerBlock; + //printf("N:%d, blockCount:%d, threadsPerBlock:%d, logn:%d\n", newN, batch, threadsPerBlock, logn);//TEST + if (batch > 0) { + if (batch > 1) { + inclusiveScanInBlock(newN, cuda_g_odata, threadsPerBlock); + + int nextNewN = batch; + int nextBatch = (nextNewN + threadsPerBlock - 1) / threadsPerBlock; + int logThPerBk = ilog2ceil(threadsPerBlock); + if (splitOnce) { + kernGenBlockSums<<>>(nextNewN, cuda_g_blockSums, cuda_g_odata); + scanGpu(nextNewN, logn - logThPerBk, cuda_g_blockSums, threadsPerBlock); + //kernGenBlockIncrementsToExclusive<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); + //cudaMemcpy(cuda_g_odata, cuda_g_blockIncrements, sizeof(int) * (newN), cudaMemcpyDeviceToDevice); +#if WRITE_EXC_WITH_INC + kernGenBlockIncrementsToIncExc<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); +#else // WRITE_EXC_WITH_INC + kernGenBlockIncrementsToInclusive<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); + cudaMemset(cuda_g_odata, 0, sizeof(int)); + cudaMemcpy(cuda_g_odata + 1, cuda_g_blockIncrements, sizeof(int) * (newN - 1), cudaMemcpyDeviceToDevice); +#endif // WRITE_EXC_WITH_INC + } + else { + kernGenBlockSums<<>>(nextNewN, cuda_g_blockSums, cuda_g_odata); + scanGpuBatch(nextNewN, logn - logThPerBk, cuda_g_blockSums, cuda_g_blockSums + nextNewN, cuda_g_blockIncrements, threadsPerBlock, + depth + 1, splitOnce); + // TODO + //kernGenBlockIncrementsToExclusive<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); + //cudaMemcpy(cuda_g_odata, cuda_g_blockIncrements, sizeof(int) * (newN), cudaMemcpyDeviceToDevice); +#if WRITE_EXC_WITH_INC + kernGenBlockIncrementsToIncExc<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); +#else // WRITE_EXC_WITH_INC + kernGenBlockIncrementsToInclusive<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); + cudaMemset(cuda_g_odata, 0, sizeof(int)); + cudaMemcpy(cuda_g_odata + 1, cuda_g_blockIncrements, sizeof(int) * (newN - 1), cudaMemcpyDeviceToDevice); +#endif // WRITE_EXC_WITH_INC + } + } + else { + //inclusiveScanInBlock(newN, cuda_g_blockIncrements, threadsPerBlock); // Not correct if in depth > 0 + inclusiveScanInBlock(newN, cuda_g_odata, threadsPerBlock); + + cudaMemcpy(cuda_g_blockIncrements, cuda_g_odata, sizeof(int) * (newN), cudaMemcpyDeviceToDevice); + cudaMemset(cuda_g_odata, 0, sizeof(int)); + cudaMemcpy(cuda_g_odata + 1, cuda_g_blockIncrements, sizeof(int) * (newN - 1), cudaMemcpyDeviceToDevice); + } + } + } + + /** + * Performs prefix-sum (aka scan) on idata, storing the result into odata. + */ + void scanBatch(int n, int *odata, const int *idata, bool splitOnce) { + int threadsPerBlock = 512;//128; + + int* cuda_g_odata = nullptr; + int* cuda_g_blockSums = nullptr; + int* cuda_g_blockIncrements = nullptr; + + int logn = ilog2ceil(n); + size_t newN = 1i64 << logn; + size_t sizeNewN = sizeof(int) * newN; + cudaMalloc(&cuda_g_odata, sizeNewN); + cudaMalloc(&cuda_g_blockSums, sizeNewN); + cudaMalloc(&cuda_g_blockIncrements, sizeNewN); + //cudaMemset(cuda_g_odata, 0, sizeNewN); + cudaMemset(cuda_g_blockSums, 0, sizeNewN); + //cudaMemset(cuda_g_blockIncrements, 0, sizeNewN); + cudaMemcpy(cuda_g_odata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + cudaMemcpy(cuda_g_blockIncrements, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToDevice); + + timer().startGpuTimer(); + // DONE + scanGpuBatch(newN, logn, cuda_g_odata, cuda_g_blockSums, cuda_g_blockIncrements, threadsPerBlock, 0, splitOnce); + timer().endGpuTimer(); + + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_blockIncrements); + cudaFree(cuda_g_blockSums); + cudaFree(cuda_g_odata); + } + + void compactGpuBatch(int newN, int logn, int* cuda_g_odata, int* cuda_g_bools, int* cuda_g_indices, int* cuda_g_blockSums, int* cuda_g_blockIncrements, + const int* cuda_g_idata, int threadsPerBlock = 128, bool splitOnce = true) { + int blockCountSimplePara = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + Common::kernMapToBoolean<<>>(newN, cuda_g_bools, cuda_g_idata); + cudaMemcpy(cuda_g_indices, cuda_g_bools, sizeof(int) * newN, cudaMemcpyDeviceToDevice); + scanGpuBatch(newN, logn, cuda_g_indices, cuda_g_blockSums, cuda_g_blockIncrements, threadsPerBlock, 0, splitOnce); + Common::kernScatter<<>>(newN, cuda_g_odata, cuda_g_idata, cuda_g_bools, cuda_g_indices); + } + + /** + * Performs stream compaction on idata, storing the result into odata. + * All zeroes are discarded. + * + * @param n The number of elements in idata. + * @param odata The array into which to store elements. + * @param idata The array of elements to compact. + * @returns The number of elements remaining after compaction. + */ + int compactBatch(int n, int *odata, const int *idata, bool splitOnce) { + int threadsPerBlock = 512;//128 + + int* cuda_g_odata = nullptr; + int* cuda_g_idata = nullptr; + int* cuda_g_bools = nullptr; + int* cuda_g_indices = nullptr; + + int* cuda_g_blockSums = nullptr; + int* cuda_g_blockIncrements = nullptr; + + int logn = ilog2ceil(n); + size_t newN = 1i64 << logn; + size_t sizeNewN = sizeof(int) * newN; + cudaMalloc(&cuda_g_odata, sizeNewN); + cudaMalloc(&cuda_g_idata, sizeNewN); + cudaMalloc(&cuda_g_bools, sizeNewN); + cudaMalloc(&cuda_g_indices, sizeNewN); + cudaMemset(cuda_g_odata, 0, sizeNewN); + cudaMemcpy(cuda_g_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + + cudaMalloc(&cuda_g_blockSums, sizeNewN); + cudaMalloc(&cuda_g_blockIncrements, sizeNewN); + + cudaMemset(cuda_g_blockSums, 0, sizeNewN); + cudaMemset(cuda_g_blockIncrements, 0, sizeNewN); + + timer().startGpuTimer(); + // DONE + compactGpuBatch(newN, logn, cuda_g_odata, cuda_g_bools, cuda_g_indices, cuda_g_blockSums, cuda_g_blockIncrements, cuda_g_idata, threadsPerBlock); + timer().endGpuTimer(); + + int inclusivePrefixSum = 0, lastEle = 0; + cudaMemcpy(&inclusivePrefixSum, cuda_g_indices + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + cudaMemcpy(&lastEle, cuda_g_bools + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * newN, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_blockIncrements); + cudaFree(cuda_g_blockSums); + + cudaFree(cuda_g_odata); + cudaFree(cuda_g_idata); + cudaFree(cuda_g_bools); + cudaFree(cuda_g_indices); + + return inclusivePrefixSum + lastEle; + } + } + + namespace EfficientTest { + using StreamCompaction::Common::PerformanceTimer; + PerformanceTimer& timer() + { + static PerformanceTimer timer; + return timer; + } + + void scan(int n, int* odata, const int* idata) { + int threadsPerBlock = 128; + + int logn = ilog2ceil(n); + int newN = 1 << logn; + + int* cuda_g_odata = nullptr; + //int* cuda_g_idata = nullptr; + cudaMalloc(&cuda_g_odata, sizeof(int) * newN); + //cudaMalloc(&cuda_g_idata, sizeof(int) * newN); + + //cudaMemset(cuda_g_odata, 0, sizeof(odata) * newN); + cudaMemcpy(cuda_g_odata, idata, sizeof(int) * (newN), cudaMemcpyHostToDevice); + + cudaDeviceSynchronize(); + + timer().startGpuTimer(); + // DONE + Efficient::inclusiveScanInBlock(newN, cuda_g_odata, threadsPerBlock); + timer().endGpuTimer(); + + odata[0] = 0; + cudaMemcpy(odata + 1, cuda_g_odata, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_odata); } } } diff --git a/stream_compaction/efficient.h b/stream_compaction/efficient.h index 803cb4f..4685721 100644 --- a/stream_compaction/efficient.h +++ b/stream_compaction/efficient.h @@ -9,5 +9,14 @@ namespace StreamCompaction { void scan(int n, int *odata, const int *idata); int compact(int n, int *odata, const int *idata); + + void scanBatch(int n, int* odata, const int* idata, bool splitOnce = true); + + int compactBatch(int n, int *odata, const int *idata, bool splitOnce = true); + } + namespace EfficientTest { + StreamCompaction::Common::PerformanceTimer& timer(); + + void scan(int n, int *odata, const int *idata); } } diff --git a/stream_compaction/naive.cu b/stream_compaction/naive.cu index 4308876..25e1a33 100644 --- a/stream_compaction/naive.cu +++ b/stream_compaction/naive.cu @@ -11,15 +11,56 @@ namespace StreamCompaction { static PerformanceTimer timer; return timer; } - // TODO: __global__ + // DONE: __global__ + __global__ void kernScanOnce(int n, int* odata, const int* idata, int stride) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index >= n) { + return; + } + int fromIdx = index - stride; + int prevOdata = fromIdx < 0 ? 0 : idata[fromIdx]; + + odata[index] = idata[index] + prevOdata; + } /** * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ - void scan(int n, int *odata, const int *idata) { + void scan(int n, int* odata, const int* idata) { + int threadsPerBlock = 128; + + int* cuda_g_odata = nullptr; + int* cuda_g_idata = nullptr; + cudaMalloc(&cuda_g_odata, sizeof(int) * n); + cudaMalloc(&cuda_g_idata, sizeof(int) * n); + + //cudaMemset(cuda_g_odata, 0, sizeof(odata) * n); + cudaMemcpy(cuda_g_odata, idata, sizeof(int) * (n), cudaMemcpyHostToDevice); + + cudaDeviceSynchronize(); + //int logn = ilog2ceil(n); + timer().startGpuTimer(); - // TODO + // DONE + //int stride = 1; + //for (int i = 0; i < logn; ++i) { + for(int stride = 1; stride <= n; stride <<= 1) { + //int nextStride = stride << 1; + int blockCount = (n + (threadsPerBlock - 1)) / threadsPerBlock; + std::swap(cuda_g_odata, cuda_g_idata); + //printf("stride:%d, blockCount:%d, threadsPerBlock:%d\n", stride, blockCount, threadsPerBlock); + kernScanOnce<<>>(n, cuda_g_odata, cuda_g_idata, stride); + //stride = nextStride; + //cudaDeviceSynchronize(); + } timer().endGpuTimer(); + + odata[0] = 0; + cudaMemcpy(odata + 1, cuda_g_odata, sizeof(int) * (n - 1), cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_idata); + cudaFree(cuda_g_odata); } } } diff --git a/stream_compaction/thrust.cu b/stream_compaction/thrust.cu index 1def45e..682141c 100644 --- a/stream_compaction/thrust.cu +++ b/stream_compaction/thrust.cu @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "common.h" #include "thrust.h" @@ -18,11 +20,66 @@ namespace StreamCompaction { * Performs prefix-sum (aka scan) on idata, storing the result into odata. */ void scan(int n, int *odata, const int *idata) { + thrust::host_vector hst_odata(odata, odata + n), hst_idata(idata, idata + n); + thrust::device_vector dev_odata(hst_odata), dev_idata(hst_idata); + timer().startGpuTimer(); - // TODO use `thrust::exclusive_scan` + // DONE use `thrust::exclusive_scan` // example: for device_vectors dv_in and dv_out: // thrust::exclusive_scan(dv_in.begin(), dv_in.end(), dv_out.begin()); + thrust::exclusive_scan(dev_idata.begin(), dev_idata.end(), dev_odata.begin()); timer().endGpuTimer(); + + cudaMemcpy(odata, dev_odata.data().get(), sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + } + + + struct IsZero { + __device__ bool operator()(const int x) { + return x == 0; + } + } isZero; + /** + * Performs stream compaction on idata, storing the result into odata. + * All zeroes are discarded. + * + * @param n The number of elements in idata. + * @param odata The array into which to store elements. + * @param idata The array of elements to compact. + * @returns The number of elements remaining after compaction. + */ + int compact(int n, int *odata, const int *idata) { + thrust::host_vector hst_odata(idata, idata + n); + thrust::device_vector dev_odata(hst_odata); + + timer().startGpuTimer(); + // DONE + thrust::detail::normal_iterator> dev_endIt = thrust::remove_if(dev_odata.begin(), dev_odata.end(), isZero); + timer().endGpuTimer(); + int count = (dev_endIt - dev_odata.begin()); + //cudaMemcpy(odata, dev_odata.data().get(), sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaMemcpy(odata, dev_odata.data().get(), sizeof(int) * count, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + return count; + //int count = 0; + //for (int i = 0; i < n; ++i) { + // count += odata[i] == 0 ? 0 : 1; + //} + //return count; + } + + void sort(int n, int* odata, const int* idata) { + thrust::host_vector hst_idata(idata, idata + n); + thrust::device_vector dev_idata(hst_idata); + + timer().startGpuTimer(); + thrust::sort(dev_idata.begin(), dev_idata.end()); + timer().endGpuTimer(); + + cudaMemcpy(odata, dev_idata.data().get(), sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); } } } diff --git a/stream_compaction/thrust.h b/stream_compaction/thrust.h index fe98206..aa3f754 100644 --- a/stream_compaction/thrust.h +++ b/stream_compaction/thrust.h @@ -7,5 +7,9 @@ namespace StreamCompaction { StreamCompaction::Common::PerformanceTimer& timer(); void scan(int n, int *odata, const int *idata); + + int compact(int n, int *odata, const int *idata); + + void sort(int n, int* odata, const int* idata); } } From c8bb24eb394ae81d2e05605d640a447cd6e1e1bf Mon Sep 17 00:00:00 2001 From: PacosLelouch <276613158@qq.com> Date: Sun, 19 Sep 2021 21:46:52 -0400 Subject: [PATCH 2/5] Implement sorting and add test code. --- CMakeLists.txt | 4 + src/main.cpp | 232 +------------- src/main.hpp | 13 + src/recording_helpers.cpp | 546 +++++++++++++++++++++++++++++++++ src/recording_helpers.hpp | 72 +++++ src/testing_helpers.cpp | 307 ++++++++++++++++++ src/testing_helpers.hpp | 38 +-- stream_compaction/cpu.cu | 8 + stream_compaction/cpu.h | 2 + stream_compaction/efficient.cu | 143 ++++++++- stream_compaction/efficient.h | 2 + 11 files changed, 1110 insertions(+), 257 deletions(-) create mode 100644 src/main.hpp create mode 100644 src/recording_helpers.cpp create mode 100644 src/recording_helpers.hpp create mode 100644 src/testing_helpers.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c504b62..e2fa7ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,11 +40,15 @@ add_subdirectory(stream_compaction) include_directories(.) set(headers + "src/main.hpp" "src/testing_helpers.hpp" + "src/recording_helpers.hpp" ) set(sources "src/main.cpp" + "src/testing_helpers.cpp" + "src/recording_helpers.cpp" ) list(SORT headers) diff --git a/src/main.cpp b/src/main.cpp index ef32917..cf9f437 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,233 +6,33 @@ * @copyright University of Pennsylvania */ -#include -#include -#include -#include -#include +#include "main.hpp" #include "testing_helpers.hpp" +#include "recording_helpers.hpp" +#define RECORD_PERFORMANCE_ANALYSIS 0//1 +#define RECORD_PERFORMANCE_ANALYSIS_SAMPLE 1000 + +#if RECORD_PERFORMANCE_ANALYSIS +const int SIZE = 1 << 24; +#else // RECORD_PERFORMANCE_ANALYSIS const int SIZE = 1 << 20;//1 << 24;//1 << 22;//1 << 20;//1 << 18;//1 << 16;//1 << 12;//1 << 3;//1 << 8; // feel free to change the size of array +#endif // RECORD_PERFORMANCE_ANALYSIS const int NPOT = SIZE - 3; // Non-Power-Of-Two int *a = new int[SIZE]; int *b = new int[SIZE]; int *c = new int[SIZE]; int main(int argc, char* argv[]) { - // Scan tests - - printf("\n"); - printf("***************************\n"); - printf("** SCAN TESTS %010d **\n", SIZE); - printf("***************************\n"); - - genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case - a[SIZE - 1] = 0; - printArray(SIZE, a, true); - - // initialize b using StreamCompaction::CPU::scan you implement - // We use b for further comparison. Make sure your StreamCompaction::CPU::scan is correct. - // At first all cases passed because b && c are all zeroes. - zeroArray(SIZE, b); - printDesc("cpu scan, power-of-two"); - StreamCompaction::CPU::scan(SIZE, b, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(SIZE, b, true); - - zeroArray(SIZE, c); - printDesc("cpu scan, non-power-of-two"); - StreamCompaction::CPU::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(NPOT, b, true); - printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("naive scan, power-of-two"); - StreamCompaction::Naive::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(SIZE, b, c); - - /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan - onesArray(SIZE, c); - printDesc("1s array for finding bugs"); - StreamCompaction::Naive::scan(SIZE, c, a); - printArray(SIZE, c, true); */ - - zeroArray(SIZE, c); - printDesc("naive scan, non-power-of-two"); - StreamCompaction::Naive::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true); - printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient scan, power-of-two"); - StreamCompaction::Efficient::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpResult(SIZE, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient scan, non-power-of-two"); - StreamCompaction::Efficient::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(NPOT, c, true); - printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("thrust scan, power-of-two"); - StreamCompaction::Thrust::scan(SIZE, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpResult(SIZE, b, c); - - zeroArray(SIZE, c); - printDesc("thrust scan, non-power-of-two"); - StreamCompaction::Thrust::scan(NPOT, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(NPOT, c, true); - printCmpResult(NPOT, b, c); - - ///////////////////////////////////////// - //if (SIZE <= (1 << 7)) { - // zeroArray(SIZE, c); - // printDesc("work-efficient-test scan, power-of-two"); - // StreamCompaction::EfficientTest::scan(SIZE, c, a); - // printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - // //printArray(SIZE, c, true);// - // printCmpResult(SIZE, b, c); - //} - - //zeroArray(SIZE, c); - //printDesc("work-efficient-sh-mem-opt-onesplit scan, power-of-two"); - //StreamCompaction::Efficient::scanBatch(SIZE, c, a, true); - //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - ////printArray(SIZE, c, true);// - //printCmpResult(SIZE, b, c); - - //zeroArray(SIZE, c); - //printDesc("work-efficient-sh-mem-opt-onesplit scan, non-power-of-two"); - //StreamCompaction::Efficient::scanBatch(NPOT, c, a, true); - //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - ////printArray(SIZE, c, true);// - //printCmpResult(NPOT, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient-sh-mem-recursive scan, power-of-two"); - StreamCompaction::Efficient::scanBatch(SIZE, c, a, false); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpResult(SIZE, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient-sh-mem-recursive scan, non-power-of-two"); - StreamCompaction::Efficient::scanBatch(NPOT, c, a, false); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpResult(NPOT, b, c); - ///////////////////////////////////////// - - printf("\n"); - printf("*****************************\n"); - printf("** STREAM COMPACTION TESTS **\n"); - printf("*****************************\n"); - - // Compaction tests - - genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case - a[SIZE - 1] = 4;//0; - printArray(SIZE, a, true); - - int count, expectedCount, expectedNPOT; - - // initialize b using StreamCompaction::CPU::compactWithoutScan you implement - // We use b for further comparison. Make sure your StreamCompaction::CPU::compactWithoutScan is correct. - zeroArray(SIZE, b); - printDesc("cpu compact without scan, power-of-two"); - count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - expectedCount = count; - printArray(count, b, true); - printCmpLenResult(count, expectedCount, b, b); - - zeroArray(SIZE, c); - printDesc("cpu compact without scan, non-power-of-two"); - count = StreamCompaction::CPU::compactWithoutScan(NPOT, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - expectedNPOT = count; - printArray(count, c, true); - printCmpLenResult(count, expectedNPOT, b, c); - - zeroArray(SIZE, c); - printDesc("cpu compact with scan"); - count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); - printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); - printArray(count, c, true); - printCmpLenResult(count, expectedCount, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient compact, power-of-two"); - count = StreamCompaction::Efficient::compact(SIZE, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); - printCmpLenResult(count, expectedCount, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient compact, non-power-of-two"); - count = StreamCompaction::Efficient::compact(NPOT, c, a); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true); - printCmpLenResult(count, expectedNPOT, b, c); - - zeroArray(SIZE, c); - printDesc("thrust compact, power-of-two"); - count = StreamCompaction::Thrust::compact(SIZE, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true);// - printCmpLenResult(count, expectedCount, b, c); - - zeroArray(SIZE, c); - printDesc("thrust compact, non-power-of-two"); - count = StreamCompaction::Thrust::compact(NPOT, c, a); - printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(count, c, true);// - printCmpLenResult(count, expectedNPOT, b, c); - - ///////////////////////////////////////// - - //zeroArray(SIZE, c); - //printDesc("work-efficient-sh-mem-opt-onesplit compact, power-of-two"); - //StreamCompaction::Efficient::compactBatch(SIZE, c, a, true); - //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - ////printArray(SIZE, c, true);// - //printCmpLenResult(count, expectedNPOT, b, c); - - //zeroArray(SIZE, c); - //printDesc("work-efficient-sh-mem-opt-onesplit compact, non-power-of-two"); - //StreamCompaction::Efficient::compactBatch(NPOT, c, a, true); - //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - ////printArray(SIZE, c, true);// - //printCmpLenResult(count, expectedNPOT, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient-sh-mem-recursive compact, power-of-two"); - StreamCompaction::Efficient::compactBatch(SIZE, c, a, false); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpLenResult(count, expectedNPOT, b, c); - - zeroArray(SIZE, c); - printDesc("work-efficient-sh-mem-recursive compact, non-power-of-two"); - StreamCompaction::Efficient::compactBatch(NPOT, c, a, false); - printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); - //printArray(SIZE, c, true);// - printCmpLenResult(count, expectedNPOT, b, c); - ///////////////////////////////////////// - + testMain(); +#if RECORD_PERFORMANCE_ANALYSIS + if (!RecordingHelpers::recordingMain(RECORD_PERFORMANCE_ANALYSIS_SAMPLE)) { + exit(1); + } +#endif // RECORD_PERFORMANCE_ANALYSIS //system("pause"); // stop Win32 console from closing on exit delete[] a; delete[] b; delete[] c; + return 0; } diff --git a/src/main.hpp b/src/main.hpp new file mode 100644 index 0000000..d893f24 --- /dev/null +++ b/src/main.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include +#include +#include + +extern const int SIZE; +extern const int NPOT; +extern int* a; +extern int* b; +extern int* c; diff --git a/src/recording_helpers.cpp b/src/recording_helpers.cpp new file mode 100644 index 0000000..de792d0 --- /dev/null +++ b/src/recording_helpers.cpp @@ -0,0 +1,546 @@ +#include +#include "recording_helpers.hpp" +#include "testing_helpers.hpp" +#include "main.hpp" + +#define CHECK_BODY(precondition, body, file, line) (precondition && body && (printf(" At line %d in '%s'\n", line, file) || 1)) + +RecordingHelpers::Recorder& RecordingHelpers::Recorder::get() { + static Recorder recorder; + return recorder; +} + +void RecordingHelpers::Recorder::reset() { + samplesSizesFileToEventToTime.empty(); + logSizes.empty(); + fileNames.empty(); +} + +void RecordingHelpers::Recorder::reserveSampleCount(size_t count) { + samplesSizesFileToEventToTime.reserve(count); +} + +void RecordingHelpers::Recorder::increaseSampleCount() { + samplesSizesFileToEventToTime.emplace_back(); + samplesSizesFileToEventToTime.back().resize(logSizes.size()); +} + +size_t RecordingHelpers::Recorder::recordLogSize(int logSize) { + auto it = logSizeToIndex.find(logSize); + if (it != logSizeToIndex.end()) { + return it->second; + } + size_t result = logSizes.size(); + logSizes.push_back(logSize); + logSizeToIndex[logSize] = result; + return result; +} + +void RecordingHelpers::Recorder::recordFileEventSizeDuration(const std::string& fileName, const std::string& eventName, int logSize, float duration) { + std::vector>>& SizesFileToEventToTime = samplesSizesFileToEventToTime.back(); + std::unordered_map>& fileToEventToTime = SizesFileToEventToTime[recordLogSize(logSize)]; + std::unordered_map& eventToTime = registerEventToTimeInternal(fileName, fileToEventToTime); + eventToTime[eventName] = duration; +} + +const std::vector& RecordingHelpers::Recorder::getLogSizes() { + return logSizes; +} + +void RecordingHelpers::Recorder::writeToFiles(const std::string& baseDir) const { + if (samplesSizesFileToEventToTime.size() == 0 || samplesSizesFileToEventToTime[0].size() == 0) { + return; + } + for (const std::string& fileName : fileNames) { + std::vector eventNames; + auto& firstEventToTime = samplesSizesFileToEventToTime[0][0].at(fileName); + eventNames.reserve(firstEventToTime.size()); + for (auto& eventTimePair : firstEventToTime) { + eventNames.push_back(eventTimePair.first); + } + + std::vector> samplesHeaders(samplesSizesFileToEventToTime.size() + 3); + + for (size_t sample = 0; sample < samplesSizesFileToEventToTime.size(); ++sample) { + samplesHeaders[sample].push_back("sample-" + std::to_string(sample)); + } + + samplesHeaders[samplesSizesFileToEventToTime.size()].push_back("average"); + samplesHeaders[samplesSizesFileToEventToTime.size() + 1].push_back("max"); + samplesHeaders[samplesSizesFileToEventToTime.size() + 2].push_back("min"); + + for (std::string& eventName : eventNames) { + for (auto& headers : samplesHeaders) { + headers.push_back(eventName); + } + } + + std::vector>> samplesSizesTimes(samplesSizesFileToEventToTime.size() + 3, std::vector>(logSizes.size(), std::vector(eventNames.size()))); + double invSampleCount = 1. / samplesSizesFileToEventToTime.size(); + + for (size_t sample = 0; sample < samplesSizesFileToEventToTime.size(); ++sample) { + for (size_t sizeIdx = 0; sizeIdx < logSizes.size(); ++sizeIdx) { + int logSize = logSizes[sizeIdx]; + for (size_t eventIdx = 0; eventIdx < eventNames.size(); ++eventIdx) { + const std::string& eventName = eventNames[eventIdx]; + double value = samplesSizesFileToEventToTime[sample][sizeIdx].at(fileName).at(eventName); + samplesSizesTimes[sample][sizeIdx][eventIdx] = value; + samplesSizesTimes[samplesSizesFileToEventToTime.size()][sizeIdx][eventIdx] += value * invSampleCount; + if (value > samplesSizesTimes[samplesSizesFileToEventToTime.size() + 1][sizeIdx][eventIdx]) { + samplesSizesTimes[samplesSizesFileToEventToTime.size() + 1][sizeIdx][eventIdx] = value; + } + if (value < samplesSizesTimes[samplesSizesFileToEventToTime.size() + 2][sizeIdx][eventIdx] || samplesSizesTimes[samplesSizesFileToEventToTime.size() + 2][sizeIdx][eventIdx] == 0.) { + samplesSizesTimes[samplesSizesFileToEventToTime.size() + 2][sizeIdx][eventIdx] = value; + } + } + } + } + + std::ofstream fout; + fout.open(baseDir + fileName); + + for (size_t i = 0; i < samplesSizesTimes.size(); ++i) { + size_t block = (i < 3) ? samplesSizesTimes.size() - 3 + i : i - 3; + auto& headers = samplesHeaders[block]; + for (size_t j = 0; j + 1 < headers.size(); ++j) { + fout << headers[j] << ','; + } + fout << headers.back() << std::endl; + + auto& sizesTimes = samplesSizesTimes[block]; + for (size_t sizeIdx = 0; sizeIdx < logSizes.size(); ++sizeIdx) { + fout << (1 << logSizes[sizeIdx]); + for (size_t j = 0; j < sizesTimes[sizeIdx].size(); ++j) { + fout << ',' << sizesTimes[sizeIdx][j]; + } + fout << std::endl; + } + + fout << std::endl; + } + + fout.close(); + } +} + +std::unordered_map& RecordingHelpers::Recorder::registerEventToTimeInternal(const std::string& fileName, std::unordered_map>& fileToEventToTime) +{ + auto it = fileToEventToTime.find(fileName); + if (it != fileToEventToTime.end()) { + return it->second; + } + fileToEventToTime[fileName] = {}; + fileNames.insert(fileName); + return fileToEventToTime[fileName]; +} + +inline bool needToCheckResult(int sampleCount) { + return true; + //return false; +} + +bool recordingAux(int logSize, const std::string& scanDir, const std::string& compactDir, const std::string& sortDir, int sampleCount) { + RecordingHelpers::Recorder& recorder = RecordingHelpers::Recorder::get(); + int SIZE = 1 << logSize; + int NPOT = SIZE - 3; + + genArray(SIZE, a, 50); + //a[SIZE - 1] = 0; + //printArray(SIZE, a, true); + + // initialize b using StreamCompaction::CPU::scan you implement + // We use b for further comparison. Make sure your StreamCompaction::CPU::scan is correct. + // At first all cases passed because b && c are all zeroes. + zeroArray(SIZE, b); + //printDesc("cpu scan, power-of-two"); + StreamCompaction::CPU::scan(SIZE, b, a); + recorder.recordFileEventSizeDuration("power_of_two_" + scanDir, "cpu", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + //printArray(SIZE, b, true); + + zeroArray(SIZE, c); + //printDesc("cpu scan, non-power-of-two"); + StreamCompaction::CPU::scan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + scanDir, "cpu", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + //printArray(NPOT, b, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("naive scan, power-of-two"); + StreamCompaction::Naive::scan(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + scanDir, "naive", logSize, StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan + onesArray(SIZE, c); + printDesc("1s array for finding bugs"); + StreamCompaction::Naive::scan(SIZE, c, a); + printArray(SIZE, c, true); */ + + zeroArray(SIZE, c); + //printDesc("naive scan, non-power-of-two"); + StreamCompaction::Naive::scan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + scanDir, "naive", logSize, StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient scan, power-of-two"); + StreamCompaction::Efficient::scan(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + scanDir, "work-efficient", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient scan, non-power-of-two"); + StreamCompaction::Efficient::scan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + scanDir, "work-efficient", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("thrust scan, power-of-two"); + StreamCompaction::Thrust::scan(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + scanDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + //printDesc("thrust scan, non-power-of-two"); + StreamCompaction::Thrust::scan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + scanDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + + ///////////////////////////////////////// + //if (SIZE <= (1 << 7)) { + // zeroArray(SIZE, c); + // printDesc("work-efficient-test scan, power-of-two"); + // StreamCompaction::EfficientTest::scan(SIZE, c, a); + // printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + // //printArray(SIZE, c, true);// + // printCmpResult(SIZE, b, c); + //} + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, power-of-two"); + //StreamCompaction::Efficient::scanBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(SIZE, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, non-power-of-two"); + //StreamCompaction::Efficient::scanBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient-hierarchical scan, power-of-two"); + StreamCompaction::Efficient::scanBatch(SIZE, c, a, false); + recorder.recordFileEventSizeDuration("power_of_two_" + scanDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient-hierarchical scan, non-power-of-two"); + StreamCompaction::Efficient::scanBatch(NPOT, c, a, false); + recorder.recordFileEventSizeDuration("non_power_of_two_" + scanDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + ///////////////////////////////////////// + + //printf("\n"); + //printf("****************************************\n"); + //printf("** STREAM COMPACTION TESTS %010d **\n", SIZE); + //printf("****************************************\n"); + + // Compaction tests + + genArray(SIZE, a, 4); // Leave a 0 at the end to test that edge case + //a[SIZE - 1] = 3;//3;//0; + //printArray(SIZE, a, true); + + int count, expectedCount, expectedNPOT; + + // initialize b using StreamCompaction::CPU::compactWithoutScan you implement + // We use b for further comparison. Make sure your StreamCompaction::CPU::compactWithoutScan is correct. + zeroArray(SIZE, b); + //printDesc("cpu compact without scan, power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); + recorder.recordFileEventSizeDuration("power_of_two_" + compactDir, "cpu-without-scan", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + expectedCount = count; + //printArray(count, b, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedCount, b, b), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedCount, b, b); + + zeroArray(SIZE, c); + //printDesc("cpu compact without scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + compactDir, "cpu-without-scan", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + expectedNPOT = count; + //printArray(count, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedNPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("cpu compact with scan, power-of-two"); + count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + compactDir, "cpu-with-scan", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + //printArray(count, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedCount, b, b), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + //printDesc("cpu compact with scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithScan(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + compactDir, "cpu-with-scan", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + //printArray(count, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedNPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient compact, power-of-two"); + count = StreamCompaction::Efficient::compact(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + compactDir, "work-efficient", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedCount, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient compact, non-power-of-two"); + count = StreamCompaction::Efficient::compact(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + compactDir, "work-efficient", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true); + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedNPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("thrust compact, power-of-two"); + count = StreamCompaction::Thrust::compact(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + compactDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedCount, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + //printDesc("thrust compact, non-power-of-two"); + count = StreamCompaction::Thrust::compact(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + compactDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedNPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedNPOT, b, c); + + ///////////////////////////////////////// + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, power-of-two"); + //count = StreamCompaction::Efficient::compactBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, non-power-of-two"); + //count = StreamCompaction::Efficient::compactBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient-hierarchical compact, power-of-two"); + count = StreamCompaction::Efficient::compactBatch(SIZE, c, a, false); + recorder.recordFileEventSizeDuration("power_of_two_" + compactDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedCount, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient-hierarchical compact, non-power-of-two"); + count = StreamCompaction::Efficient::compactBatch(NPOT, c, a, false); + recorder.recordFileEventSizeDuration("non_power_of_two_" + compactDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpLenResult(count, expectedNPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpLenResult(count, expectedNPOT, b, c); + ///////////////////////////////////////// + + //printf("\n"); + //printf("***************************\n"); + //printf("** SORT TESTS %010d **\n", SIZE); + //printf("***************************\n"); + + genArray(SIZE, a, SIZE >> 2); // Leave a 0 at the end to test that edge case + //a[SIZE - 1] = 0;//9999;//0; + //printArray(SIZE, a, true); + + zeroArray(SIZE, b); + //printDesc("cpu sort, power-of-two"); + StreamCompaction::CPU::sort(SIZE, b, a); + recorder.recordFileEventSizeDuration("power_of_two_" + sortDir, "cpu", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + //printArray(SIZE, b, true); + + zeroArray(SIZE, c); + //printDesc("thrust sort, power-of-two"); + StreamCompaction::Thrust::sort(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + sortDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient sort, power-of-two"); + StreamCompaction::Efficient::sort(SIZE, c, a); + recorder.recordFileEventSizeDuration("power_of_two_" + sortDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(SIZE, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, b); + //printDesc("cpu sort, non-power-of-two"); + StreamCompaction::CPU::sort(NPOT, b, a); + //printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + recorder.recordFileEventSizeDuration("non_power_of_two_" + sortDir, "cpu", logSize, StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation()); + //printArray(NPOT, b, true); + + zeroArray(SIZE, c); + //printDesc("thrust sort, non-power-of-two"); + StreamCompaction::Thrust::sort(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + sortDir, "thrust", logSize, StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + //printDesc("work-efficient sort, non-power-of-two"); + StreamCompaction::Efficient::sort(NPOT, c, a); + recorder.recordFileEventSizeDuration("non_power_of_two_" + sortDir, "work-efficient-hierarchical", logSize, StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation()); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + if (CHECK_BODY(needToCheckResult(sampleCount), !RecordingHelpers::checkCmpResult(NPOT, b, c), __FILE__, __LINE__)) { + return false; + } + //printCmpResult(NPOT, b, c); + + return true; +} + +bool RecordingHelpers::recordingMain(int maxCount) { + printf("\n"); + printf("******************************************\n"); + printf("** RECORD PERFORMANCE FOR %6d TIMES **\n", maxCount); + printf("******************************************\n"); + + auto startTime = std::chrono::high_resolution_clock::now(); + + std::string baseDir = "../profile/"; + std::string scanFile = "scan.csv"; + std::string compactFile = "compact.csv"; + std::string sortFile = "sort.csv"; + + Recorder& recorder = Recorder::get(); + + recorder.reset(); + recorder.reserveSampleCount(maxCount); + + for (int logSize = 12; (1 << logSize) <= SIZE; logSize += 2) { + recorder.recordLogSize(logSize); + } + + for (int count = 0; count < maxCount; ++count) { + recorder.increaseSampleCount(); + for (int logSize : recorder.getLogSizes()) { + if (!recordingAux(logSize, scanFile, compactFile, sortFile, count)) { + return false; + } + } + auto endTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration duroSecond = endTime - startTime; + printf("Complete count %d, %lf second elpased.\n", count, duroSecond.count()); + } + recorder.writeToFiles(baseDir); + + printf("\n"); + printf("*****************\n"); + printf("** FILES SAVED **\n"); + printf("*****************\n"); + return true; +} + diff --git a/src/recording_helpers.hpp b/src/recording_helpers.hpp new file mode 100644 index 0000000..1d0d244 --- /dev/null +++ b/src/recording_helpers.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include + +namespace RecordingHelpers { + template + int cmpArrays(int n, T *a, T *b) { + for (int i = 0; i < n; i++) { + if (a[i] != b[i]) { + printf(" a[%d] = %d, b[%d] = %d\n", i, a[i], i, b[i]); + return 1; + } + } + return 0; + } + + template + bool checkCmpResult(int n, T *a, T *b) { + if (cmpArrays(n, a, b)) { + printf(" FAIL VALUE \n"); + return false; + } + return true; + } + + template + bool checkCmpLenResult(int n, int expN, T *a, T *b) { + if (n != expN) { + printf(" expected %d elements, got %d\n", expN, n); + return false; + } + if (cmpArrays(n, a, b)) { + printf(" FAIL VALUE \n"); + return false; + } + return true; + } + + class Recorder { + public: + static Recorder& get(); + + void reset(); + + void reserveSampleCount(size_t count); + + void increaseSampleCount(); + + size_t recordLogSize(int size); + + void recordFileEventSizeDuration(const std::string& fileName, const std::string& eventName, int logSize, float duration); + + const std::vector& getLogSizes(); + + void writeToFiles(const std::string& baseDir = "./") const; + + protected: + std::unordered_map& registerEventToTimeInternal(const std::string& fileName, std::unordered_map>& fileToEventToTime); + + private: + std::vector>>> samplesSizesFileToEventToTime; + + std::unordered_map logSizeToIndex; + std::vector logSizes; + std::unordered_set fileNames; + }; + + bool recordingMain(int maxCount = 1000); +} diff --git a/src/testing_helpers.cpp b/src/testing_helpers.cpp new file mode 100644 index 0000000..e3a995f --- /dev/null +++ b/src/testing_helpers.cpp @@ -0,0 +1,307 @@ +#include "main.hpp" +#include "testing_helpers.hpp" + +void printDesc(const char *desc) { + printf("==== %s ====\n", desc); +} + +void zeroArray(int n, int *a) { + for (int i = 0; i < n; i++) { + a[i] = 0; + } +} + +void onesArray(int n, int *a) { + for (int i = 0; i < n; i++) { + a[i] = 1; + } +} + +void genArray(int n, int *a, int maxval) { + srand(time(nullptr)); + + for (int i = 0; i < n; i++) { + a[i] = rand() % maxval; + } +} + +void printArray(int n, int *a, bool abridged) { + printf(" [ "); + for (int i = 0; i < n; i++) { + if (abridged && i + 2 == 15 && n > 16) { + i = n - 2; + printf("... "); + } + printf("%3d ", a[i]); + } + printf("]\n"); +} + +void testMain() { + // Scan tests + + printf("\n"); + printf("***************************\n"); + printf("** SCAN TESTS %010d **\n", SIZE); + printf("***************************\n"); + + genArray(SIZE - 1, a, 50); // Leave a 0 at the end to test that edge case + a[SIZE - 1] = 0; + printArray(SIZE, a, true); + + // initialize b using StreamCompaction::CPU::scan you implement + // We use b for further comparison. Make sure your StreamCompaction::CPU::scan is correct. + // At first all cases passed because b && c are all zeroes. + zeroArray(SIZE, b); + printDesc("cpu scan, power-of-two"); + StreamCompaction::CPU::scan(SIZE, b, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(SIZE, b, true); + + zeroArray(SIZE, c); + printDesc("cpu scan, non-power-of-two"); + StreamCompaction::CPU::scan(NPOT, c, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(NPOT, b, true); + printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("naive scan, power-of-two"); + StreamCompaction::Naive::scan(SIZE, c, a); + printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true); + printCmpResult(SIZE, b, c); + + /* For bug-finding only: Array of 1s to help find bugs in stream compaction or scan + onesArray(SIZE, c); + printDesc("1s array for finding bugs"); + StreamCompaction::Naive::scan(SIZE, c, a); + printArray(SIZE, c, true); */ + + zeroArray(SIZE, c); + printDesc("naive scan, non-power-of-two"); + StreamCompaction::Naive::scan(NPOT, c, a); + printElapsedTime(StreamCompaction::Naive::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true); + printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient scan, power-of-two"); + StreamCompaction::Efficient::scan(SIZE, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient scan, non-power-of-two"); + StreamCompaction::Efficient::scan(NPOT, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true); + printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("thrust scan, power-of-two"); + StreamCompaction::Thrust::scan(SIZE, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + printDesc("thrust scan, non-power-of-two"); + StreamCompaction::Thrust::scan(NPOT, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true); + printCmpResult(NPOT, b, c); + + ///////////////////////////////////////// + //if (SIZE <= (1 << 7)) { + // zeroArray(SIZE, c); + // printDesc("work-efficient-test scan, power-of-two"); + // StreamCompaction::EfficientTest::scan(SIZE, c, a); + // printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + // //printArray(SIZE, c, true);// + // printCmpResult(SIZE, b, c); + //} + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, power-of-two"); + //StreamCompaction::Efficient::scanBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(SIZE, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit scan, non-power-of-two"); + //StreamCompaction::Efficient::scanBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-hierarchical scan, power-of-two"); + StreamCompaction::Efficient::scanBatch(SIZE, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-hierarchical scan, non-power-of-two"); + StreamCompaction::Efficient::scanBatch(NPOT, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(NPOT, b, c); + ///////////////////////////////////////// + + printf("\n"); + printf("****************************************\n"); + printf("** STREAM COMPACTION TESTS %010d **\n", SIZE); + printf("****************************************\n"); + + // Compaction tests + + genArray(SIZE - 1, a, 4); // Leave a 0 at the end to test that edge case + a[SIZE - 1] = 3;//3;//0; + printArray(SIZE, a, true); + + int count, expectedCount, expectedNPOT; + + // initialize b using StreamCompaction::CPU::compactWithoutScan you implement + // We use b for further comparison. Make sure your StreamCompaction::CPU::compactWithoutScan is correct. + zeroArray(SIZE, b); + printDesc("cpu compact without scan, power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(SIZE, b, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + expectedCount = count; + printArray(count, b, true); + printCmpLenResult(count, expectedCount, b, b); + + zeroArray(SIZE, c); + printDesc("cpu compact without scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithoutScan(NPOT, c, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + expectedNPOT = count; + printArray(count, c, true); + printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("cpu compact with scan, power-of-two"); + count = StreamCompaction::CPU::compactWithScan(SIZE, c, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(count, c, true); + printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + printDesc("cpu compact with scan, non-power-of-two"); + count = StreamCompaction::CPU::compactWithScan(NPOT, c, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(count, c, true); + printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient compact, power-of-two"); + count = StreamCompaction::Efficient::compact(SIZE, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true); + 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); + printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("thrust compact, power-of-two"); + count = StreamCompaction::Thrust::compact(SIZE, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + printDesc("thrust compact, non-power-of-two"); + count = StreamCompaction::Thrust::compact(NPOT, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(count, c, true);// + printCmpLenResult(count, expectedNPOT, b, c); + + ///////////////////////////////////////// + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, power-of-two"); + //count = StreamCompaction::Efficient::compactBatch(SIZE, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + //zeroArray(SIZE, c); + //printDesc("work-efficient-sh-mem-opt-onesplit compact, non-power-of-two"); + //count = StreamCompaction::Efficient::compactBatch(NPOT, c, a, true); + //printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + ////printArray(SIZE, c, true);// + //printCmpLenResult(count, expectedNPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-hierarchical compact, power-of-two"); + count = StreamCompaction::Efficient::compactBatch(SIZE, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpLenResult(count, expectedCount, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient-hierarchical compact, non-power-of-two"); + count = StreamCompaction::Efficient::compactBatch(NPOT, c, a, false); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpLenResult(count, expectedNPOT, b, c); + ///////////////////////////////////////// + + printf("\n"); + printf("***************************\n"); + printf("** SORT TESTS %010d **\n", SIZE); + printf("***************************\n"); + + genArray(SIZE - 1, a, SIZE >> 2); // Leave a 0 at the end to test that edge case + a[SIZE - 1] = 0;//9999;//0; + printArray(SIZE, a, true); + + zeroArray(SIZE, b); + printDesc("cpu sort, power-of-two"); + StreamCompaction::CPU::sort(SIZE, b, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(SIZE, b, true); + + zeroArray(SIZE, c); + printDesc("thrust sort, power-of-two"); + StreamCompaction::Thrust::sort(SIZE, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient sort, power-of-two"); + StreamCompaction::Efficient::sort(SIZE, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(SIZE, b, c); + + zeroArray(SIZE, b); + printDesc("cpu sort, non-power-of-two"); + StreamCompaction::CPU::sort(NPOT, b, a); + printElapsedTime(StreamCompaction::CPU::timer().getCpuElapsedTimeForPreviousOperation(), "(std::chrono Measured)"); + printArray(NPOT, b, true); + + zeroArray(SIZE, c); + printDesc("thrust sort, non-power-of-two"); + StreamCompaction::Thrust::sort(NPOT, c, a); + printElapsedTime(StreamCompaction::Thrust::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(NPOT, c, true);// + printCmpResult(NPOT, b, c); + + zeroArray(SIZE, c); + printDesc("work-efficient sort, non-power-of-two"); + StreamCompaction::Efficient::sort(NPOT, c, a); + printElapsedTime(StreamCompaction::Efficient::timer().getGpuElapsedTimeForPreviousOperation(), "(CUDA Measured)"); + //printArray(SIZE, c, true);// + printCmpResult(NPOT, b, c); +} diff --git a/src/testing_helpers.hpp b/src/testing_helpers.hpp index 025e94a..5652461 100644 --- a/src/testing_helpers.hpp +++ b/src/testing_helpers.hpp @@ -17,9 +17,7 @@ int cmpArrays(int n, T *a, T *b) { return 0; } -void printDesc(const char *desc) { - printf("==== %s ====\n", desc); -} +void printDesc(const char* desc); template void printCmpResult(int n, T *a, T *b) { @@ -37,40 +35,18 @@ void printCmpLenResult(int n, int expN, T *a, T *b) { cmpArrays(n, a, b) ? "FAIL VALUE" : "passed"); } -void zeroArray(int n, int *a) { - for (int i = 0; i < n; i++) { - a[i] = 0; - } -} - -void onesArray(int n, int *a) { - for (int i = 0; i < n; i++) { - a[i] = 1; - } -} +void zeroArray(int n, int* a); -void genArray(int n, int *a, int maxval) { - srand(time(nullptr)); +void onesArray(int n, int* a); - for (int i = 0; i < n; i++) { - a[i] = rand() % maxval; - } -} +void genArray(int n, int* a, int maxval); -void printArray(int n, int *a, bool abridged = false) { - printf(" [ "); - for (int i = 0; i < n; i++) { - if (abridged && i + 2 == 15 && n > 16) { - i = n - 2; - printf("... "); - } - printf("%3d ", a[i]); - } - printf("]\n"); -} +void printArray(int n, int* a, bool abridged = false); template void printElapsedTime(T time, std::string note = "") { std::cout << " elapsed time: " << time << "ms " << note << std::endl; } + +void testMain(); diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index d035dc4..b33f90e 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -77,5 +77,13 @@ namespace StreamCompaction { timer().endCpuTimer(); return size; } + + void sort(int n, int* odata, const int* idata) { + memcpy(odata, idata, sizeof(int) * n); + timer().startCpuTimer(); + std::sort(odata, odata + n); + //std::stable_sort(odata, odata + n); + timer().endCpuTimer(); + } } } diff --git a/stream_compaction/cpu.h b/stream_compaction/cpu.h index 873c047..222b77a 100644 --- a/stream_compaction/cpu.h +++ b/stream_compaction/cpu.h @@ -11,5 +11,7 @@ namespace StreamCompaction { int compactWithoutScan(int n, int *odata, const int *idata); int compactWithScan(int n, int *odata, const int *idata); + + void sort(int n, int* odata, const int* idata); } } diff --git a/stream_compaction/efficient.cu b/stream_compaction/efficient.cu index 34365d0..413aa31 100644 --- a/stream_compaction/efficient.cu +++ b/stream_compaction/efficient.cu @@ -140,7 +140,7 @@ namespace StreamCompaction { cudaMemcpy(&inclusivePrefixSum, cuda_g_indices + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); cudaMemcpy(&lastEle, cuda_g_bools + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); - cudaMemcpy(odata, cuda_g_odata, sizeof(int) * newN, cudaMemcpyDeviceToHost); + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); cudaDeviceSynchronize(); cudaFree(cuda_g_odata); @@ -203,6 +203,22 @@ namespace StreamCompaction { // //odata[totalIdx] = s_idata[localIdx] + prevOdata; //} + //__global__ void kernInclusiveScanSerialInBlock(int n, int *odata) { + // int bkDim = blockDim.x; + // int index = ((blockIdx.x * bkDim) + threadIdx.x) * bkDim; + // if (index >= n) { + // return; + // } + // int loopLimit = n > bkDim ? bkDim : n; + // int sum = 0; + // for (int i = 0; i < loopLimit; ++i) { + // int totalIndex = index + i; + // int temp = odata[totalIndex]; + // sum += temp; + // odata[totalIndex] = sum; + // } + //} + __global__ void kernInclusiveScanWorkEfficientInBlock(int n, int* odata) { extern __shared__ int sharedMemory[]; int bkDim = blockDim.x; @@ -234,8 +250,9 @@ namespace StreamCompaction { unsigned int stride = 2; - // Up sweep - for (; stride <= loopLimit; stride <<= 1) { + // Up sweep. It is not necessary to calculate the rightmost sum + //for (; stride <= loopLimit; stride <<= 1) { + for (; stride < loopLimit; stride <<= 1) { localIdx = tid * stride + (stride - 1); __syncthreads(); if (localIdx < loopLimit) { @@ -245,7 +262,7 @@ namespace StreamCompaction { } //printf("tid:%d, stride:%d\n", tid, stride); - stride >>= 1; + //stride >>= 1; // Because the rightmost sum is not calculated, we don't need to halve the stride if (tid * stride + stride == loopLimit) { s_odata[loopLimit - 1] = 0; } @@ -267,11 +284,12 @@ namespace StreamCompaction { s_odata[localIdxToRead] += s_idata[localIdxToRead]; s_odata[localIdxToRead2] += s_idata[localIdxToRead2]; + // Write back odata[indexBaseInBlock + localIdxToRead] = s_odata[localIdxToRead]; odata[indexBaseInBlock + localIdxToRead2] = s_odata[localIdxToRead2]; } - void inclusiveScanInBlock(int newN, int* cuda_g_odata, int threadsPerBlock = 128) { + inline void inclusiveScanInBlock(int newN, int* cuda_g_odata, int threadsPerBlock = 128) { //for(int stride = 1; stride <= n; stride <<= 1) { // int blockCount = (newN + (threadsPerBlock - 1)) / threadsPerBlock; // //printf("stride:%d, blockCount:%d, threadsPerBlock:%d\n", stride, blockCount, threadsPerBlock); @@ -344,12 +362,18 @@ namespace StreamCompaction { //} void scanGpuBatch(int newN, int logn, int* cuda_g_odata, int* cuda_g_blockSums, int* cuda_g_blockIncrements, int threadsPerBlock = 128, - int depth = 0, bool splitOnce = true) { + int depth = 0, bool splitOnce = false) { int batch = (newN + threadsPerBlock - 1) / threadsPerBlock; //printf("N:%d, blockCount:%d, threadsPerBlock:%d, logn:%d\n", newN, batch, threadsPerBlock, logn);//TEST if (batch > 0) { if (batch > 1) { - inclusiveScanInBlock(newN, cuda_g_odata, threadsPerBlock); + //if (depth < 2) { + // int serialBatch = std::max(1, batch / threadsPerBlock); + // kernInclusiveScanSerialInBlock<<>>(newN, cuda_g_odata); + //} + //else { + inclusiveScanInBlock(newN, cuda_g_odata, threadsPerBlock); + //} int nextNewN = batch; int nextBatch = (nextNewN + threadsPerBlock - 1) / threadsPerBlock; @@ -371,7 +395,6 @@ namespace StreamCompaction { kernGenBlockSums<<>>(nextNewN, cuda_g_blockSums, cuda_g_odata); scanGpuBatch(nextNewN, logn - logThPerBk, cuda_g_blockSums, cuda_g_blockSums + nextNewN, cuda_g_blockIncrements, threadsPerBlock, depth + 1, splitOnce); - // TODO //kernGenBlockIncrementsToExclusive<<>>(newN, cuda_g_blockIncrements, cuda_g_odata, cuda_g_blockSums); //cudaMemcpy(cuda_g_odata, cuda_g_blockIncrements, sizeof(int) * (newN), cudaMemcpyDeviceToDevice); #if WRITE_EXC_WITH_INC @@ -430,7 +453,7 @@ namespace StreamCompaction { } void compactGpuBatch(int newN, int logn, int* cuda_g_odata, int* cuda_g_bools, int* cuda_g_indices, int* cuda_g_blockSums, int* cuda_g_blockIncrements, - const int* cuda_g_idata, int threadsPerBlock = 128, bool splitOnce = true) { + const int* cuda_g_idata, int threadsPerBlock = 128, bool splitOnce = false) { int blockCountSimplePara = (newN + (threadsPerBlock - 1)) / threadsPerBlock; Common::kernMapToBoolean<<>>(newN, cuda_g_bools, cuda_g_idata); cudaMemcpy(cuda_g_indices, cuda_g_bools, sizeof(int) * newN, cudaMemcpyDeviceToDevice); @@ -483,7 +506,7 @@ namespace StreamCompaction { cudaMemcpy(&inclusivePrefixSum, cuda_g_indices + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); cudaMemcpy(&lastEle, cuda_g_bools + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); - cudaMemcpy(odata, cuda_g_odata, sizeof(int) * newN, cudaMemcpyDeviceToHost); + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); cudaDeviceSynchronize(); cudaFree(cuda_g_blockIncrements); @@ -496,6 +519,106 @@ namespace StreamCompaction { return inclusivePrefixSum + lastEle; } + + ///////// Sort + + __global__ void kernMapToBooleanWithBit(int n, int* boolOnes, int* boolZeros, const int *idata, int bit) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index < n) { + int nonZero = (idata[index] & (1 << bit)) >> bit; + boolOnes[index] = nonZero; + boolZeros[index] = 1 - nonZero; + } + } + + __global__ void kernScatter01(int n, int *odata, + const int *idata, const int* boolOnes, const int* boolZeros, const int* indexZeros) { + int index = (blockIdx.x * blockDim.x) + threadIdx.x; + if (index < n) { + int scatterIdx = indexZeros[index]; + if (boolZeros[index] == 1) { + odata[scatterIdx] = idata[index]; + } + if (boolOnes[index] == 1) { + int lastIndexZero = indexZeros[n - 1]; + int lastBoolZero = boolZeros[n - 1]; + int totalZeros = lastIndexZero + lastBoolZero; + odata[index - scatterIdx + totalZeros] = idata[index]; + } + } + } + + void sortGpu(int newN, int logn, int* cuda_g_odata, int* cuda_g_boolOnes, int* cuda_g_boolZeros, int* cuda_g_indexOnes, int* cuda_g_indexZeros, int* cuda_g_blockSums, int* cuda_g_blockIncrements, + int* cuda_g_idata, int threadsPerBlock = 128, bool splitOnce = false) { + int blockCountSimplePara = (newN + (threadsPerBlock - 1)) / threadsPerBlock; + // Suppose all nums are non-negative, so bit = 31 is not necessary. + for (int bit = 0; bit < 31; ++bit) { + if (bit > 0) { + std::swap(cuda_g_odata, cuda_g_idata); + } + kernMapToBooleanWithBit<<>>(newN, cuda_g_boolOnes, cuda_g_boolZeros, cuda_g_idata, bit); + cudaMemcpy(cuda_g_indexZeros, cuda_g_boolZeros, sizeof(int) * newN, cudaMemcpyDeviceToDevice); + cudaMemcpy(cuda_g_indexOnes, cuda_g_boolOnes, sizeof(int) * newN, cudaMemcpyDeviceToDevice); + scanGpuBatch(newN, logn, cuda_g_indexZeros, cuda_g_blockSums, cuda_g_blockIncrements, threadsPerBlock, 0, splitOnce); + //scanGpuBatch(newN, logn, cuda_g_indexOnes, cuda_g_blockSums, cuda_g_blockIncrements, threadsPerBlock, 0, splitOnce); + kernScatter01<<>>(newN, cuda_g_odata, cuda_g_idata, cuda_g_boolOnes, cuda_g_boolZeros, cuda_g_indexZeros); + } + } + + void sort(int n, int* odata, const int* idata) { + int threadsPerBlock = 512;//128 + + int* cuda_g_odata = nullptr; + int* cuda_g_idata = nullptr; + int* cuda_g_boolOnes = nullptr; + int* cuda_g_indexOnes = nullptr; + int* cuda_g_boolZeros = nullptr; + int* cuda_g_indexZeros = nullptr; + + int* cuda_g_blockSums = nullptr; + int* cuda_g_blockIncrements = nullptr; + + int logn = ilog2ceil(n); + size_t newN = 1i64 << logn; + size_t sizeNewN = sizeof(int) * newN; + cudaMalloc(&cuda_g_odata, sizeNewN); + cudaMalloc(&cuda_g_idata, sizeNewN); + cudaMalloc(&cuda_g_boolOnes, sizeNewN); + cudaMalloc(&cuda_g_indexOnes, sizeNewN); + cudaMalloc(&cuda_g_boolZeros, sizeNewN); + cudaMalloc(&cuda_g_indexZeros, sizeNewN); + cudaMemset(cuda_g_odata, 0x7FFFFFFF, sizeNewN); + cudaMemcpy(cuda_g_idata, idata, sizeof(int) * n, cudaMemcpyHostToDevice); + cudaMemset(cuda_g_idata + n, 0x7FFFFFFF, sizeof(int) * (newN - n)); + + cudaMalloc(&cuda_g_blockSums, sizeNewN); + cudaMalloc(&cuda_g_blockIncrements, sizeNewN); + + cudaMemset(cuda_g_blockSums, 0, sizeNewN); + cudaMemset(cuda_g_blockIncrements, 0x7FFFFFFF, sizeNewN); + + timer().startGpuTimer(); + // DONE + sortGpu(newN, logn, cuda_g_odata, cuda_g_boolOnes, cuda_g_boolZeros, cuda_g_indexOnes, cuda_g_indexZeros, cuda_g_blockSums, cuda_g_blockIncrements, cuda_g_idata, threadsPerBlock, false); + timer().endGpuTimer(); + + //int inclusivePrefixSum = 0, lastEle = 0; + //cudaMemcpy(&inclusivePrefixSum, cuda_g_indices + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + //cudaMemcpy(&lastEle, cuda_g_bools + newN - 1, sizeof(int), cudaMemcpyDeviceToHost); + + cudaMemcpy(odata, cuda_g_odata, sizeof(int) * n, cudaMemcpyDeviceToHost); + cudaDeviceSynchronize(); + + cudaFree(cuda_g_blockIncrements); + cudaFree(cuda_g_blockSums); + + cudaFree(cuda_g_odata); + cudaFree(cuda_g_idata); + cudaFree(cuda_g_boolZeros); + cudaFree(cuda_g_indexZeros); + cudaFree(cuda_g_boolOnes); + cudaFree(cuda_g_indexOnes); + } } namespace EfficientTest { diff --git a/stream_compaction/efficient.h b/stream_compaction/efficient.h index 4685721..1c9bf7c 100644 --- a/stream_compaction/efficient.h +++ b/stream_compaction/efficient.h @@ -13,6 +13,8 @@ namespace StreamCompaction { void scanBatch(int n, int* odata, const int* idata, bool splitOnce = true); int compactBatch(int n, int *odata, const int *idata, bool splitOnce = true); + + void sort(int n, int* odata, const int* idata); } namespace EfficientTest { StreamCompaction::Common::PerformanceTimer& timer(); From a0067a39759991c3d03c90b23860a0fb8d3278d2 Mon Sep 17 00:00:00 2001 From: PacosLelouch <276613158@qq.com> Date: Sun, 19 Sep 2021 23:52:48 -0400 Subject: [PATCH 3/5] Modify CPU compact without scan. --- stream_compaction/cpu.cu | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stream_compaction/cpu.cu b/stream_compaction/cpu.cu index b33f90e..b7d66a7 100644 --- a/stream_compaction/cpu.cu +++ b/stream_compaction/cpu.cu @@ -39,8 +39,9 @@ namespace StreamCompaction { // DONE int size = 0; for (int i = 0; i < n; ++i) { - if (idata[i] != 0) { - odata[size++] = idata[i]; + int idatai = idata[i]; + if (idatai != 0) { + odata[size++] = idatai; } } timer().endCpuTimer(); From 864518716bb408059268d0a918b2dcb2a8038837 Mon Sep 17 00:00:00 2001 From: PacosLelouch <276613158@qq.com> Date: Mon, 20 Sep 2021 00:58:12 -0400 Subject: [PATCH 4/5] Complete README. --- README.md | 205 +- img/readme/DeviceRadixSort.png | Bin 0 -> 52244 bytes img/readme/DeviceRemoveIf.png | Bin 0 -> 60903 bytes img/readme/DeviceScanKernel.png | Bin 0 -> 58231 bytes img/readme/DurationCompactNPOT1.png | Bin 0 -> 197964 bytes img/readme/DurationCompactNPOT2.png | Bin 0 -> 200327 bytes img/readme/DurationCompactPOT1.png | Bin 0 -> 198514 bytes img/readme/DurationCompactPOT2.png | Bin 0 -> 198611 bytes img/readme/DurationScanNPOT1.png | Bin 0 -> 182951 bytes img/readme/DurationScanNPOT2.png | Bin 0 -> 204770 bytes img/readme/DurationScanPOT1.png | Bin 0 -> 200777 bytes img/readme/DurationScanPOT2.png | Bin 0 -> 197657 bytes img/readme/DurationSortNPOT1.png | Bin 0 -> 145246 bytes img/readme/DurationSortNPOT2.png | Bin 0 -> 128153 bytes img/readme/DurationSortPOT1.png | Bin 0 -> 144297 bytes img/readme/DurationSortPOT2.png | Bin 0 -> 134394 bytes .../WorkEfficientHierarchicalAlgorithm.pdf | Bin 0 -> 111845 bytes .../WorkEfficientHierarchicalAlgorithm.png | Bin 0 -> 104003 bytes profile/raw/non_power_of_two_compact.csv | 9027 +++++++++++++++++ profile/raw/non_power_of_two_scan.csv | 9027 +++++++++++++++++ profile/raw/non_power_of_two_sort.csv | 9027 +++++++++++++++++ profile/raw/power_of_two_compact.csv | 9027 +++++++++++++++++ profile/raw/power_of_two_scan.csv | 9027 +++++++++++++++++ profile/raw/power_of_two_sort.csv | 9027 +++++++++++++++++ profile/summary.xlsx | Bin 0 -> 77665 bytes 25 files changed, 54361 insertions(+), 6 deletions(-) create mode 100644 img/readme/DeviceRadixSort.png create mode 100644 img/readme/DeviceRemoveIf.png create mode 100644 img/readme/DeviceScanKernel.png create mode 100644 img/readme/DurationCompactNPOT1.png create mode 100644 img/readme/DurationCompactNPOT2.png create mode 100644 img/readme/DurationCompactPOT1.png create mode 100644 img/readme/DurationCompactPOT2.png create mode 100644 img/readme/DurationScanNPOT1.png create mode 100644 img/readme/DurationScanNPOT2.png create mode 100644 img/readme/DurationScanPOT1.png create mode 100644 img/readme/DurationScanPOT2.png create mode 100644 img/readme/DurationSortNPOT1.png create mode 100644 img/readme/DurationSortNPOT2.png create mode 100644 img/readme/DurationSortPOT1.png create mode 100644 img/readme/DurationSortPOT2.png create mode 100644 img/readme/WorkEfficientHierarchicalAlgorithm.pdf create mode 100644 img/readme/WorkEfficientHierarchicalAlgorithm.png create mode 100644 profile/raw/non_power_of_two_compact.csv create mode 100644 profile/raw/non_power_of_two_scan.csv create mode 100644 profile/raw/non_power_of_two_sort.csv create mode 100644 profile/raw/power_of_two_compact.csv create mode 100644 profile/raw/power_of_two_scan.csv create mode 100644 profile/raw/power_of_two_sort.csv create mode 100644 profile/summary.xlsx diff --git a/README.md b/README.md index 0e38ddb..669fd9e 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,205 @@ 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) +* Xuntong Liang + * [LinkedIn](https://www.linkedin.com/in/xuntong-liang-406429181/), [GitHub](https://github.com/PacosLelouch), [twitter](https://twitter.com/XTL90234545). +* Tested on: Windows 10, i7-10750H @ 2.60GHz 16GB, RTX 2070 Super with Max-Q 8192MB -### (TODO: Your README) +## Tasks -Include analysis, etc. (Remember, this is public, so don't put -anything here that you don't want to share with the world.) +In this project, I have done several tasks include: + +- Implemented the CPU scan and stream compaction without and with scan. +- Implemented the "Naïve" GPU scan. +- Implemented the "Work-Efficient" GPU scan and stream compaction. Optimized these implementations to be faster than CPU's implementations. (Extra Credit) +- Implemented the "Work-Efficient-Hierarchical" GPU scan and stream compaction, which take advantage of shared memory. (Extra Credit) +- Implemented the radix sort with the "Work-Efficient-Hierarchical". (Extra Credit) +- Compared those with Thrust's implementation of "exclusive_sort", "remove_if", and "sort", and the CPU quick sort. +- Completed performance analysis with `Nsight`, `std::chrono`, and `cudaEvent`. Generated some plots for comparison. + + + +### About Work-Efficient-Hierarchical + +The `Work-Efficient-Hierarchical` implementation comes up with a recursive algorithm. We divide the array into blocks and compute the inclusive sum in each block. Then we take the last element of each block to be a new array. We compute the exclusive sum of this new array. Each element is added into its corresponding block of the inclusive sum array, and then we get the inclusive sum of the whole array. But how to compute the exclusive sum of the new array if this new array can be divided into multiple blocks? We convert this goal into computing the inclusive sum and shifting it into the exclusive sum. The process of computing the inclusive sum can be the same as what we did just know. Example is shown as the figure below. + +![Work-Efficient-Hierarchical Algorithm](img/readme/WorkEfficientHierarchicalAlgorithm.png) + + + +### About Radix Sort + +I implemented my radix sort by naively sweeping the 31 bits of integer (suppose all of the elements are non-negative), and do stream compaction in each sweep. The outermost function's prototype is `void sort(int n, int* odata, const int* idata)` so it can be called in the same way as `void scan(int n, int *odata, const int *idata)` and `int compact(int n, int *odata, const int *idata)`. + + + +## Test Output + +First of all, let's take a look at a straight-forward test output with 1048576 (2^20) elements. It contains a group of test which tells us the duration of each algorithm and whether it passed the test or not. + +```C++ +*************************** +** SCAN TESTS 0001048576 ** +*************************** + [ 8 25 28 12 36 17 34 22 28 36 12 9 21 ... 27 0 ] +==== cpu scan, power-of-two ==== + elapsed time: 0.8124ms (std::chrono Measured) + [ 0 8 33 61 73 109 126 160 182 210 246 258 267 ... 25676644 25676671 ] +==== cpu scan, non-power-of-two ==== + elapsed time: 0.5706ms (std::chrono Measured) + [ 0 8 33 61 73 109 126 160 182 210 246 258 267 ... 25676598 25676600 ] + passed +==== naive scan, power-of-two ==== + elapsed time: 0.699104ms (CUDA Measured) + passed +==== naive scan, non-power-of-two ==== + elapsed time: 0.673728ms (CUDA Measured) + passed +==== work-efficient scan, power-of-two ==== + elapsed time: 0.311136ms (CUDA Measured) + passed +==== work-efficient scan, non-power-of-two ==== + elapsed time: 0.309504ms (CUDA Measured) + passed +==== thrust scan, power-of-two ==== + elapsed time: 0.17056ms (CUDA Measured) + passed +==== thrust scan, non-power-of-two ==== + elapsed time: 0.161856ms (CUDA Measured) + passed +==== work-efficient-hierarchical scan, power-of-two ==== + elapsed time: 0.2256ms (CUDA Measured) + passed +==== work-efficient-hierarchical scan, non-power-of-two ==== + elapsed time: 0.227264ms (CUDA Measured) + passed + +**************************************** +** STREAM COMPACTION TESTS 0001048576 ** +**************************************** + [ 0 3 0 2 2 1 2 0 0 2 2 1 3 ... 1 3 ] +==== cpu compact without scan, power-of-two ==== + elapsed time: 2.6959ms (std::chrono Measured) + [ 3 2 2 1 2 2 2 1 3 1 1 2 2 ... 1 3 ] + passed +==== cpu compact without scan, non-power-of-two ==== + elapsed time: 2.3748ms (std::chrono Measured) + [ 3 2 2 1 2 2 2 1 3 1 1 2 2 ... 3 2 ] + passed +==== cpu compact with scan, power-of-two ==== + elapsed time: 1.3746ms (std::chrono Measured) + [ 3 2 2 1 2 2 2 1 3 1 1 2 2 ... 1 3 ] + passed +==== cpu compact with scan, non-power-of-two ==== + elapsed time: 1.8382ms (std::chrono Measured) + [ 3 2 2 1 2 2 2 1 3 1 1 2 2 ... 3 2 ] + passed +==== work-efficient compact, power-of-two ==== + elapsed time: 0.474464ms (CUDA Measured) + passed +==== work-efficient compact, non-power-of-two ==== + elapsed time: 0.46448ms (CUDA Measured) + passed +==== thrust compact, power-of-two ==== + elapsed time: 0.267296ms (CUDA Measured) + passed +==== thrust compact, non-power-of-two ==== + elapsed time: 0.286272ms (CUDA Measured) + passed +==== work-efficient-hierarchical compact, power-of-two ==== + elapsed time: 0.350208ms (CUDA Measured) + passed +==== work-efficient-hierarchical compact, non-power-of-two ==== + elapsed time: 0.348096ms (CUDA Measured) + passed + +*************************** +** SORT TESTS 0001048576 ** +*************************** + [ 25708 19375 9028 5662 11186 20417 26534 15472 11928 29886 16862 23409 11471 ... 17877 0 ] +==== cpu sort, power-of-two ==== + elapsed time: 58.3795ms (std::chrono Measured) + [ 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 32767 32767 ] +==== thrust sort, power-of-two ==== + elapsed time: 0.516096ms (CUDA Measured) + passed +==== work-efficient sort, power-of-two ==== + elapsed time: 13.355ms (CUDA Measured) + passed +==== cpu sort, non-power-of-two ==== + elapsed time: 67.2949ms (std::chrono Measured) + [ 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 32767 32767 ] +==== thrust sort, non-power-of-two ==== + elapsed time: 0.570944ms (CUDA Measured) + passed +==== work-efficient sort, non-power-of-two ==== + elapsed time: 13.0683ms (CUDA Measured) + passed +``` + + + +## Performance Analysis + +From the test output above, we can see that the CPU scan appears similar performance with the "Naïve" GPU scan. The "Work-Efficient" scan is faster, and the "Work-Efficient-Hierarchical" scan is much faster, while it is still a bit slower than Thrust's implementation (Which is faster depends on the number of elements. You will see the result in further analysis). The similar phenomena are shown about stream compaction and sorting. + +To get further analysis, I ran 1000 group of tests and compute the average duration of each algorithm. For each test, I chose 2^n elements for n from 12 to 24 with stride of 2. The tests with non-power-of-two number took 2^n-3 elements. + + + +### Analysis for Scan + +Comparisons of duration of scan with power-of-two elements are shown first. + +![ScanPOT(1)](img/readme/DurationScanPOT1.png) + +![ScanPOT(2)](img/readme/DurationScanPOT2.png) + + + +From the plot we can find that when the number of elements is smaller than 65536, CPU scan performs the best, maybe because the computing power of one CPU core is better than the computing power of one GPU core, and CPU can take advantage of cache when the number is small. And when the number is not greater than 1048576, the "Work-Efficient-Hierarchical" implementation is faster than the Thrust's implementation. When the number is very large, the Thrust's implementation performs the best, but we can see that the "Work-Efficient-Hierarchical" appears performance improvement compared to the "Work-Efficient" in all situations. One of the interesting thing is, the "Naïve" implementation is almost worse than the implementation on CPU. The consequence is similar with non-power-of-two elements. + +If we take a look at what happened when we call `thrust::exclusive_scan` by `Nsight`, there are two kernel functions called: `DeviceScanInitKernel` and `DeviceScanKernel`. What make me surprised is that there are only 547 blocks with 128 threads in `DeviceScanKernel`, and the number is even smaller in `DeviceScanInitKernel`. Also, these two functions only span about 30 microseconds, but `cudaEvent` tells us the whole process needs about 200 microseconds. Maybe there are some CUDA operations other than kernel functions and I find that `cudaMemcpyAsync` was called. I don't know what magic is in it. + +![Thrust's Scan](img/readme/DeviceScanKernel.png) + + + +### Analysis for Stream Compaction + +Comparisons of duration of stream compaction with power-of-two elements are shown first. + +![CompactionPOT(1)](img/readme/DurationCompactPOT1.png) + +![CompactionPOT(2)](img/readme/DurationCompactPOT2.png) + +Because the stream compaction algorithm is an application of scan, something is similar to the scan algorithm that, in very small number, CPU runs best. In middle number, `Work-Efficient-Hierarchical` runs the best. In very large number, `thrust::remove_if` runs the best. Another discover is that `CPU-with-Scan` is faster than `CPU-without-Scan`. It seems to do more operations with scan, but maybe because it take advantage of cache while scanning in place. The consequence is similar with non-power-of-two elements. + + + +Also, I am really surprised at what happened with `thrust::remove_if`. Much fewer threads than elements as well. The `cudaMemcpyAsync` was called too. + +![Thrust's Scan](img/readme/DeviceRemoveIf.png) + + + +### Analysis for Sort + +Comparisons of duration of sorting with power-of-two elements are shown first. I use the quick sort in standard library with CPU's implementation. + +![SortPOT(1)](img/readme/DurationSortPOT1.png) + +![SortPOT(2)](img/readme/DurationSortPOT2.png) + + + +The GPU's implementations perform better than CPU's implementations with numbers larger than or equal to 65536, although my `Work-Efficient-Hierarchical` is much slower than `thrust::sort`. The `thrust::sort` is faster than the CPU's quick sort even with a small number, which is different from scan and stream compaction. + +My implementation is sweeping the 31 bits of the integer naively (Suppose all the elements are non-negative so that the highest bit is always 0). The Thrust's implementation may be more complicated. Take a look at the kernel functions. There are `DeviceRadixSortHistogramKernel`, `DeviceRadixSortExclusiveSumKernel`, followed by four `DeviceRadixSortOnesweepKernel`. Also, there are several `cudaMemsetAsync` among them (Async again!). The `DeviceRadixSortExclusiveSumKernel` ran in really short time, which makes me curious about what it really does. As far as I am concerned, the `DeiviceRadixSortHistogramKernel` may be critical to avoid sweeping all bits of the integer. + +![DeviceRadixSort](img/readme/DeviceRadixSort.png) + + + +- All the raw data for performance analysis can be found in the `profile` folder. diff --git a/img/readme/DeviceRadixSort.png b/img/readme/DeviceRadixSort.png new file mode 100644 index 0000000000000000000000000000000000000000..04875339a9433d6e66d0913585d62c159b2ec065 GIT binary patch literal 52244 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 WIPI5wKkzIl=WxXFF!2|U>;D7!3Z%#Y literal 0 HcmV?d00001 diff --git a/img/readme/DeviceRemoveIf.png b/img/readme/DeviceRemoveIf.png new file mode 100644 index 0000000000000000000000000000000000000000..9fbdd5095095eb83be98da4949bcd527dee4aa13 GIT binary patch literal 60903 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 z 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℞f@ab+1% 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%0AQd=sPvd6? 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 literal 0 HcmV?d00001 diff --git a/img/readme/DeviceScanKernel.png b/img/readme/DeviceScanKernel.png new file mode 100644 index 0000000000000000000000000000000000000000..c93bde8c966f556f2d059095cdc2dae4cd12f558 GIT binary patch literal 58231 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