Как правильно распараллелить алгоритм C с OpenMP? - PullRequest
0 голосов
/ 06 ноября 2018

Я начинаю с OpenMP и мне нужно распараллелить некоторый C-код. Я перепробовал много способов и никогда не достиг желаемых результатов.

Я покажу вам мое распараллеливание. Если вы можете посоветовать мне, как правильно распараллелить его, я буду очень признателен.

У меня есть рабочий (но не перфоманизированный) распараллеленный код. Я прокомментировал MAYUS comments, где производительность потеряна (я могу знать, где код потерял производительность из-за того, что я тестировал код в течение длительного времени):

#include <File.h>
#include <time.h>
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <omp.h>

#define sqr(x) ((x)*(x))
#define MAX_ITER_NO_IMPR 10



void fail(const char * str) {
    fprintf(stderr,"%s", str);
    exit(-1);
}

/**
* calc_distance calculates the distance between a given point and a cluster
* @param int -dim: number of columns (variables) in the data set to be classified
* @param float * -: first arrray to calculate de distance
* @param float * -: Second array to calculate de distance 
* @return float: Euclidean distance of two vectors
*/
float calc_distance(int dim, float *p1, float  *p2) {
    float distance_sq_sum = 0;

    for (int i = 0; i < dim; ++i)
        distance_sq_sum += sqr(p1[i] - p2[i]);

    return distance_sq_sum;  
}

/**
* calc_all_distances computes the euclidean distances between centros ids and dataset points. 
* @param int -dim: number of columns (variables) in the data set to be classified
* @param int -n: number of rows (points) in the data set to be classified   
* @param int -k: number of clusters to be calculated
* @param float * -X: dataset to be classified
* @param float * -centroid: prototypes of each cluster. 
* @param float * -distance_output[n][k] contains the distance between all elements * in the dataset and all clusters
* return void  
*/
void calc_all_distances(int dim, int n, int k, float *X, float *centroid, float *distance_output) {
    #pragma omp parallel for simd collapse(2)
    for (int i = 0; i < n; ++i) // for each point
        for (int j = 0; j < k; ++j) // for each cluster 
            // calculate distance between point and cluster centroid
            distance_output[i*k+j] = calc_distance(dim, &X[i*dim], &centroid[j*dim]);
}


/**
* calc_total_distance calculates the clustering overall distance.  
* @param int -dim: number of columns (variables) in the data set to be classified
* @param int -n: number of rows (points) in the data set to be classified   
* @param int -k: number of clusters to be calculated
* @param float * -X: dataset to be classified
* @param float * -centroid: prototypes of each cluster. 
* @param int * - cluster_assignment_index: current cluster assignment to each point
* @return float overall distance. This is what the algorithm tried to minimize  
*/
float calc_total_distance(int dim, int n, int k, float *X, float *centroids, int *cluster_assignment_index) {
    // NOTE: a point with cluster assignment -1 is ignored
    float tot_D = 0;

    // for every point
    #pragma omp parallel for simd reduction(+:tot_D)
//HERE PERFOMANCE IS LOST
    for (int i = 0; i < n; ++i) {
        // which cluster is it in?
        int active_cluster = cluster_assignment_index[i];

        // sum distance
        if (active_cluster != -1)
            tot_D += calc_distance(dim, &X[i*dim], &centroids[active_cluster*dim]);
    }

    return tot_D;
}


/**
* choose_all_clusters_from_distances obtains the closest cluster for each point.  
* @param int -dim: number of columns (variables) in the data set to be classified
* @param int -n: number of rows (points) in the data set to be classified   
* @param int -k: number of clusters to be calculated
* @param float * -distance_array[n][k] contains the distance between all elements * in the dataset and all clusters
* @param int* - cluster_assignment_index contains the assigned cluster to each point 
* @return void
*/
void choose_all_clusters_from_distances(int dim, int n, int k, float *distance_array, int *cluster_assignment_index) {
    // for each point
    #pragma omp parallel for simd
    for (int i = 0; i < n; ++i) {
        int best_index = -1;
        float closest_distance = INFINITY;

        // for each cluster
//  #pragma omp privete(best_index, closest_distance)
// BEST_INDEX AND CLOSEST_DISTANCE SHOULD NOT BE PRIVATED?
        for (int j = 0; j < k; ++j) {
            // distance between point and cluster centroid
            float cur_distance = distance_array[i*k+j];
            if (cur_distance < closest_distance) {
                best_index = j;
                closest_distance = cur_distance;
            }
        }

        // record in array
        cluster_assignment_index[i] = best_index;
    }
}

