有没有办法在 Chapel 中自定义整个数组语句的默认并行化行为?

Is there a way to customize the default parallelization behavior of whole-array statements in Chapel?

根据 Chapel 的可用文档,(整个)数组 像

这样的语句
A = B + alpha * C;   // with A, B, and C being arrays, and alpha some scalar

在所有迭代中以如下语言实现:

forall (a,b,c) in zip(A,B,C) do
   a = b + alpha * c;

因此,数组语句默认由一组并行执行 线程。不幸的是,这似乎也完全排除了 此类语句的(部分或完整)向量化。这个 对于习惯于 Fortran 或 Python/Numpy 等语言(默认行为通常是仅对数组语句进行矢量化)的程序员来说,这可能会导致性能意外。

对于使用(整个)数组语句的代码,数组小到 大小适中,向量化损失(由 Linux 硬件确认 性能计数器)和固有的显着开销 并行线程(不适合有效地利用 此类问题中可用的细粒度数据并行性)可能会导致 在显着的性能损失。作为一个例子,考虑 以下版本的 Jacobi 迭代都解决了相同的问题 在 300 x 300 个区域的域上:

Jacobi_1使用数组语句,如下:

/*
 *  Jacobi_1
 *
 *  This program (adapted from the Chapel distribution) performs
 *  niter iterations of the Jacobi method for the Laplace equation
 *  using (whole-)array statements.
 *
 */

config var n = 300;                  // size of n x n grid
config var niter = 10000;            // number of iterations to perform

proc main() {

  const Domain = {0..n+1,0..n+1};    // domain including boundary points

  var iteration = 0;                 // iteration counter
  var X, XNew: [Domain] real = 0.0;  // declare arrays: 
                                     //   X stores approximate solution
                                     //   XNew stores the next solution  
  X[n+1,1..n] = 1.0;                 // Set south boundary values to 1.0

  do {

    // compute next approximation
    XNew[1..n,1..n] =
      ( X[0..n-1,1..n] + X[2..n+1,1..n] +
        X[1..n,2..n+1] + X[1..n,0..n-1] ) / 4.0;

    // update X with next approximation
    X[1..n,1..n] = XNew[1..n,1..n];

    // advance iteration counter
    iteration += 1;

  } while (iteration < niter);

  writeln("Jacobi computation complete.");
  writeln("# of iterations: ", iteration);

} // main

Jacobi_2 在整个过程中使用串行 for 循环(即仅(自动)矢量化 允许后端 C 编译器):

/*
 *  Jacobi_2
 *
 *  This program (adapted from the Chapel distribution) performs
 *  niter iterations of the Jacobi method for the Laplace equation
 *  using (serial) for-loops.
 *
 */

config var n = 300;                  // size of n x n grid
config var niter = 10000;            // number of iterations to perform

proc main() {

  const Domain = {0..n+1,0..n+1};    // domain including boundary points

  var iteration = 0;                 // iteration counter
  var X, XNew: [Domain] real = 0.0;  // declare arrays: 
                                     //   X stores approximate solution
                                     //   XNew stores the next solution  
  for j in 1..n do
    X[n+1,j] = 1.0;                  // Set south boundary values to 1.0

  do {

    // compute next approximation
    for i in 1..n do
      for j in 1..n do  
        XNew[i,j] = ( X[i-1,j] + X[i+1,j] +
                      X[i,j+1] + X[i,j-1] ) / 4.0;

    // update X with next approximation
    for i in 1..n do
      for j in 1..n do
        X[i,j] = XNew[i,j];

    // advance iteration counter
    iteration += 1;

  } while (iteration < niter);

  writeln("Jacobi computation complete.");
  writeln("# of iterations: ", iteration);

} // main

Jacobi_3,最后,对最内层的循环进行了向量化,并且只有 最外层循环线程:

/*
 *  Jacobi_3
 *
 *  This program (adapted from the Chapel distribution) performs
 *  niter iterations of the Jacobi method for the Laplace equation
 *  using both parallel and serial (vectorized) loops.
 *
 */

