From 7e6f37d51fdcd4c5130b3261b0de513af3d1d50a Mon Sep 17 00:00:00 2001 From: David Goz <david.goz@inaf.it> Date: Fri, 2 Aug 2024 10:57:53 +0200 Subject: [PATCH] omp block matrix --- cuda-omp/omp/7/mat_mult.c | 185 ++++++++++++++++ cuda-omp/omp/7/mat_mult_block.c | 208 ++++++++++++++++++ cuda-omp/omp/miscellaneous/structure.c | 59 ++--- .../not_opt/jacobi_serial_not_opt_len14 | Bin 0 -> 17416 bytes 4 files changed, 427 insertions(+), 25 deletions(-) create mode 100644 cuda-omp/omp/7/mat_mult.c create mode 100644 cuda-omp/omp/7/mat_mult_block.c create mode 100755 jacobi/serial/not_opt/jacobi_serial_not_opt_len14 diff --git a/cuda-omp/omp/7/mat_mult.c b/cuda-omp/omp/7/mat_mult.c new file mode 100644 index 0000000..8ad7fcd --- /dev/null +++ b/cuda-omp/omp/7/mat_mult.c @@ -0,0 +1,185 @@ +//////////////////////////////////////////////////////////////////////////////////////////////// +// - Naive matrix multiplication algorithm +// for (size_t i=0 ; i<N ; i++) +// for (size_t j=0 ; j<N ; j++) +// for (size_t k=0 ; k<_N ; k++) +// C[(i * N) + j] += A[(i * N) + k] * B[(k * N) + j]; +// +//////////////////////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Author: David Goz +// mail : david.goz@inaf.it +// date : 31.07.2024 +// code tested using nvhpc +// +// - Compile the code: +// $ nvc -mp=gpu -gpu=ccnative,debug,lineinfo -target=gpu -Minfo=all -v classwork.c -o classwork_omp +// - Run the code: +// $ ./classwork_omp +////////////////////////////////////////////////////////////////////////////////////////////////// + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> +#include <assert.h> +#include <omp.h> +#include <string.h> + +#define N 512 +#define SIZE (N * N) // matrix size +typedef double MyData; // do not change + +#define LOOP 100 +#define NDEBUG + +double wall_time() +{ + struct timespec ts; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts); + const double ret = (double) (ts.tv_sec) + (double) ts.tv_nsec * 1.0e-9; + + return ret; +} + +void CPU_mat_mult(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + for (size_t i=0 ; i<size ; i++) + for (size_t j=0 ; j<size ; j++) + for (size_t k=0 ; k<size ; k++) + C[(i * size) + j] += (A[(i * size) + k] * B[(k * size) + j]); + + return; +} + +void GPU_mat_mult(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + #pragma omp target + { + #pragma omp teams distribute num_teams(size) + for (size_t i=0 ; i<size ; i++) + { + #pragma omp parallel for num_threads(size) + for (size_t j=0 ; j<size ; j++) + { + MyData value = (MyData)0; + for (size_t k=0 ; k<size ; k++) + value += (A[(i * size) + k] * B[(k * size) + j]); + + C[(i * size) + j] = value; + } // omp thread + } // omp teams + } // omp target + + return; +} + +void GPU_mat_mult_no_loops(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + #pragma omp target + { + #pragma omp teams num_teams(size) + { + const size_t team_size = (size * omp_get_team_num()); + + #pragma omp parallel firstprivate(team_size) num_threads(size) + { + const size_t tid = omp_get_thread_num(); + MyData value = (MyData)0; + for (size_t k=0 ; k<size ; k++) + value += (A[team_size + k] * B[(k * size) + tid]); + + C[team_size + tid] = value; + } // omp threads + } // omp teams + } // omp target + + return; +} + +void check(const MyData *const __restrict__ cpu_matrix, + const MyData *const __restrict__ gpu_matrix) +{ + int flag; + for (size_t i=0 ; i<SIZE ; i++) + flag = ((cpu_matrix[i] != gpu_matrix[i]) ? 1 : 0); + + if (!flag) + printf("\n\t Result OK"); + else + printf("\n\t Result wrong"); + + return; +} + +int main() +{ + double time; + MyData *buffer = (MyData *)calloc(4 * SIZE, sizeof(MyData)); + assert(buffer != NULL); + + // host reference matrix A + MyData *const restrict A = buffer; + MyData *const restrict B = A + SIZE; + MyData *const restrict C_CPU = B + SIZE; + MyData *const restrict C_GPU = C_CPU + SIZE; + for (size_t i=0 ; i<SIZE ; i++) + { + A[i] = drand48(); + B[i] = drand48(); + } + + ////////////////////////// CPU naive algorithm ////////////////////////////////////////// + CPU_mat_mult(A, B, C_CPU, N); + ///////////////////////////////////////////////////////////////////////////////////////// + + // copy/alloc data to the GPU + #pragma omp target enter data map(to: A[0:SIZE], B[0:SIZE]) map(alloc: C_GPU[0:SIZE]) + + /////////////////////////// GPU naive algorithm //////////////////////////////////////// + time = 0.0; + for (unsigned short int loop=0 ; loop<LOOP ; loop++) + { + const double start = wall_time(); + GPU_mat_mult(A, B, C_GPU, N); + time += (wall_time() - start); + } + + #pragma omp target update from(C_GPU[0:SIZE]) + check(C_CPU, C_GPU); + printf("\n\t GPU naive time %lg [s]\n", (time / LOOP)); + //////////////////////////////////////////////////////////////////////////////// + + /////////////////////////// GPU naive no loops algorithm //////////////////////////// + time = 0.0; + for (unsigned short int loop=0 ; loop<LOOP ; loop++) + { + const double start = wall_time(); + GPU_mat_mult_no_loops(A, B, C_GPU, N); + time += (wall_time() - start); + } + + #pragma omp target update from(C_GPU[0:SIZE]) + check(C_CPU, C_GPU); + printf("\n\t GPU naive no loops time %lg [s]\n", (time / LOOP)); + //////////////////////////////////////////////////////////////////////////////// + + // free CPU memory + free(buffer); + // free GPU memory + #pragma omp target exit data map(delete: A[0:SIZE], B[0:SIZE], C_CPU[0:SIZE]) + + printf("\n"); + + return EXIT_SUCCESS; +} diff --git a/cuda-omp/omp/7/mat_mult_block.c b/cuda-omp/omp/7/mat_mult_block.c new file mode 100644 index 0000000..cd84ad1 --- /dev/null +++ b/cuda-omp/omp/7/mat_mult_block.c @@ -0,0 +1,208 @@ +//////////////////////////////////////////////////////////////////////////////////////////////// +// - Block matrix multiplication algorithm +// +// const size_t Nblocks = (N / Bsize); +// +// // loop over blocks of matrix C +// for (size_t ib=0 ; ib<Nblocks ; ib++) +// { +// for (size_t jb=0 ; jb<Nblocks ; jb++) +// { +// +// // loop over blocks of rows of A +// for (size_t kb=0 ; kb<Nblocks ; kb++) +// +// +// +// for (size_t i=0 ; i<N ; i++) +// for (size_t j=0 ; j<N ; j++) +// for (size_t k=0 ; k<_N ; k++) +// C[(i * N) + j] += A[(i * N) + k] * B[(k * N) + j]; +// +// } // jb +// } // ib +// - Exploit shared-memory. +//////////////////////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Author: David Goz +// mail : david.goz@inaf.it +// date : 31.07.2024 +// code tested using nvhpc +// +// - Compile the code: +// $ nvc -mp=gpu -gpu=ccnative,debug,lineinfo -target=gpu -Minfo=all -v classwork.c -o classwork_omp +// - Run the code: +// $ ./classwork_omp +////////////////////////////////////////////////////////////////////////////////////////////////// + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> +#include <assert.h> +#include <omp.h> +#include <string.h> + +#define N 512 +#define SIZE (N * N) // matrix size +typedef double MyData; // do not change +#define BLOCKSIZE 32 // number of threads per block + +// sanity check +#if BLOCKSIZE > 1024 +#error BLOCKSIZE cannot be larger than 1024 +#endif + +#define LOOP 100 +#define NDEBUG + +double wall_time() +{ + struct timespec ts; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts); + const double ret = (double) (ts.tv_sec) + (double) ts.tv_nsec * 1.0e-9; + + return ret; +} + +void CPU_mat_mult(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + for (size_t i=0 ; i<size ; i++) + for (size_t j=0 ; j<size ; j++) + for (size_t k=0 ; k<size ; k++) + C[(i * size) + j] += (A[(i * size) + k] * B[(k * size) + j]); + + return; +} + +void GPU_mat_mult(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + #pragma omp target + { + #pragma omp teams distribute num_teams(size) + for (size_t i=0 ; i<size ; i++) + { + #pragma omp parallel for num_threads(size) + for (size_t j=0 ; j<size ; j++) + { + MyData value = (MyData)0; + for (size_t k=0 ; k<size ; k++) + value += (A[(i * size) + k] * B[(k * size) + j]); + + C[(i * size) + j] = value; + } // omp thread + } // omp teams + } // omp target + + return; +} + +void GPU_mat_mult_no_loops(const MyData *const restrict A, + const MyData *const restrict B, + MyData *const restrict C, + const size_t size) +{ + #pragma omp target + { + #pragma omp teams num_teams(size) + { + const size_t team_size = (size * omp_get_team_num()); + + #pragma omp parallel firstprivate(team_size) num_threads(size) + { + const size_t tid = omp_get_thread_num(); + MyData value = (MyData)0; + for (size_t k=0 ; k<size ; k++) + value += (A[team_size + k] * B[(k * size) + tid]); + + C[team_size + tid] = value; + } // omp threads + } // omp teams + } // omp target + + return; +} + +void check(const MyData *const __restrict__ cpu_matrix, + const MyData *const __restrict__ gpu_matrix) +{ + int flag; + for (size_t i=0 ; i<SIZE ; i++) + flag = ((cpu_matrix[i] != gpu_matrix[i]) ? 1 : 0); + + if (!flag) + printf("\n\t Result OK"); + else + printf("\n\t Result wrong"); + + return; +} + +int main() +{ + double time; + MyData *buffer = (MyData *)calloc(4 * SIZE, sizeof(MyData)); + assert(buffer != NULL); + + // host reference matrix A + MyData *const restrict A = buffer; + MyData *const restrict B = A + SIZE; + MyData *const restrict C_CPU = B + SIZE; + MyData *const restrict C_GPU = C_CPU + SIZE; + for (size_t i=0 ; i<SIZE ; i++) + { + A[i] = drand48(); + B[i] = drand48(); + } + + ////////////////////////// CPU naive algorithm ////////////////////////////////////////// + CPU_mat_mult(A, B, C_CPU, N); + ///////////////////////////////////////////////////////////////////////////////////////// + + // copy/alloc data to the GPU + #pragma omp target enter data map(to: A[0:SIZE], B[0:SIZE]) map(alloc: C_GPU[0:SIZE]) + + /////////////////////////// GPU naive algorithm //////////////////////////////////////// + time = 0.0; + for (unsigned short int loop=0 ; loop<LOOP ; loop++) + { + const double start = wall_time(); + GPU_mat_mult(A, B, C_GPU, N); + time += (wall_time() - start); + } + + #pragma omp target update from(C_GPU[0:SIZE]) + check(C_CPU, C_GPU); + printf("\n\t GPU naive time %lg [s]\n", (time / LOOP)); + //////////////////////////////////////////////////////////////////////////////// + + /////////////////////////// GPU naive no loops algorithm //////////////////////////// + time = 0.0; + for (unsigned short int loop=0 ; loop<LOOP ; loop++) + { + const double start = wall_time(); + GPU_mat_mult_no_loops(A, B, C_GPU, N); + time += (wall_time() - start); + } + + #pragma omp target update from(C_GPU[0:SIZE]) + check(C_CPU, C_GPU); + printf("\n\t GPU naive no loops time %lg [s]\n", (time / LOOP)); + //////////////////////////////////////////////////////////////////////////////// + + // free CPU memory + free(buffer); + // free GPU memory + #pragma omp target exit data map(delete: A[0:SIZE], B[0:SIZE], C_CPU[0:SIZE]) + + printf("\n"); + + return EXIT_SUCCESS; +} diff --git a/cuda-omp/omp/miscellaneous/structure.c b/cuda-omp/omp/miscellaneous/structure.c index c12b212..fc0041c 100644 --- a/cuda-omp/omp/miscellaneous/structure.c +++ b/cuda-omp/omp/miscellaneous/structure.c @@ -17,7 +17,8 @@ #include <assert.h> #include <omp.h> -#define N 8 +#define SIZE 8 +#define SIZE_2 (SIZE / 2) typedef double MyData; typedef struct my_span @@ -29,26 +30,20 @@ typedef struct my_span void allocate( span *my_struct, const size_t size) { - span tmp; /* allocate the buffer on the host memory */ - tmp.ptr = (MyData *)malloc(size * sizeof(MyData)); - assert(tmp.ptr != NULL); - tmp.N = size; - - /* declare how the object 'span' has to be mapped on the device */ - #pragma omp declare mapper(span tmp) map(from: tmp, tmp.ptr[0: tmp.N]) - - my_struct->ptr = tmp.ptr; - my_struct->N = tmp.N; + my_struct->ptr = (MyData *)calloc(size, sizeof(MyData)); + assert(my_struct->ptr != NULL); + my_struct->N = size; return; } -void print(const span *const A) +void print(const span *const A, + const char *const string) { printf("\n"); - for (size_t i=0 ; i<A->N ; i++) - printf("\n\t array[%i] = %lg", i, A->ptr[i]); + for (int i=0 ; i<A->N ; i++) + printf("\n\t %s[%d] = %lg", string, i, A->ptr[i]); printf("\n\n"); return; @@ -56,27 +51,41 @@ void print(const span *const A) int main() { - span A, B; + span A, B, C; - allocate(&A, N); - allocate(&B, N); + allocate(&A, SIZE); + allocate(&B, SIZE); + allocate(&C, SIZE); + + /* declare how the object 'span' has to be mapped on the device */ + #pragma omp declare mapper(all : span S) map(to: S) map(from: S.ptr[0 : S.N]) + #pragma omp declare mapper(left : span S) map(to: S) map(from: S.ptr[0 : S.N/2]) + #pragma omp declare mapper(right: span S) map(to: S) map(from: S.ptr[S.N/2: S.N/2]) /* init on the GPU */ - #pragma omp target +#pragma omp target map(mapper(all): A) map(mapper(left): B) map(mapper(right): C) { - for (size_t i=0 ; i<N ; i++) - { - A.ptr[i] = (MyData)(i); - B.ptr[i] = (MyData)(2 * i); - } + #pragma omp loop + for (size_t i=0 ; i<SIZE ; i++) + A.ptr[i] = (MyData)(i); + + #pragma omp loop + for (size_t ii=0 ; ii<SIZE_2 ; ii++) + B.ptr[ii] = (MyData)(ii); + + #pragma omp loop + for (size_t iii=SIZE_2 ; iii<SIZE ; iii++) + C.ptr[iii] = (MyData)(iii); } - print(&A); - print(&B); + print(&A, "A"); + print(&B, "B"); + print(&C, "C"); /* free the host's memory */ free(A.ptr); free(B.ptr); + free(C.ptr); return 0; } diff --git a/jacobi/serial/not_opt/jacobi_serial_not_opt_len14 b/jacobi/serial/not_opt/jacobi_serial_not_opt_len14 new file mode 100755 index 0000000000000000000000000000000000000000..f546329f4a6d492f2c2a4495e5a65dbeb9ea452d GIT binary patch literal 17416 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 E02<39ApigX literal 0 HcmV?d00001 -- GitLab