/**
* calc_cluster_centroids calculates the new prototypes of all clusters 
* @param int -dim: number of columns (variables) in the data set to be classified
* @param int -n: number of rows (points) in the data set to be classified   
* @param int -k: number of clusters to be calculated
* @param float * -X: dataset to be classified
* @param int * - cluster_assigment_index:  
* @param float * -new_cluster_centroid: it is the output with the new cluster prototypes
*/

void calc_cluster_centroids(int dim, int n, int k, float *X, int *cluster_assignment_index, float *new_cluster_centroid) {
    int * cluster_member_count = (int *) calloc (k,sizeof(float));

    // sum all points
    // for every point
    #pragma omp parallel for simd
//HERE PERFOMANCE IS LOST
    for (int i = 0; i < n; ++i) {
        // which cluster is it in?
        int active_cluster = cluster_assignment_index[i];

        // update count of members in that cluster
        ++cluster_member_count[active_cluster];

        // sum point coordinates for finding centroid
        for (int j = 0; j < dim; ++j)
            new_cluster_centroid[active_cluster*dim + j] += X[i*dim + j];
    }


    // now divide each coordinate sum by number of members to find mean/centroid
    // for each cluster
    #pragma omp for
    for (int i = 0; i < k; ++i) {
        if (cluster_member_count[i] == 0) {
            //printf("WARNING: Empty cluster %d! \n", i);
        //break;
            // SEÑAL PARA CANCELAR EL BUCLE
        #pragma omp cancel for
        }
    // CANCELAR LA EJECUCIÓN DE TODOS LOS HILOS
    #pragma omp cancellation point for

        // for each dimension
    #pragma omp simd 
//HERE PERFOMANCE IS BETTER
        for (int j = 0; j < dim; ++j)
            new_cluster_centroid[i*dim + j] /= cluster_member_count[i];  /// XXXX will divide by zero here for any empty clusters!
    }
}

/**
* get_cluster_member_count the member of each cluster
* @param int -n: number of rows (points) in the data set to be classified   
* @param int -k: number of clusters to be calculated
* @param int* - cluster_assignment_index contains the assigned cluster to each point 
* @param int * -cluster_member_count: count members of each cluster 
*/
void get_cluster_member_count(int n, int k, int *cluster_assignment_index, int *cluster_member_count) {
    // count members of each cluster    
    #pragma omp parallel for
    for (int i = 0; i < n; ++i)
    #pragma omp atomic update
        ++cluster_member_count[cluster_assignment_index[i]];
}


/**
* Visualize the number of members for all clusters
*/
void cluster_diag(int dim, int n, int k, float *X, int *cluster_assignment_index, float *cluster_centroid) {
    int * cluster_member_count = (int *) calloc (k, sizeof(int));

    get_cluster_member_count(n, k, cluster_assignment_index, cluster_member_count);

    printf("  Final clusters\n");
    #pragma omp parallel for ordered
//HERE PERFOMANCE IS LOST
    for (int i = 0; i < k; ++i) { 
    #pragma omp ordered
        printf("\tcluster %d:  members: %8d, for the centroid (", i, cluster_member_count[i]);
        for (int j = 0; j < dim; ++j)  
            #pragma omp ordered 
            printf ("%f, ", cluster_centroid[i*dim + j]);
    #pragma omp ordered
        printf (")\n");
    }
}

void copy_assignment_array(int n, int *src, int *tgt) {
    #pragma omp  parallel for simd
    for (int i = 0; i < n; ++i)
        tgt[i] = src[i];
}  


int assignment_change_count(int n, int a[], int b[]) {
    int change_count = 0;

    #pragma omp parallel for reduction(+:change_count)
    for (int i = 0; i < n; ++i)
        if (a[i] != b[i])
            ++change_count;

    return change_count;
}