config var n = 300;                  // size of n x n grid
config var niter = 10000;            // number of iterations to perform

proc main() {

  const Domain = {0..n+1,0..n+1};    // domain including boundary points

  var iteration = 0;                 // iteration counter
  var X, XNew: [Domain] real = 0.0;  // declare arrays: 
                                     //   X stores approximate solution
                                     //   XNew stores the next solution  
  for j in vectorizeOnly(1..n) do
    X[n+1,j] = 1.0;                  // Set south boundary values to 1.0

  do {

    // compute next approximation
    forall i in 1..n do
      for j in vectorizeOnly(1..n) do
        XNew[i,j] = ( X[i-1,j] + X[i+1,j] +
                      X[i,j+1] + X[i,j-1] ) / 4.0;

    // update X with next approximation
    forall i in 1..n do
      for j in vectorizeOnly(1..n) do
        X[i,j] = XNew[i,j];

    // advance iteration counter
    iteration += 1;

  } while (iteration < niter);

  writeln("Jacobi computation complete.");
  writeln("# of iterations: ", iteration);

} // main

运行 这些代码在具有 2 个处理器内核并使用两个 并行线程,人们发现 Jacobi_1 是(令人惊讶的) 比 Jacobi_2 慢十倍以上,它本身是(预期的) 比 Jacobi_3.

慢 ~1.6 倍

不幸的是,这种默认行为使得数组语句完全 对于我的用例没有吸引力,即使对于有益的算法也是如此 极大地来自更简洁的符号和可读性 (whole-)array语句可以提供。

Chapel 中的用户是否有办法更改此默认行为? 也就是说,用户可以自定义整个数组的默认并行化吗? Jacobi_1 中使用的数组语句将 表现得像 Jacobi_2 中的代码(这对代码开发和调试很有用),或 Jacobi_3 中的代码(在这三者中,它是生产计算的首选方法) ?

我试图通过将对“vectorizeOnly()”的调用插入到 上面 "Domain" 的定义,但没有用。

Chapel 的意图是 to support vectorization automatically within the per-task serial loops that are used to implement forall loops (for cases that are legally vectorizable). Yet that capability is not well-supported today, as you note (even the vectorizeOnly() iterator that you're using is only considered prototypical)。

我会提到,与使用(默认)C 后端相比,使用 Chapel 的 LLVM 后端我们往往会看到更好的矢量化结果,而且在使用 Simon Moll 的基于 LLVM 的区域向量器(萨尔大学)。但我们也看到过 LLVM 后端性能不如 C 后端的情况,因此您的情况可能会有所不同。但如果你关心矢量化,那值得一试。

针对您的具体问题:

Are there ways for the user in Chapel to change this default behavior?

有。对于显式 forall 循环,您可以 write your own parallel iterator which can be used to specify a different implementation strategy for a forall loop than our default iterators use. If you implement one that you like, you can then write (or clone and modify) a domain map (background here) 来控制默认情况下如何实现给定数组上的循环(即,如果没有显式调用迭代器)。这允许最终用户为 Chapel 阵列指定不同于我们默认支持的实现策略。

关于您的三个代码变体,我注意到第一个使用多维压缩,这在当今已知存在严重的性能问题。这可能是它与其他产品之间性能差异的主要原因。例如,我怀疑如果您使用 forall (i,j) in Domain ... 形式重写它,然后使用每个维度的 +/-1 索引,您会看到显着的改进(而且,我猜,性能更具可比性到第三种情况)。

对于第三个,我想知道您看到的好处是由于矢量化还是仅仅由于多任务处理,因为您已经避免了第一个的性能问题和第二个的串行实现。例如,您是否检查过使用 vectorizeOnly() 迭代器是否比没有该迭代器的相同代码增加了任何性能改进(或使用二进制文件上的工具来检查是否正在发生矢量化?)

在任何 Chapel 性能研究中,确保抛出 --fast 编译器标志。同样,为了获得最佳矢量化结果,您可以尝试 LLVM 后端。