C OpenMP程序(k-means+;+;)不可缩放
目前,我正在使用OpenMP和C编程一个并行版本的k-means++。到目前为止,我正在实现质心的初始化。如果您不熟悉此过程,其工作原理大致相同。给定具有C OpenMP程序(k-means+;+;)不可缩放,c,parallel-processing,openmp,C,Parallel Processing,Openmp,目前,我正在使用OpenMP和C编程一个并行版本的k-means++。到目前为止,我正在实现质心的初始化。如果您不熟悉此过程,其工作原理大致相同。给定具有n点的dataset(矩阵),k质心使用“概率函数”(也称为轮盘选择)初始化 假设您有n=4个点和以下到某些质心的距离数组: distances = [2, 4, 6, 8] dist_sum = 20 通过将距离的每个条目除以距离和,并添加之前的结果,定义一个累积概率数组,如下所示: probs = [0.1, 0.2, 0.3,
n
点的dataset
(矩阵),k
质心使用“概率函数”(也称为轮盘选择)初始化
假设您有n=4
个点和以下到某些质心的距离数组:
distances = [2, 4, 6, 8]
dist_sum = 20
通过将距离
的每个条目除以距离和
,并添加之前的结果,定义一个累积概率数组,如下所示:
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
,使用r
和acc_probs
选择下一个点,在acc_probs
上迭代,直到r
。在本例中,所选点为i=2
,因为r
问题
在本例中,我使用的是非常大的矩阵(大约n=1600000
个点)。尽管该程序给出了正确的答案(即质心的良好初始化),但其伸缩性不如预期。此函数根据此算法计算初始质心
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;
}
dist\u sum
通过添加dist\u sum\u线程的每个条目来定义。此函数按预期工作,但当线程数增加时,执行时间会增加。这显示了一些性能指标
我的实现有什么问题,特别是openmp?总之,只使用了两个pragma:
# pragma omp parallel ...
{
get thread id
# pragma omp for schedule(static,chunk)
{
compute distances ...
}
fill distances and dist_sum_threads[id]
}
换句话说,我消除了障碍、互斥访问和其他可能导致额外开销的因素。但是,随着线程数的增加,执行时间最差
更新
- 先前的代码已更改为。与我以前的代码类似。在这种情况下,计算
n=100000
点和k=16
质心之间的距离李>
- 在并行区域之前和之后,使用
omp\u get\u wtime
测量执行时间。总时间存储在wtime\u explored
中
- 我包括一个计算距离和的缩减。然而,它并没有像预期的那样工作(下面被评论为糟糕的并行缩减)。
dist_sum
的正确值是999857108020.0
,但是,当使用p
线程计算它时,结果是999857108020.0*p
,这是错误的
- 表演情节是
- 这是主要的并行功能,完整代码位于:
double**并行计算距离(double**数据集,整数n,整数d,整数k,长整数*总运算){
双距离=0,错误=0,心智主义=0;
int cn、cd、ck、水貂、id、cp;
//平行区域前复位
距离和=0;
//--开始时间--
wtime_start=omp_get_wtime();
//并联回路
#pragma omp parallel shared(距离、簇、质心、数据集、块、距离和、距离和线程)private(id、cn、ck、cd、cp、error、dist、mindsist、mink)
{
id=omp_get_thread_num();
dist_sum_线程[id]=0;//重置
//2.根据质心重新计算距离
#计划的pragma omp(静态、块)
对于(cn=0;cn您的代码不是一个假设,我只能在此提出假设。然而,以下是我认为(可能)发生的事情(没有具体的重要性顺序):
- 当您更新
dist\u sum\u线程
和total\u ops\u线程
时,您会遇到错误共享。只需声明reduce(+:dist\u sum)即可完全避免前者
并在并行区域内直接使用dist\u-sum
。您也可以对total\u-ops\u-threads
使用本地total\u-ops
声明的reducement(+)
执行类似操作,并在最后将其累加到*total\u-ops
(顺便说一句,dist-sum
已计算但从未使用过…)
- 无论如何,代码看起来都是内存受限的,因为您有大量的内存访问,几乎不需要计算。因此,预期的速度提高主要取决于您的内存带宽和并行代码时可以访问的内存控制器的数量。有关更多详细信息,请参阅
- 鉴于您的问题可能存在上述内存绑定特征,请尝试使用内存放置(
numactl
可能和/或线程关联proc\u bind
)。您还可以尝试使用线程调度策略和/或尝试查看是否无法对将数据阻塞到缓存中的问题应用某些循环平铺
- 您没有详细说明您测量时间的方式,但请注意,只有在挂钟时间的情况下,而不是CPU时间的情况下,速度才有意义。请使用
omp\u get\u 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;
}
double**并行计算距离(double**数据集,int n,int d,
整数k,长整数*总运算){
//平行区域前复位
距离和=0;
//--开始时间--
wtime_start=omp_get_wtime();
长整数tot_ops=0;
//并联回路
#pragma omp并行用于缩减(+:距离和,tot_
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;
}
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;
}