/*
* This is C source code for a simple implementation of the popular k-means clustering algorithm. 
* It is based on the implementation in Matlab, which was in turn based on GAF Seber, 
* Multivariate Observations, 1964, and H Spath, Cluster Dissection and Analysis: Theory, FORTRAN Programs, Examples.
* @param int -dim: number of columns (variables) in the data set to be classified (dimension of data)
* @param float * -X: dataset to be classified (pointer to data)
* @param int -n: number of rows (points) in the data set to be classified (number of elements)
* @param int -k: number of clusters to be calculated
* @param float * -cluster_centroid: Initial clusters prototypes or centros (initial cluster centroids)
* @param int iterations -: number of iterations to be performed
* @param int * cluster_assignment_final -: Output classitfication  
*/
void kmeans(int dim, float *X, int n, int k, float *cluster_centroid, int iterations, int *cluster_assignment_final) {
    int floatPointerSize = n * k * sizeof(float);
    int intPointerSize = n * sizeof(int);
    float *dist = (float *) malloc( floatPointerSize );
    int *cluster_assignment_cur = (int *) malloc( intPointerSize );
    int  *cluster_assignment_prev = (int *) malloc( intPointerSize );
    float *point_move_score = (float *) malloc( floatPointerSize );

    if (!dist || !cluster_assignment_cur || !cluster_assignment_prev || !point_move_score)
        fail("Error allocating dist arrays\n");

    // Initial setup. Assignment Step  
    calc_all_distances(dim, n, k, X, cluster_centroid, dist);
    choose_all_clusters_from_distances(dim, n, k, dist, cluster_assignment_cur);
    copy_assignment_array(n, cluster_assignment_cur, cluster_assignment_prev);

    //The initial quality is the one obtained from the random election
    float prev_totD = calc_total_distance(dim, n, k, X, cluster_centroid, cluster_assignment_cur);

    int numVariations = 0;
    // UPDATE STEP
    // for (int batch=0; (batch < iterations) && (numVariations <MAX_ITER_NO_IMPR); ++batch) {

   for (int batch = 0; batch < iterations; ++batch) {
        //printf("Batch step: %d \n", batch);
        //cluster_diag(dim, n, k, X, cluster_assignment_cur, cluster_centroid);

        // update cluster centroids. Update Step
        calc_cluster_centroids(dim, n, k, X, cluster_assignment_cur, cluster_centroid);

        float totD = calc_total_distance(dim, n, k, X, cluster_centroid, cluster_assignment_cur);

        // see if we've failed to improve
        if (totD >= prev_totD){
            // failed to improve - currently solution worse than previous
            // restore old assignments
            copy_assignment_array(n, cluster_assignment_prev, cluster_assignment_cur);

            // recalc centroids
            // calc_cluster_centroids(dim, n, k, X, cluster_assignment_cur, cluster_centroid);    
            //printf("\tNegative progress made on this step - iteration completed (%.2f) \n", prev_totD-totD);
            ++numVariations; //To implement no convergence criteria               
        }
        else { // We have made some improvements        
            // save previous step
            copy_assignment_array(n, cluster_assignment_cur, cluster_assignment_prev);
            // move all points to nearest cluster
            calc_all_distances(dim, n, k, X, cluster_centroid, dist);
            choose_all_clusters_from_distances(dim, n, k, dist, cluster_assignment_cur);
            //check how many assignments are different  
            //int change_count = assignment_change_count(n, cluster_assignment_cur, cluster_assignment_prev);
            //printf("\tIn the batch: %d, has changed: %d element to a different cluster with an improvement of %f \n", batch, change_count, prev_totD-totD);
            //fflush(stdout);
            prev_totD = totD;
        } 
    }

// COMENTAR ESTA LÍNEA PARA NO MOSTRAR RESULTADOS
    cluster_diag(dim, n, k, X, cluster_assignment_cur, cluster_centroid);

    // write to output array
    copy_assignment_array(n, cluster_assignment_cur, cluster_assignment_final);    

    //Free memory
    free(dist);
    free(cluster_assignment_cur);
    free(cluster_assignment_prev);
    free(point_move_score);
}           



