2016-12-13 2 views
0

Я пытаюсь оптимизировать некоторый контрольный код умножения матричной матрицы, который использует OpenMP для процессора MAESTRO. MAESTRO имеет 49 процессоров, расположенных в двумерном массиве в конфигурации 7x7. Каждое ядро ​​имеет свой собственный кеш L1 и L2. Расположение платы можно посмотреть здесь: http://i.imgur.com/naCWTuK.png.Производительность OpenMP для разных типов данных в архитектуре NUMA

Мой главный вопрос: могут ли различные типы данных (char vs short vs int и т. Д.) Напрямую влиять на производительность кода OpenMP на процессорах на базе NUMA? Если да, есть ли способ облегчить его? Ниже приводится мое объяснение, почему я спрашиваю об этом.

Мне дали набор тестов, которые были использованы исследовательской группой для измерения производительности данного процессора. Эти тесты привели к повышению производительности для других процессоров, но они столкнулись с проблемой не видеть того же типа результатов при запуске на MAESTRO. Вот отрывок из матричного умножения бенчмарка из базового кода, который я получил:

Соответствующие макросы из файла заголовка (MAESTRO это 64-разрядная версия):

#include <stdio.h> 
#include <stdlib.h> 
#include <math.h> 
#include <time.h> 
#include <sys/time.h> 
#include <cblas.h> 
#include <omp.h> 

//set data types 
#ifdef ARCH64 
    //64-bit architectures 
    #define INT8_TYPE char 
    #define INT16_TYPE short 
    #define INT32_TYPE int 
    #define INT64_TYPE long 
#else 
    //32-bit architectures 
    #define INT8_TYPE char 
    #define INT16_TYPE short 
    #define INT32_TYPE long 
    #define INT64_TYPE long long 
#endif 
#define SPFP_TYPE float 
#define DPFP_TYPE double 

//setup timer 

//us resolution 
#define TIME_STRUCT struct timeval 
#define TIME_GET(time) gettimeofday((time),NULL) 
#define TIME_DOUBLE(time) (time).tv_sec+1E-6*(time).tv_usec 
#define TIME_RUNTIME(start,end) TIME_DOUBLE(end)-TIME_DOUBLE(start) 

//select random seed method 
#ifdef FIXED_SEED 
    //fixed 
    #define SEED 376134299 
#else 
    //based on system time 
    #define SEED time(NULL) 
#endif 

32-разрядное целое число матрицы умножения тест:

double matrix_matrix_mult_int32(int size,int threads) 
{ 


//initialize index variables, random number generator, and timer 
    int i,j,k; 
    srand(SEED); 
    TIME_STRUCT start,end; 

//allocate memory for matrices 
INT32_TYPE *A=malloc(sizeof(INT32_TYPE)*(size*size)); 
INT32_TYPE *B=malloc(sizeof(INT32_TYPE)*(size*size)); 
INT64_TYPE *C=malloc(sizeof(INT64_TYPE)*(size*size)); 

//initialize input matrices to random numbers 
//initialize output matrix to zeros 
for(i=0;i<(size*size);i++) 
{ 
    A[i]=rand(); 
    B[i]=rand(); 
    C[i]=0; 
} 

//serial operation 
if(threads==1) 
{ 
    //start timer 
    TIME_GET(&start); 
    //computation 
    for(i=0;i<size;i++) 
    { 
     for(k=0;k<size;k++) 
     { 
      for(j=0;j<size;j++) 
      { 
       C[i*size+j]+=A[i*size+k]*B[k*size+j]; 
      } 
     } 
    } 
    //end timer 
    TIME_GET(&end); 
} 
//parallel operation 
else 
{ 
    //start timer 
    TIME_GET(&start); 
    //parallelize with OpenMP 
    #pragma omp parallel for num_threads(threads) private(i,j,k) 
    for(i=0;i<size;i++) 
    { 
     for(k=0;k<size;k++) 
     { 
      for(j=0;j<size;j++) 
      { 
       C[i*size+j]+=A[i*size+k]*B[k*size+j]; 
      } 
     } 
    } 
    //end timer 
    TIME_GET(&end); 
} 

//free memory 
free(C); 
free(B); 
free(A); 

//compute and return runtime 
return TIME_RUNTIME(start,end); 
} 

Выполнение вышеуказанного теста последовательно приводило к лучшей производительности, чем работа с OpenMP. Мне было поручено оптимизировать бенчмарк для MAESTRO, чтобы получить лучшую производительность. Используя следующий код, я был в состоянии получить прирост производительности:

