【问题标题】:Parallelizing nested loops with dependencies使用依赖项并行化嵌套循环
【发布时间】:2009-10-03 10:14:53
【问题描述】:

我的问题是关于如何最好地构建我的 (C++) 代码以支持并行化耗时的计算。有问题的伪代码具有以下结构:

for a_1(y_1) in A_1
    for a_2(y_2) in A_2(a_1)
        ...
            for a_n(y_n) in A_n(a_1, ..., a_{n-1})
                y_n = f(a_1, ..., a_n)
            y_{n-1} = g_n(Y_n)
        ...
    y_1 = g_2(Y_2)
.

粗略地说,每个循环迭代集合A_i 中的元素,其连续元素依赖于来自先前迭代的反馈y_i。换句话说,要确定下一个a_i,我们必须完成对当前a_i 的所有计算。此外,内部集依赖于外部迭代。写成递归形式:

Iterate(A_i, a_1, ..., a_{i-1}):
    for a_i(h_i) in A_i
        Y_i += Iterate(A_{i+1}, a_1, ..., a_i)
    return g(Y_i)

Iterate(any, a_1, ..., a_n):
    return f(a_1, ..., a_n)

Iterate(A_1)

假设 f(...) 是一个耗时的计算,并且反馈函数 g(...) 很简单(快速)。现在,如果所有集合 A_i 都是“大”的,那么问题是可并行化的,令人尴尬。目前,我有一个线程池,只是将最内层循环的计算扔到池中。问题是,最内层的循环通常是对单例的迭代,因此线程池中只有一个正在运行的线程。我曾考虑过使用期货将值返回到外部循环,但这需要期货等,而且它变得非常混乱(我认为)。

我意识到我上面列出的结构相当复杂,所以我也对一些简化案例感兴趣:

  1. a_i(h_i) = a_i ;独立于 h_i
  2. A_i(a_1, ..., a_{i-1}) = A_i ;独立于 a_1, ... a_{i-1}
  3. g_i = 0 ;独立于 H_{i+1}
  4. 所有外部循环都是“大”的;这些集合中的元素数量远大于核心数量。

现在,在实践中,n


编辑:

清理了第一个伪代码块,使其与另一个一致。 由于人们无法理解我的数学符号,这里有一个更具体的简单示例:

#include <cmath>
#include <iostream>
#include <vector>
using namespace std;

double f(double a1, int a2, double a3){ // Very slow function
    cout << a1 << ", " << a2 << ", " << a3 << endl;
    return pow(a1*a3, a2) + a1 + a2 + a3; // just some contrived example
}

int g2(const vector<double> &Y3){ // average-ish
    double sum = 0;
    for(int i = 0; i < Y3.size(); ++i){ sum += Y3[i]; }
    return int(sum / (Y3.size() + 1));
}

double g1(const vector<int> &Y2){ // return 1/(min(Y2)+1.0)
    int imin = 0; int minval = 0;
    for(int i = 1; i < Y2.size(); ++i){
        if(Y2[i] < minval){
            imin = i;
            minval = Y2[imin];
        }
    }
    return 1.0/double(minval+1.0);
}

int main(){
    for(double a1 = 0.0, h1 = 10.0; a1 < 1.0; a1 += h1){ // for a1 in A1
        vector<int> Y2;
        for(int a2 = 2, h2 = 1; a2 <= (int)(5*a1+10); a2 += h2){ // for a2 in A2(a1)
            vector<double> Y3;
            for(double a3 = 6.0, h3 = 1.0; a3 >= (a1+a2); a3 -= 0.5*h3){ // for a3 in A2(a1, a2)
                h3 = f(a1, a2, a3);
                Y3.push_back(h3);
            }
            h2 = g2(Y3);
            Y2.push_back(h2);
        }
        h1 = g1(Y2);
    }

    return 0;
}

我随机选择了这些值,结果f 只被评估了 3 次。请注意,上述代码不可并行化。 假设可以查询循环的增量是否依赖于更高的循环。

我也应该澄清我所追求的。当我最初说结构时,我也许应该说并行化方法或类似的东西。例如,我第一次尝试并行化是将最内层的f 调用放入线程池,并在最内层循环的末尾加入。如上所述,当最内层循环仅迭代一个元素时,这不起作用。这实际上并不需要对现有代码进行重大重组,如果可能的话,我想避免它。

【问题讨论】:

    标签: multithreading parallel-processing


    【解决方案1】:

    您可以尝试以 map-reduce 问题 (http://en.wikipedia.org/wiki/MapReduce) 的形式表达您的问题,使每个级别的嵌套都成为一个 map-reduce 作业。 for 循环将转换为映射,而 g_i 调用将转换为归约步骤。

    您可以尝试使您的伪语言更清晰...也许将其表示为 n=3 或 n=4 的 python 程序?你的“for”是一个普通的for 循环吗?如果是这样,我真的不明白第一对括号。

    我不确定您的问题是否可以以所述形式并行化。如果你说循环的变量依赖于之前的迭代,那么在我看来它更像是一个顺序问题。

    【讨论】:

    • 这正是我要做的。如果您可以安排它,以便每个步骤都可以使用消息语义完成,那会让您的生活更轻松。
    • 你是对的,如果循环的变量严格依赖于前一次迭代,它就不能并行化。但有时它只依赖于 3 次迭代前的值,或者根本不依赖。假设您能够查询是否存在这样的依赖关系,是否有可能比简单的顺序做得更好?问题是,如果每个 for 循环都超过一个单例(或一个小集合),我不想特殊情况。
    • 试试 Haskell 的元编译器。它可能会在没有提示的情况下推断迭代之间的数据依赖性并并行化代码,无论您的代码代表什么特殊情况。然后你(可能)不必从头开始为每个案例显式编写并行代码。
    【解决方案2】:

    说实话,你的符号乍一看很难理解(至少对我来说)。也许如果您可以更明确或可能使用 C 或 C++ 代码。您的并行化方法是什么(pthreads、openmp 等)?我怀疑一个问题是您可以改善负载平衡。例如,您可能不想以发牌方式将工作分配给线程。

    【讨论】:

    • 我目前使用 pthreads。关于符号,是的,我知道。问题是所有集合都包含不同类型的对象,因此以类似 C 的方式编写它需要很多额外的细节。 Iterate 中的 += 应该真正表示“添加到集合”。
    【解决方案3】:

    如果可能的话,加速像这样的深度嵌套调用集的最佳方法是不使用深度嵌套调用集。

    您通常可以重新组织您的数据或数据中的链接,以获得可以节省您循环的参考,或者您有时可以找到一种方法来一个接一个地排列循环,存储中间信息.有时甚至需要创建不同的对象结构。

    我并不是说这总是有效,但即使删除一个级别也会比您可能尝试的任何其他方法更显着地减少时间。

    如果我能理解你的伪代码,我可能会试一试,但我猜你已经抽象出大部分结构,无论如何,这些结构都是有见地的设计所需的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-11-18
      • 1970-01-01
      • 2020-06-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多