/**
* random_init_centroid chooses random prototypes that belong to the dataset. They are points of the dataset.   
*@param float * -: cluster_centro_if: clustes id choosen
*@param float * -: dataSetMatrix 
*@param int clusters: Number of cluster to be don. 
*@param int rows in number of rows in the dataset; i.e. points
*@param int columns: number of columns. Point's dimension. 
*@return void
*/
void random_init_centroid (float * cluster_centro_id, float * dataSetMatrix, int clusters, int rows, int columns) {
   srand(time(NULL));

   for (int i=0; i<clusters; ++i) {
        int r = rand()%rows; 
        for (int j=0; j<columns;++j) {
            cluster_centro_id[i*columns+j]=dataSetMatrix[r*columns+j];
            //printf ("Los indices son  %d\n", r*columns+j);        
        }       
    }
}   



int main( int argc, char *argv[] ) {
/**/
    // COMPROBAR QUE LA CANCELACIÓN ESTÉ ACTIVADA    
    if( !omp_get_cancellation() )
    {
        //printf("Cancellations were not enabled, enabling cancellation and rerunning program\n");
        putenv("OMP_CANCELLATION=true");
        execv(argv[0], argv);
    }
    // COMPROBAR QUE EL PROGRAMA SIEMPRE SE EJECUTE EN PARALELO
    int numHilos = 0;
    #pragma omp parallel
    {
        if ( omp_get_thread_num() == 0 ) numHilos = omp_get_num_threads();
    }
    if (numHilos == 1) {
    //printf("Program is executing sequentially, setting 2 threads and rerunning program\n");
        putenv("OMP_NUM_THREADS=2");
        execv(argv[0], argv);
    }
/**/

    float *cluster_centroid;   // initial cluster centroids. The size is Clusters x rows
    int *clustering_output;  // output
    int rows=0, columns=0, clusters=1;
    int iterations = 1000;
    float * dataSetMatrix=NULL;
    char c, *fileName=NULL;

    //int err=system("clear");

    while ((c = getopt (argc, argv, "v:c:f:i:h")) != -1) {
        switch (c) {
        case 'v':
            printf("K means algorithm v.1.0\n\n");
        return 0;
        case 'c':
            clusters = atoi(optarg);
            if (clusters < 1) { 
                    printf ("the minimum number of clusters is 1\n");
                    return 0;
                }
                break;
        case 'f':
                fileName = (char *) malloc (strlen(optarg)+1);  
            strcpy(fileName,optarg);
            break;
        case 'i':
                iterations = atoi (optarg);  
                break;
        case 'h':
        case '?':
            printf("Usage:\trun -c number of clusters -f fichero.txt -i number of iterations [-h | -? HELP] \n");
        printf("\t<Params>\n");
        printf("\t\t-v\t\tOutput version information and exit\n");
            return 0;
        }
    }

    //printf ("..............Loading data set...............\n "); 
    // Get file size dataset
    getSizeFile( fileName, &rows, &columns );

    clustering_output = (int *) malloc (rows*sizeof(int));
    // Reserve dynamic memory for dataset matrix
    reserveDynamicMemoryForMatrix( &dataSetMatrix, rows, columns );

    // Set data in the dataset matrix
    setDataInMatrix( dataSetMatrix, fileName, rows, columns );

    //printf ("-------DataSet: \n");
    //printMatrix(dataSetMatrix, rows, columns);

    // printf ("..............Done..............\n "); 
    cluster_centroid = (float *) malloc (clusters*columns*sizeof(float));
    random_init_centroid (cluster_centroid, dataSetMatrix, clusters, rows, columns);   

    //printf (".........Initial Prototypes: ................ \n");
    //printMatrix(cluster_centroid, clusters, columns);

// COMENTAR ESTAS LÍNEA PARA NO MOSTRAR RESULTADOS
    printf ("The number of instance: %d Variables: %d Clusters: %d and Iterations: %d\n", rows, columns,clusters, iterations);
//  printf ("File: %d; \tClusters: %d; \tIterations: %d\n", filename, clusters, iterations);
//    
    double ini = omp_get_wtime();
    kmeans (columns, dataSetMatrix, rows, clusters, cluster_centroid, iterations, clustering_output);  
    double fin = omp_get_wtime();
    printf ("The execution time is %lf seconds\n", fin-ini);

    // Free memory
    free (dataSetMatrix); 
    free (cluster_centroid);
    free (clustering_output); 
}

