【问题标题】:Partially parallel loops using openmp tasks使用 openmp 任务的部分并行循环
【发布时间】:2015-12-22 14:21:41
【问题描述】:

先决条件:

  • 并行引擎:OpenMP 3.1+(如果需要,可以是 OpenMP 4.0)
  • 并行结构:OpenMP 任务
  • 编译器:gcc 4.9.x(支持 OpenMP 4.0)

输入:

  • 带循环的 C 代码
  • 循环有交叉迭代数据依赖(ies):“i+1”迭代需要来自“i”迭代的数据(只有这种依赖,没有别的)
  • 循环体可以部分依赖
  • 循环不能分成两个循环;循环体应保持实心
  • 任何合理的东西都可以添加到循环或循环体函数定义中

代码示例:

(此处 conf/config/configData 变量仅用于说明目的,主要关注的是 value/valueData 变量。)

void loopFunc(const char* config, int* value)
{
    int conf;
    conf = prepare(config);         // independent, does not change “config”
    *value = process(conf, *value); // dependent, takes prev., produce next
    return;
}

int main()
{
    int N = 100;
    char* configData;           // never changes
    int valueData = 0;          // initial value
    …
    for (int i = 0; i < N; i++)
    {
        loopFunc(configData, &valueData);
    }
    …
}

需要:

  • 使用 omp 任务的并行循环(不能使用 omp for / omp 部分)
  • “准备”功能应与其他“准备”或“处理”功能并行执行
  • “进程”函数应根据数据依赖进行排序

已提出和实施的内容:

  • 定义整数标志
  • 为其分配第一次迭代的次数
  • 每次迭代需要数据时都会等待标志等于它的迭代
  • 下一次迭代的数据准备好时更新标志值

像这样:

(我提醒一下,conf/config/configData 变量仅用于说明目的,主要兴趣在于 value/valueData 变量。)

void loopFunc(const char* config, int* value, volatile int *parSync, int iteration)
{
    int conf;
    conf = prepare(config);         // independent, do not change “config”
    while (*parSync != iteration)   // wait for previous to be ready
    {
        #pragma omp taskyield
    }
    *value = process(conf, *value); // dependent, takes prev., produce next
    *parSync = iteration + 1;       // inform next about readiness
    return;
}

int main()
{
    int N = 100;
    char* configData;           // never changes
    int valueData = 0;          // initial value
    volatile int parallelSync = 0;
    …
    omp_set_num_threads(5);
    #pragma omp parallel
    #pragma omp single
    for (int i = 0; i < N; i++)
    {
        #pragma omp task shared(configData, valueData, parallelSync) firstprivate(i)
            loopFunc(configData, &valueData, &parallelSync, i);
    }
    #pragma omp taskwait
    …
}

发生了什么:

失败了。 :)

原因是openmp任务占用了openmp线程。 例如,如果我们定义 5 个 openmp 线程(如上面的代码)。

  • “For”循环生成 100 个任务。
  • OpenMP 运行时将 5 个任意任务分配给 5 个线程并启动这些任务。

如果已启动的任务中没有 i=0 的任务(有时会发生),则执行任务永远等待,永远占用线程,永远不会启动 i=0 的任务。

下一步是什么?

我没有其他想法如何实现所需的计算模式。

当前解决方案

感谢下面@parallelgeek 的想法

int main()
{
    int N = 10;
    char* configData;           // never changes
    int valueData = 0;          // initial value
    volatile int parallelSync = 0;
    int workers;
    volatile int workingTasks = 0;
    ...
    omp_set_num_threads(5);
    #pragma omp parallel
    #pragma omp single
    {
        workers = omp_get_num_threads()-1;  // reserve 1 thread for task generation

        for (int i = 0; i < N; i++)
        {
            while (workingTasks >= workers)
            {
                #pragma omp taskyield
            }

            #pragma omp atomic update
                workingTasks++;

            #pragma omp task shared(configData, valueData, parallelSync, workingTasks) firstprivate(i)
            {
                loopFunc(configData, &valueData, &parallelSync, i);

                #pragma omp atomic update
                    workingTasks--;
            }
        }
        #pragma omp taskwait
    }
}

【问题讨论】:

  • 让我们看看,你声明了 char* configData;,然后你将它传递给 loopFunc() 里面的 loopFunc() 你将它传递给 process(conf, value);*问: process() 到底是什么?
  • @Michi:这里 conf/config/configData 变量仅用于说明目的,主要兴趣在于 value/valueData 变量。更新了帖子。 “准备”说明了与值无关的函数,它需要时间但不与其他迭代交互。 “过程”说明了从上一次迭代中获取数据并为下一次迭代产生新值的函数,
  • @吉尔斯。 “int &xxx”,AFAIK,只能在 C++ 中使用?我需要与 C 兼容......因此“刷新”似乎根本不可能,好像 omp 不允许刷新取消引用的值,如 flush(*parSync)......
  • 让我烦恼的另一件事是标准规定“每当线程到达任务调度点时,实现可能导致它执行任务切换,开始或恢复执行绑定到当前团队的不同任务”。这里没有义务,所以你的代码的行为无论如何都是实现定义的,不是吗?但以防万一,我也会使用OMP_WAIT_POLICY 将其设置为PASSIVE,以防它允许更好的任务调度......
  • @Gilles,是的,解决方案的行为是不可预测的,这是主要问题。这就是为什么我在这里提出这个问题..

标签: c multithreading parallel-processing task openmp


【解决方案1】:
  1. AFAIK volatile 不会阻止硬件重新排序,这就是为什么你 最终可能会导致内存混乱,因为尚未写入数据, 而消费线程已经将标志视为true
  2. 这就是为什么一点建议:改用 C11 原子以确保数据的可见性。如我所见,gcc 4.9 支持 c11 C11Status in GCC
  3. 您可以尝试将生成的任务按K个任务分组,其中K == ThreadNum只有在任何正在运行的任务完成后才开始生成后续任务(生成第一组中的任务后)。因此,您有一个不变量每次只有 K 个任务在 K 个线程上运行和调度
  4. 也可以通过使用 C11 中的原子标志来满足任务间依赖关系。

【讨论】:

    猜你喜欢
    • 2021-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-15
    • 2023-04-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多