# Changes

## Algo holics

, 02:00, 8 April 2019
Kernel Version 2
{| class="wikitable mw-collapsible mw-collapsed"
! Flat profileCall Graph
|-
|
{| class="wikitable mw-collapsible mw-collapsed"
! Flat profileCall Graph
|-
|
=== Assignment 2 ===
After carefully weighing the projects from assignment 1, we first decided Our initial idea was to parallelize use the neural networkcode for our assignment 2. However, on further inspection of But since the source code, it algorithm itself was found that the network had a not very low accuracy accurate (2/10 correct predictions even after ten thousand iteration10,000 training iterations), we decided to paralellize merge sort. To improve the algorithm first and then parallelize did Soon we realized that since its Big O classification was n log n, offloading computations to GPU would not sound very pragmaticbe that effective. So , we decided settled with the next most interesting topic for the groupcosine transform library, as described below. ====Cosine Tranformation==== The Cosine_Transform is a simple C++ library which was merge sortdemonstrates properties of the Discrete cosine Transform for real data. Here The Discrete Cosine Transform or DCT is how the code was changed used to its parallel versioncreate jpeg (compressed images).
The formula used here is: | (√1/n) , if u=0; 0≤v≤n-1 C(u,v) ===CUDA enabled functions==== The main function was changed to perform the copying of data from host to device, launch the kernel, copy back results from the device to host and release all memory | (√2/n) * cos[((2*v+1)π*u)/2n], on host and device. if 1≤u≤n-1; 0≤v≤n-1
}
float end_time r = clocknew double[n]; for (i = 0; i < n; i++ ) - start_time;{ k = seed //copy back the device array to host array127773; cudaMemcpy seed = 16807 * (h_C, d_actual, size seed - k * 127773 ) - k * sizeof2836; if (longseed < 0 ), cudaMemcpyDeviceToHost{ seed = seed + i4_huge; } r[i] = ( double )( seed ) * 4.656612875E-10; } return r; }
The serial code did not use recusion double *cosine_transform_data ( int n, which is usually utilized double d[] ){ double angle; double *c; int i; int j; c = new double[n]; for merge sort( i = 0; i < n; i++ ){ c[i] = 0. Instead, it did a bottoms-up merge, wherein the array was sliced into pieces and each piece was sorted. The width of each slice changed with each iteration0; for ( j = 0; j < n; j++ ){ angle = pi * ( double ) ( i * ( 2 * j + 1 ) ) / ( double ) ( 2 * n ); c[i] = c[i] + cos ( angle ) * d[j]; } c[i] = c[i] * sqrt ( 2. This was done using two functions int the serial code: merge0 / (...double ) and merge_sort(...n ) ). The former function was responsible for slicing the array into different widths. The latter received this slices and sorted them by creating a local STL map. ; } return c;In the CUDA code for this, it seemed advisable to make merge_sort as the kernel. This can be justified because each thread now gets a slice of the array to sort, which means more work is done concurrently than before. Also, this enabled removal of the two nested for-loops from the serial code. Here is how the new merge_sort function looks like: }
__global__ void merge_sortreportTime(long const char*sourceAmsg, long *destA, long size, long width, long slices, dim3 *threads, dim3 *blockssteady_clock::duration span) { int index auto ms = threadIdx.x + blockIdx.x * blockDim.x; //may need to change this int beg = width * index * slices, middle, end; for (long slice = 0; slice duration_cast< slices; slice++) { //basically removed the two for loops if milliseconds>(beg < size) { middle = min(beg + (width >> 1), sizespan); end = min(beg + width, size); std::cout << msg << " - took - " << merge ms.count(sourceA, destA, beg, middle, end)<< " millisecs" << std::endl; beg += width; } }
}
As previously __global__ void cosTransformKernel(double *a, the merge_sort(double *b, int n) function calls the merge function{ double angle; const double pi = 3.141592653589793; int i = blockIdx.x * blockDim. In the serial code, an STL map is utilized to store the sliced array's values that are later used as values to be assigned to the original array based on the index comparison or value comparisonx + threadIdx.x; for(int j=0; j<n; j++){ angle = pi * (double) (i*(2*j+1)) / (double)(2*n);Now, the merge function is strictly b[i] += cos ( angle ) * a device function that is called from the kernel for each slice to be sorted[j]; } b[i] *= sqrt( 2.0 / (double) n ); }
__device__ void mergeint main (long *sourceAint argc, long char*destA, long b, long c, long eargv[] ) { long i1 = b; long i2 = c; for if (long k argc != b; k < e; k++2) { if (i1 std::cerr << c && (i2 >= e || sourceAargv[i10] < sourceA[i2])) {< ": invalid number of arguments\n"; destA std::cerr << "Usage: " << argv[k] = sourceA[i10]<< " size_of_vector\n"; i1++ return 1; } else { destA[k] int n = sourceAstd::atoi(argv[i21]); i2++ cosine_transform_test01 (n); } } return 0;
}
void cosine_transform_test01 ( int size){ int n =size; int seed; double *r; double *hs; double *s =new double[n]; double *d_a; double *d_b; //allocate memory on the device for the randomly generated array and for the array in which transform values will be stored cudaMalloc((void**)&d_a,sizeof(double) * n); cudaMalloc((void**)&d_b,sizeof(double) * n); seed =123456789; r =Profiling resultsr8vec_uniform_01_new ( n, seed ); //copy randomly generated values from host to device cudaMemcpy(d_a,r,sizeof(double)*n,cudaMemcpyHostToDevice); int nblks =(n + ntpb - 1) / ntpb; steady_clock::time_point ts, te; ts =steady_clock::now(); cosTransformKernel<<<nblks,ntpb>>>(d_a,d_b,size); cudaDeviceSynchronize(); te =steady_clock::now(); reportTime("Cosine Transform on device",te-ts); cudaMemcpy(s,d_b,sizeof(double)*n,cudaMemcpyDeviceToHost); ts =steady_clock::now(); hs = cosine_transform_data ( n, r ); te = steady_clock::now(); reportTime("Cosine Transform on host",te-ts);  cudaFree(d_a); cudaFree(d_b); delete [] r; delete [] s; delete [] hs;}  |}
[[FileThe graph for the execution time difference between the device and the host looks like:serialvParallel.jpg|750px]]
There is [[File:kernel1.png]] Even though the kernel includes a stark difference between both versionsfor-loop the execution time has decreased drastically. The parallel code outperforms our expectations and seems very efficientThats because each thread is now responsible for one calculating one element of the final Cos transformed matrix(unit vector).
=== Assignment 3 ===

For optimizing the code better, we thought of removing the iterative loop from the kernel by using threadIdx.y to control calculation of each element's cosine for that position in the supposed matrix. The problem in this was that each thread was in a racing condition to write to the same memory location, to sum up the cosine transformations for all elements of that row. We solved this by using the atomic function. Its prototype is as follows.

=====Kernel Version 2=====

{| class="wikitable mw-collapsible mw-collapsed"
! Kernel 2
|-
|
# include <cmath>
# include <cstdlib>
# include <iostream>
# include <iomanip>
# include <ctime>
# include <chrono>
# include <cstdlib>
# include <cmath>
#include <limits>
#include <cuda_runtime.h>
#include <cuda.h>
using namespace std;
using namespace std::chrono;
const double pi = 3.141592653589793;
const unsigned ntpb = 32;
void cosine_transform_test01 ( int size );

double *r8vec_uniform_01_new ( int n, int &seed ){
int i;
const int i4_huge = 2147483647;
int k;
double *r;
if ( seed == 0 ){
cerr << "\n";
cerr << "R8VEC_UNIFORM_01_NEW - Fatal error!\n";
cerr << " Input value of SEED = 0.\n";
exit ( 1 );
}
r = new double[n];
for ( i = 0; i < n; i++ ){
k = seed / 127773;
seed = 16807 * ( seed - k * 127773 ) - k * 2836;
if ( seed < 0 ){
seed = seed + i4_huge;
}
r[i] = ( double ) ( seed ) * 4.656612875E-10;
}
return r;
}

double *cosine_transform_data ( int n, double d[] ){
double angle;
double *c;
int i;
int j;
c = new double[n];
for ( i = 0; i < n; i++ ){
c[i] = 0.0;
for ( j = 0; j < n; j++ ){
angle = pi * ( double ) ( i * ( 2 * j + 1 ) ) / ( double ) ( 2 * n );
c[i] = c[i] + cos ( angle ) * d[j];
}
c[i] = c[i] * sqrt ( 2.0 / ( double ) ( n ) );
}
return c;
}

void reportTime(const char* msg, steady_clock::duration span) {
auto ms = duration_cast<milliseconds>(span);
std::cout << msg << " - took - " <<
ms.count() << " millisecs" << std::endl;
}

__global__ void cosTransformKernel(double *a, double *b, const int n){
double angle;
const double pi = 3.141592653589793;
int j = blockIdx.x * blockDim.x + threadIdx.x;
int i = blockIdx.y * blockDim.y + threadIdx.y;
if(i<n && j<n){
angle = pi * ( double ) ( i * ( 2 * j + 1 ) ) / ( double ) ( 2 * n );
double value = cos ( angle ) * a[j];
}
//square root of the whole cos transformed row term
if(j==n-1 && i<n){
b[i] *= sqrt ( 2.0 / ( double ) ( n ) );
}
}

int main (int argc, char* argv[] ){
if (argc != 2) {
std::cerr << argv[0] << ": invalid number of arguments\n";
std::cerr << "Usage: " << argv[0] << " size_of_vector\n";
return 1;
}
int n = std::atoi(argv[1]);
cosine_transform_test01 (n);
return 0;
}

void cosine_transform_test01 ( int size){
int n = size;
int seed;
double *r;
double *hs; //host side pointer to store the array returned from host side cosine_transform_data, for comparison purposes
double *s = new double[n];
//double *t;
double *d_a;
double *d_b;
//allocate memory on the device for the randomly generated array and for the array in which transform values will be stored
cudaMalloc((void**)&d_a,sizeof(double) * n);
cudaMalloc((void**)&d_b,sizeof(double) * n);
seed = 123456789;
r = r8vec_uniform_01_new ( n, seed );
//copy randomly generated values from host to device
for(int i=0; i<n; i++)
s[i]=0.0;
cudaMemcpy(d_a,r,sizeof(double)*n,cudaMemcpyHostToDevice);
cudaMemcpy(d_b,s,sizeof(double)*n,cudaMemcpyHostToDevice);
int nblks = (n + ntpb - 1) / ntpb;
dim3 grid(nblks,nblks,1);
dim3 block(ntpb,ntpb,1);
cosTransformKernel<<<grid,block>>>(d_a,d_b,size);
reportTime("Cosine Transform on device",te-ts);
cudaMemcpy(s,d_b,sizeof(double)*n,cudaMemcpyDeviceToHost);
hs = cosine_transform_data ( n, r );
reportTime("Cosine Transform on host",te-ts);

cudaFree(d_a);
cudaFree(d_b);
delete [] r;
delete [] s;
delete [] hs;
//delete [] t;

return;
}

|}

Here is a comparison between the naive and optimized kernel

[[File:kernel2.jpg]]

Evidently, there is some performance boost for the new version. However, each call to atomicAdd by a thread locks the global memory until the old value is read and added to the passed value. This deters faster execution as might be expected.
57
edits