Но теперь проблема в том, что у него есть условия карьерной передачи, потому что последовательно его время выполнения составляет 9.1XXs, а параллельно - 90.XXXs, так что ... производительность снижается. Есть идеи, почему это может быть?

У меня есть вопросы:

  • Что не так? Почему моя производительность распараллеливания хуже, чем последовательная?
  • Должен ли я использовать atomic для ++cluster_member_count[active_cluster]; (атомарный для приращений вектора)
  • В for цикле внутри другого, я должен использовать pragma omp for только в первом цикле? или тоже во второй (детский) for цикл тоже? (в этом случае, я должен использовать omp_set_nested(1))? Я имею в виду:

    // EXAMPLE
    for (;;) {
        // DO SOMETHING
        for(;;) {
            // DO SOMETHING
        }
    }
    

    В этом примере циклы не могут быть вложенными. Какой вариант будет лучше?

    // EXAMPLE - FIRST LOOP PARALLEL
    #pragma omp parallel for
    for (;;) {
        // DO SOMETHING
        for(;;) {
            // DO SOMETHING
        }
    }
    

    или

    // EXAMPLE - FIRST AND SECOND LOOP PARALLEL NO NESTED
    omp_set_nested(0); // default option
    #pragma omp parallel for
    for (;;) {
        // DO SOMETHING
        #pragma omp parallel for
        for(;;) {
            // DO SOMETHING
        }
    }
    

    или

    // EXAMPLE - FIRST AND SECOND LOOP PARALLEL NESTED
    omp_set_nested(1);
    #pragma omp parallel for
    for (;;) {
        // DO SOMETHING
        #pragma omp parallel for
        for(;;) {
            // DO SOMETHING
        }
    }
    

Спасибо.

1 Ответ

0 голосов
/ 06 ноября 2018

Не прошел весь код. Вам нужно распараллеливать циклы, когда имеется значительный объем данных, и в большинстве случаев достаточно одного уровня распараллеливания. например использование #omp pragma parallel for в calc_all_distances нормально. Однако функция calc_distance вызывается из этого цикла, поэтому функция не должна распараллеливаться.

float calc_distance(int dim, float *p1, float  *p2) {

    float distance_sq_sum = 0;
    // Serial loop as the function will be called from already parallellized block of code
    for (int i = 0; i < dim; ++i)
        distance_sq_sum += sqr(p1[i] - p2[i]);

    return distance_sq_sum;
}

void calc_all_distances(int dim, int n, int k, float *restrict X, float *restrict centroid, float *restrict distance_output) {
    // Parallelization is done there. 
    #pragma omp parallel for schedule(static) collapse(2)
    for (int i = 0; i < n; ++i) // for each point
        for (int j = 0; j < k; ++j) // for each cluster 
            // calculate distance between point and cluster centroid
            distance_output[i*k+j] = calc_distance(dim, &X[i*dim], &centroid[j*dim]);
}

Предложение schedule(dynamic,1) создает значительные накладные расходы и лучше всего используется, когда время выполнения для итераций значительно варьируется, и некоторые потоки могут загружаться вычислениями, в то время как другие выполняются и бездействуют. Это заставит каждый поток запрашивать к мастеру один кусок данных для обработки после каждой итерации. Здесь значение по умолчанию schedule(static) (рабочая нагрузка разделяется раз и навсегда в начале цикла) выглядит лучше, поскольку объем работы одинаков независимо от итерации. schedule(guided) - это компромисс. Предложение collapse(2) распараллеливает оба цикла вместе (полезно, если имеется меньше точек, чем у потоков openmp, в которых распараллеливание через первый цикл не будет оптимальным), но в противном случае может ухудшить производительность. Ключевое слово restrict сообщает компилятору, что он может ожидать, что все три указателя будут указывать на разные области памяти (и что в основном запись в distance_output не изменит значения X или centroid) и может оптимизировать код на основе в предположении.

В calc_cluster_centroids: #pragma omp atomic действительно должен защищать оператор ++cluster_member_count[active_cluster];. То же самое верно там, где делается увеличение значений, совместно используемых всеми потоками. Кроме того, cluster_member_count выделяется некоторая память, которая никогда не освобождается (утечка памяти).

...