我正在尝试实现Hogwild! 线性SVM算法,但在我的实现中遇到了虚假共享的问题。
我的代码如下,但我尝试的背景是计算哪些样本未通过我的测试,并根据那组向量进行更新。Hogwild!(据我所知)只是在相同的内存上完全异步地进行更新。这会在数学意义上由于不当时间的更新而产生“噪音”。
遗憾的是,当我尝试进行这些异步更新时,L1缓存会被失效,必须重新获取。以下是我的代码。
有没有好的方法可以在不失去异步性的情况下修复这种虚假共享问题?(我更像是一名数学家而不是计算机科学家)。这篇文章提到使用不同的优化级别可以解决这个问题。
void update(size_t epoch, const double *X_data, const int *X_indices, const int *X_indptr, const int *Y, double *W, double reg, double step_size, size_t nodes, size_t X_height, size_t X_width) { size_t i, j; double step = step_size/(1 + epoch); double c;#pragma omp parallel shared(W, X_data, X_indices, X_indptr, Y) private(i, j, c) {#pragma for schedule(static) for (i=0;i<X_height;i++) { c = 0.0; for (j=X_indptr[i];j<X_indptr[i+1];j++) c += X_data[j]*W[X_indices[j]]; // Scaled to discount the MPI scaling if (Y[i]*c > 1) continue; for (j=X_indptr[i];j<X_indptr[i+1];j++) W[X_indices[j]] += step*Y[i]*X_data[j]/(X_height*nodes); } // END FOR OMP PARALLELIZED #pragma for schedule(static) // Might not do much for (i=0;i<X_width;i++) // (1 - self.reg*step)*self.W/self.nodes + W[i] *= (1 - reg*step)/nodes; }}
回答:
我对你提到的算法了解不多,但在我看来,它整体上更受内存限制而不是计算限制。为了说服你,这是我对你代码的快速重写:
void update( size_t epoch, const double *X_data, const int *X_indices, const int *X_indptr, const int *Y, double *W, double reg, double step_size, size_t nodes, size_t X_height, size_t X_width ) { const double step = step_size / ( 1 + epoch ); const double ratio = step / ( X_height * nodes ); const double tapper = ( 1 - reg * step ) / nodes; #pragma omp parallel { #pragma omp for schedule( static ) for ( size_t i = 0; i < X_height; i++ ) { double c = 0; for ( int j = X_indptr[i]; j < X_indptr[i+1]; j++ ) { c += X_data[j] * W[X_indices[j]]; // Scaled to discount the MPI scaling } if ( Y[i] * c <= 1 ) { double ratioYi = Y[i] * ratio; for ( int j = X_indptr[i]; j < X_indptr[i+1]; j++ ) { // ATTENTION: this will collide across threads and have undefined result BY DESIGN W[X_indices[j]] += ratioYi * X_data[j]; } } } // END FOR OMP PARALLELIZED #pragma omp for schedule( static ) // Might not do much for ( size_t i = 0; i < X_width; i++ ) { // (1 - self.reg*step)*self.W/self.nodes + W[i] *= tapper; } }}
如你所见,我做了一些更改。大多数更改纯粹是风格上的(如缩进、间距、变量声明位置等),但有些确实很重要。例如,通过在尽可能浅的循环中定义ratio
和ratioYi
,我移除了(或帮助编译器移除,如果它本就会这样做的话)代码中的大部分计算。突然之间,代码几乎只访问数据并进行很少的计算变得显而易见。因此,除非你有一台多插槽(或多内存控制器)的共享内存机器,否则你不会从这种OpenMP并行化中看到多少(如果有的话)加速效果。
此外,该算法在并行更新W
时接受的“设计上”的竞争条件,即使在你指出的论文中得到了解释,仍让我感到困惑。我仍然不希望依赖于计算代码(或任何代码)的未定义行为。
无论如何,假设代码确实做了你想要的,具有可扩展性,并且确实仅受虚假共享(或这里确实是真实共享,因为你允许数据冲突)导致的L1缓存失效的限制,一个可能的“解决方案”是增加W
数组的大小,例如将其大小翻倍,并只在每个第二个索引处存储有意义的数据。在你的算法中,这不会改变任何事情。只是,你需要将X_indices
乘以2。这样做,你可以通过机械地将存储在一个缓存行中的有用数据数量减少一半,更加限制虚假共享的可能性。然而,对于受内存限制的代码来说,增加内存消耗可能不是最好的主意…但由于这是一个非常简单的测试,不妨尝试一下,看看是否有任何好处。
最后还要提到,你的代码在OpenMP并行化中有一个错误,你使用了#pragma for
而不是#pragma omp for
。不确定这是复制到这里时的打字错误,但最好提一下以防万一。