簡體   English   中英

OpenMP程序(k-means ++)無法擴展

[英]OpenMP program (k-means++) does not scale

目前,我正在使用OpenMP和C編寫並行版本的k-means ++。到目前為止,我正在實現質心的初始化。 如果您不熟悉此過程,則大致如下 給定具有n個點的dataset (矩陣),使用“概率函數”(也稱為輪盤選擇)來啟動k質心。

假設您有n=4個點以及以下到某些質心的距離數組:

distances = [2, 4, 6, 8]
dist_sum  = 20

從這些中,通過將每個distances條目除以dist_sum並添加之前的結果來定義累積概率數組,如下所示:

probs     = [0.1, 0.2, 0.3, 0.4] = [2/20, 4/20, 6/20, 8/20]
acc_probs = [0.1, 0.3, 0.6, 1.0]

然后,執行輪盤選擇。 給定一個隨機數,比如r=0.5 ,使用racc_probs選擇下一個點,迭代acc_probs直到r < acc_probs[i] 在此示例中,所選點為i=2因為r < acc_probs[2]

問題在這種情況下,我正在處理非常大的矩陣(大約n=16 000 000點)。 盡管該程序給出了正確答案(即質心的良好初始化),但它的擴展性不如預期。 此函數計算此算法后的初始質心。

    double **parallel_init_centroids (double **dataset, int n, int d, int k, RngStream randomizer, long int *total_ops) {

    double dist=0, error=0, dist_sum=0, r=0, partial_sum=0, mindist=0;
    int cn=0, cd=0, ck = 0, cck = 0, idx = 0;
    ck = 0;

    double probs_sum = 0; // debug

    int  mink=0, id=0, cp=0;

    for (ck = 0; ck < k; ck++) {

        if ( ck == 0 ) {

            // 1. choose an initial centroid c_0 from dataset randomly
            idx = RngStream_RandInt (randomizer, 0, n-1);

        }  
        else {

            // 2. choose a successive centroid c_{ck} using roulette selection
            r = RngStream_RandU01 (randomizer);
            idx = 0;
            partial_sum = 0;
            for (cn=0; cn<n; cn++) {

                partial_sum = partial_sum + distances[cn]/dist_sum;

                if (r < partial_sum) {
                    idx = cn;
                    break;
                }
            }
        }

        // 3. copy centroid from dataset
        for (cd=0; cd<d; cd++)
            centroids[ck][cd] = dataset[idx][cd];

        // reset before parallel region
        dist_sum = 0;

        // -- parallel region --
        # pragma omp parallel shared(distances, clusters, centroids, dataset, chunk, dist_sum_threads, total_ops_threads) private(id, cn, cck, cd, cp, error, dist, mindist, mink)
        {
            id = omp_get_thread_num();
            dist_sum_threads[id] = 0;               // each thread reset its entry

            // parallel loop
            // 4. recompute distances against centroids
            # pragma omp for schedule(static,chunk)
            for (cn=0; cn<n; cn++) {

                mindist = DMAX;
                mink = 0;

                for (cck=0; cck<=ck; cck++) {

                    dist = 0;

                    for (cd=0; cd<d; cd++) {

                        error = dataset[cn][cd] - centroids[ck][cd];
                        dist = dist + (error * error);                  total_ops_threads[id]++;
                    }

                    if (dist < mindist) {
                        mindist = dist;
                        mink = ck;
                    }
                }

                distances[cn]           = mindist;
                clusters[cn]            = mink;
                dist_sum_threads[id]    += mindist;      // each thread contributes before reduction
            }
        }
        // -- parallel region --

        // 5. sequential reduction
        dist_sum = 0;
        for (cp=0; cp<p; cp++)
            dist_sum += dist_sum_threads[cp];
    }


    // stats
    *(total_ops) = 0;
    for (cp=0; cp<p; cp++)
        *(total_ops) += total_ops_threads[cp];

    // free it later
    return centroids;
}