double matrix_matrix_mult_int32(int size,int threads) 
{ 

//initialize index variables, random number generator, and timer 
    int i,j,k; 
    srand(SEED); 
    TIME_STRUCT start,end; 


    //allocate memory for matrices 
    alloc_attr_t attrA = ALLOC_INIT; 
    alloc_attr_t attrB = ALLOC_INIT; 
    alloc_attr_t attrC = ALLOC_INIT; 

    alloc_set_home(&attrA, ALLOC_HOME_INCOHERENT); 
    alloc_set_home(&attrB, ALLOC_HOME_INCOHERENT); 
    alloc_set_home(&attrC, ALLOC_HOME_TASK); 

    INT32_TYPE *A=alloc_map(&attrA, sizeof(INT32_TYPE)*(size*size)); 
    INT32_TYPE *B=alloc_map(&attrB, sizeof(INT32_TYPE)*(size*size)); 
    INT64_TYPE *C=alloc_map(&attrC, sizeof(INT64_TYPE)*(size*size)); 

    #pragma omp parallel for num_threads(threads) private(i) 
    for(i=0;i<(size*size);i++) 
    { 

     A[i] = rand(); 
     B[i] = rand(); 
     C[i] = 0; 
     tmc_mem_flush(&A[i], sizeof(A[i])); 
     tmc_mem_flush(&B[i], sizeof(B[i])); 
     tmc_mem_inv(&A[i], sizeof(A[i])); 
     tmc_mem_inv(&B[i], sizeof(B[i])); 
    } 


    //serial operation 
    if(threads==1) 
    { 
     //start timer 
     TIME_GET(&start); 

     //computation 
     for(i=0;i<size;i++) 
     { 
      for(k=0;k<size;k++) 
      { 
       for(j=0;j<size;j++) 
       { 
        C[i*size+j]+=A[i*size+k]*B[k*size+j]; 
       } 
      } 
     } 

    TIME_GET(&end); 

    } 
    else 
    { 

     TIME_GET(&start); 

     #pragma omp parallel for num_threads(threads) private(i,j,k) schedule(dynamic) 
     for(i=0;i<size;i++) 
     { 
      for(j=0;j<size;j++) 
      { 
       for(k=0;k<size;k++) 
       { 
        C[i*size+j] +=A[i*size+k]*B[k*size+j]; 
       } 
      } 
     } 

     TIME_GET(&end); 
    } 


    alloc_unmap(C, sizeof(INT64_TYPE)*(size*size)); 
    alloc_unmap(B, sizeof(INT32_TYPE)*(size*size)); 
    alloc_unmap(A, sizeof(INT32_TYPE)*(size*size)); 


    //compute and return runtime 
    return TIME_RUNTIME(start,end); 
} 

Making кэширования двух входных массивов некогерентных и с использованием OpenMP с динамическим планированием помогло мне получить распараллеленную производительность превзойти серийную производительность. Это мой первый опыт работы с процессором с архитектурой NUMA, поэтому мои «оптимизации» являются легкими, так как я все еще участвую. В любом случае, я попытался использовать одни и ту же оптимизацию с 8-разрядным целым версии коды выше со всеми тех же условиями (количества потоков и размеров массивов):

double matrix_matrix_mult_int8(int size,int threads) 
{ 

//initialize index variables, random number generator, and timer 
    int i,j,k; 
    srand(SEED); 
    TIME_STRUCT start,end; 


    //allocate memory for matrices 
    alloc_attr_t attrA = ALLOC_INIT; 
    alloc_attr_t attrB = ALLOC_INIT; 
    alloc_attr_t attrC = ALLOC_INIT; 

    alloc_set_home(&attrA, ALLOC_HOME_INCOHERENT); 
    alloc_set_home(&attrB, ALLOC_HOME_INCOHERENT); 
    alloc_set_home(&attrC, ALLOC_HOME_TASK); 

    INT8_TYPE *A=alloc_map(&attrA, sizeof(INT8_TYPE)*(size*size)); 
    INT8_TYPE *B=alloc_map(&attrB, sizeof(INT8_TYPE)*(size*size)); 
    INT16_TYPE *C=alloc_map(&attrC, sizeof(INT16_TYPE)*(size*size)); 

    #pragma omp parallel for num_threads(threads) private(i) 
    for(i=0;i<(size*size);i++) 
    { 

     A[i] = rand(); 
     B[i] = rand(); 
     C[i] = 0; 
     tmc_mem_flush(&A[i], sizeof(A[i])); 
     tmc_mem_flush(&B[i], sizeof(B[i])); 
     tmc_mem_inv(&A[i], sizeof(A[i])); 
     tmc_mem_inv(&B[i], sizeof(B[i])); 
    } 


    //serial operation 
    if(threads==1) 
    { 
     //start timer 
     TIME_GET(&start); 

     //computation 
     for(i=0;i<size;i++) 
     { 
      for(k=0;k<size;k++) 
      { 
       for(j=0;j<size;j++) 
       { 
        C[i*size+j]+=A[i*size+k]*B[k*size+j]; 
       } 
      } 
     } 

    TIME_GET(&end); 

    } 
    else 
    { 

     TIME_GET(&start); 

     #pragma omp parallel for num_threads(threads) private(i,j,k) schedule(dynamic) 
     for(i=0;i<size;i++) 
     { 
      for(j=0;j<size;j++) 
      { 
       for(k=0;k<size;k++) 
       { 
        C[i*size+j] +=A[i*size+k]*B[k*size+j]; 
       } 
      } 
     } 

     TIME_GET(&end); 
    } 


    alloc_unmap(C, sizeof(INT16_TYPE)*(size*size)); 
    alloc_unmap(B, sizeof(INT8_TYPE)*(size*size)); 
    alloc_unmap(A, sizeof(INT8_TYPE)*(size*size)); 


    //compute and return runtime 
    return TIME_RUNTIME(start,end); 
} 

Тем не менее, 8-битный OpenMP версия привела за время, которое было медленнее, чем 32-разрядная версия OpenMP. Должна ли 8-разрядная версия работать быстрее, чем 32-разрядная версия? Что может быть причиной этого несоответствия и каковы возможные вещи, которые могли бы его смягчить? Может ли это быть связано с типом данных используемых массивов или чего-то еще?

+0

Недопустимо сказать, что этот чип равен 7X7 NUMA, поскольку 7x7 NUMA означает 7 узлов на кластер и 7 кластеров. Очевидно, что этот чип имеет только 4 внешних контроллера. – user3528438

+0

Ядра в этом чипе фактически имеют довольно большой кэш L2, поэтому, если ваш набор данных недостаточно велик, то использование меньшего типа данных будет тратить много времени на преобразование в типы полной ширины и из них. Если сжатие данных не улучшает вашу производительность, значит, нет необходимости делать это. – user3528438

+0

Каков размер матрицы? btw это моя помощь вам http://lemire.me/blog/2013/09/13/are-8-bit-or-16-bit-counters-faster-than-32-bit-counters/ – dreamcrash

ответ

1

две вещи, которые приходят на ум

ваш 8-бит (один btye) тип данных по сравнению с 32-битным (четыре btye) тип данных и заданного компилятором выравнивая структуры данных границ N байт. Я думаю, что это обычно 4-байтовые границы, особенно когда он по умолчанию 32-битный. Существует опция компилятора для принудительного выравнивания границ.

Why does compiler align N byte data types on N byte boundaries?

Там могут быть дополнительные операции, случающиеся для обработки типа данных в один байт, где остальные 3 байта должны быть замаскированы, чтобы получить правильное значение, по сравнению с отсутствием маскирующих операций происходит со стандартным 32 (или 64-битным) типом данных.

Другое связано с процессором и памятью, а также то, является ли параллельный OPENMP-код, который выполняется на данном ядре, выборкой или записью данных из памяти, которая не связана непосредственно с этим ядром процессора.Тогда любой хаб (ы), который должен пройти, чтобы достигнуть отдаленной памяти, очевидно, приведет к увеличению времени выполнения. Я не уверен, что это относится к вашей системе типа MAESTRO, с которой я не знаком; но то, что я описываю, - это поздние системы INTEL 4-cpu, которые подключаются через Intel quickpath connect (QPI). Например, если вы работаете на ядре 0 процессора cpu 0, то извлечение из памяти модулей DRAM, ближайших к этому ядру процессора, будет самым быстрым, по сравнению с доступом к DRAM через QPI, подключенным к ядру N на CPU 3, или через какой-либо концентратор или бесконечно доступ к DRAM на другом лезвии или узле и т. д. Я знаю, что сродство можно обрабатывать с помощью MPI, и я считаю, что это может быть с OPENMP, но, возможно, не так. Вы можете попробовать исследовать «сближение памяти openmp cpu».