正如可以看到,並行區域計算之間的距離n d對維點k d維質心。 這項工作在p線程(最多32個)之間共享。 並行區域完成后,將填充兩個數組: distancesdist_sum_threads 第一個數組與前一個示例相同,而第二個數組包含每個線程收集的累積距離。 考慮前面的示例,如果p=2線程可用,則此數組定義如下:

dist_sum_threads[0] = 6  ([2, 4])  # filled by thread 0
dist_sum_threads[1] = 14 ([6, 8])  # filled by thread 1

dist_sum是通過添加dist_sum_threads每個條目來dist_sum_threads 此函數按預期工作,但是當線程數增加時,執行時間會增加。 顯示了一些績效指標。

我的實現有什么問題,特別是對於openmp? 總之,只使用了兩個pragma:

# pragma omp parallel ...
{
    get thread id

    # pragma omp for schedule(static,chunk)
    {
        compute distances ...
    }

    fill distances and dist_sum_threads[id]
}

換句話說,我刪除了障礙,互斥訪問以及其他可能導致額外開銷的事情。 但是,隨着線程數量的增加,執行時間最短。

更新

  • 之前的代碼已更改為mcve 此代碼段與我之前的代碼類似。 在這種情況下,計算n=100000點和k=16質心之間的距離。
  • 在並行區域之前和之后使用omp_get_wtime測量執行時間。 總時間存儲在wtime_spent
  • 我包括減少計算dist_sum 但是,它沒有按預期工作(它被評為下面的不良並行減少)。 dist_sum的正確值是999857108020.0 ,但是,當使用p線程計算它時,結果是999857108020.0 * p ,這是錯誤的。
  • 績效圖已更新
  • 這是主要的並行功能,完整的代碼位於這里

     double **parallel_compute_distances (double **dataset, int n, int d, int k, long int *total_ops) { double dist=0, error=0, mindist=0; int cn, cd, ck, mink, id, cp; // reset before parallel region dist_sum = 0; // -- start time -- wtime_start = omp_get_wtime (); // parallel loop # pragma omp parallel shared(distances, clusters, centroids, dataset, chunk, dist_sum, dist_sum_threads) private(id, cn, ck, cd, cp, error, dist, mindist, mink) { id = omp_get_thread_num(); dist_sum_threads[id] = 0; // reset // 2. recompute distances against centroids # pragma omp for schedule(static,chunk) for (cn=0; cn<n; cn++) { mindist = DMAX; mink = 0; for (ck=0; ck<k; ck++) { dist = 0; for (cd=0; cd<d; cd++) { error = dataset[cn][cd] - centroids[ck][cd]; dist = dist + (error * error); total_ops_threads[id]++; } if (dist < mindist) { mindist = dist; mink = ck; } } distances[cn] = mindist; clusters[cn] = mink; dist_sum_threads[id] += mindist; } // bad parallel reduction //#pragma omp parallel for reduction(+:dist_sum) //for (cp=0; cp<p; cp++){ // dist_sum += dist_sum_threads[cp]; //} } // -- end time -- wtime_end = omp_get_wtime (); // -- total wall time -- wtime_spent = wtime_end - wtime_start; // sequential reduction for (cp=0; cp<p; cp++) dist_sum += dist_sum_threads[cp]; // stats *(total_ops) = 0; for (cp=0; cp<p; cp++) *(total_ops) += total_ops_threads[cp]; return centroids; } 

你的代碼不是mcve我只能在這里發出假設。 但是,這是我認為(可能)發生的事情(沒有特定的重要性順序):

  • 更新dist_sum_threadstotal_ops_threads時,您會遭受錯誤共享。 您可以通過簡單地聲明reduction( +: dist_sum )並在parallel區域內直接使用dist_sum來完全避免前者。 您也可以使用本地total_ops聲明的reduction(+)total_ops_threads執行total_ops_threads操作,並在最后累積到*total_ops中。 (BTW, dist_sum計算但從未使用過......)

  • 代碼看起來仍然是內存綁定,因為你有大量的內存訪問幾乎沒有計算。 因此,預期的加速將主要取決於您的內存帶寬和您在代碼並行化時可以訪問的內存控制器的數量。 有關詳細信息,請參閱此史詩答案

  • 鑒於上述可能存在內存的問題,嘗試使用內存放置(可能是numactl和/或與proc_bind線程關聯)。 您還可以嘗試使用線程調度策略和/或嘗試查看是否有一些循環切片無法應用於您的問題以阻止數據進入緩存。

  • 您沒有詳細說明測量時間的方式,但請注意,加速僅在掛鍾時間而非CPU時間的情況下才有意義。 請使用omp_get_wtime()進行任何此類測量。

嘗試解決這些問題,並根據您的內存架構評估您的實際潛在加速。 如果您仍然覺得自己沒有達到應有的水平,那么只需更新您的問題即可。


編輯

由於您提供了一個完整的示例,我設法對您的代碼進行了一些實驗,並實現了我想到的修改(主要是為了減少錯誤共享)。

這是函數no的樣子:

double **parallel_compute_distances( double **dataset, int n, int d,
                                     int k, long int *total_ops ) {
    // reset before parallel region
    dist_sum = 0;            

    // -- start time --
    wtime_start = omp_get_wtime ();

    long int tot_ops = 0;

    // parallel loop
    # pragma omp parallel for reduction( +: dist_sum, tot_ops )
    for ( int cn = 0; cn < n; cn++ ) {
        double mindist = DMAX;
        int mink = 0;
        for ( int ck = 0; ck < k; ck++ ) {
            double dist = 0;
            for ( int cd = 0; cd < d; cd++ ) {
                double error = dataset[cn][cd] - centroids[ck][cd];
                dist += error * error;
                tot_ops++;
            }
            if ( dist < mindist ) {
                mindist = dist;
                mink = ck;
            }
        }
        distances[cn] = mindist;
        clusters[cn] = mink;
        dist_sum += mindist;
    }

    // -- end time --
    wtime_end = omp_get_wtime ();

    // -- total wall time --
    wtime_spent = wtime_end - wtime_start;

    // stats
    *(total_ops) = tot_ops;

    return centroids;
}

所以,一些評論:

  • 如前所述, dist_sum和操作總數( tot_ops )的局部變量現在聲明為reduction(+:) 這樣可以避免每個索引使用一個線程訪問同一個數組,從而觸發錯誤共享 (這幾乎是觸發它的最佳情況)。 我使用局部變量而不是total_ops作為后者是指針,它不能直接在reduction子句中使用。 但是,最后使用tot_ops更新它tot_ops完成這項工作。

  • 我盡可能地延遲了所有變量聲明。 這是一種很好的做法,因為它避免了大多數private聲明,這通常是OpenMP程序員的主要缺陷。 現在你只需要考慮兩個reduction變量和兩個數組,它們顯然是shared ,因此不需要任何額外的聲明。 這簡化了parallel指令,並有助於關注重要事項

  • 現在不再需要線程id,可以合並parallelfor指令以獲得更好的可讀性(也可能是性能)。

  • 我刪除了schedule子句,讓編譯器和/或運行時庫使用它們的默認值。 如果我有充分的理由,我只會采用不同的調度政策。

有了這個,在我的雙核筆記本電腦上唱GCC 5.3.0並使用-std=c99 -O3 -fopenmp -mtune=native -march=native編譯,我得到了各種線程的一致結果和2倍的加速2個主題。

在使用英特爾編譯器和-std=c99 -O3 -xhost -qopenmp的10核機器上,我獲得了1到10個線程的線性加速...

即使在Xeon Phi KNC上,我也可以從1到60個線程獲得接近線性的加速(然后使用更多的硬件線程仍會提供一些加速,但不會達到相同的速度)。

觀察到的加速使我意識到,與我假設的不同,代碼不受內存限制,因為您訪問的數組實際上已經很好地緩存了。 原因是你只訪問dataset[cn][cd]centroids[ck][cd] ,其中第二維非常小(40和16),因此非常適合緩存,而下一個dataset要加載cn索引可以有效預取。

您是否仍然遇到此版本代碼的可伸縮性問題?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM