Распараллеливание вычисления интеграла

#c #math #parallel-processing #mpi

#c #математика #параллельная обработка #mpi

Вопрос:

Здесь у меня есть фрагмент кода, который является функцией для вычисления интеграла от функции. В коде function() определяется как интегрируемая функция.

Я изучаю параллельное программирование, и мне нужно писать этот код параллельно. Исходная программа является последовательной, потому что на каждой итерации выполняется операция отправки на другой процессор. Чего я хочу добиться, чтобы сделать его параллельным, так это того, что каждая итерация цикла 3 операции отправки выполняются на другие 3 доступных процессора. Представьте себе 1 процессор, который разделяет задачи (ранг = 0), и 3 других процессора, которые выполняют фактические вычисления.

Будьте осторожны, это большой фрагмент кода, но я также включил комментарии, чтобы сделать его более понятным:

Последовательный код:

     if (myRank == 0)
    {
        // I am the controller, distribute the work
        for (step = 0; step < maxSteps; step  )
        {
            x[0] = x_start   stepSize*step;
            x[1] = x_start   stepSize*(step 1);
            nextRank = step % (numProcs-1)   1;
            // Send the work
            MPI_Send(x, 2, MPI_DOUBLE, nextRank, TAG_WORK, MPI_COMM_WORLD);
            // Receive the result
            MPI_Recv(y, 2, MPI_DOUBLE, nextRank, TAG_WORK, MPI_COMM_WORLD,
                MPI_STATUS_IGNORE);
            sum  = stepSize*0.5*(y[0] y[1]);
        }
        // Signal workers to stop by sending empty messages with tag TAG_END
        for (nextRank = 1; nextRank < numProcs; nextRank  )
            MPI_Send(amp;nextRank, 0, MPI_INT, nextRank, TAG_END, MPI_COMM_WORLD);
    }
    else
    {
        while (1)
        {
            // I am a worker, wait for work

            // Receive the left and right points of the trapezoid and compute
            // the corresponding function values. If the tag is TAG_END, don't
            // compute but exit.
            MPI_Recv(x, 2, MPI_DOUBLE, 0, MPI_ANY_TAG, MPI_COMM_WORLD,
                amp;status);
            if (status.MPI_TAG == TAG_END) break;
            y[0] = f(x[0]);
            y[1] = f(x[1]);
            // Send back the computed result
            MPI_Send(y, 2, MPI_DOUBLE, 0, TAG_WORK, MPI_COMM_WORLD);
        }
    }
    return sum;
}
  

Чтобы распараллелить его, я действительно жестко запрограммировал его, чтобы было ясно, что я делаю. Я увеличил цикл с шагом 3. Я добавил новые массивы для хранения значений x и y. Что я сделал, так это сначала собрал значения x в определенном массиве. Затем я отправляю каждый массив значений x на новый процессор. Затем я выполняю другую функцию для получения y-значений. Затем я отправляю их обратно в процессор (ранг = 0), чтобы добавить все «срезы интеграции».

Попытка распараллелить код

  if (myRank == 0)
    {
        // I am the controller, distribute the work
        for (step = 0; step < maxSteps; step 3)
        {
            x1[0] = x_start   stepSize*step;
            x1[1] = x_start   stepSize*(step 1);
            x2[0] = x_start   stepSize*(step 1);
            x2[1] = x_start   stepSize*((step 1) 1);
            x3[0] = x_start   stepSize*(step 2);
            x3[1] = x_start   stepSize*((step 1) 2);
            nextRank = step % (numProcs-1)   1;
            // Send the work
            MPI_Send(x1, 2, MPI_DOUBLE, 1, TAG_WORK, MPI_COMM_WORLD);
            MPI_Send(x2, 2, MPI_DOUBLE, 2, TAG_WORK, MPI_COMM_WORLD);
            MPI_Send(x3, 2, MPI_DOUBLE, 3, TAG_WORK, MPI_COMM_WORLD);
            // Receive the result
            MPI_Recv(y1, 2, MPI_DOUBLE, 1, TAG_WORK, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
            sum  = stepSize*0.5*(y1[0] y1[1]);
            MPI_Recv(y2, 2, MPI_DOUBLE, 2, TAG_WORK, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
            sum  = stepSize*0.5*(y2[0] y2[1]);
            MPI_Recv(y3, 2, MPI_DOUBLE, 3, TAG_WORK, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
            sum  = stepSize*0.5*(y3[0] y3[1]);
        }
        // Signal workers to stop by sending empty messages with tag TAG_END
        for (nextRank = 1; nextRank < numProcs; nextRank  )
            MPI_Send(amp;nextRank, 0, MPI_INT, nextRank, TAG_END, MPI_COMM_WORLD);
    }
    else if (myRank = 1)
    {
        while (1)
        {
            MPI_Recv(x1, 2, MPI_DOUBLE, 0, MPI_ANY_TAG, MPI_COMM_WORLD, amp;status);
            if (status.MPI_TAG == TAG_END) break;
            y1[0] = func(x1[0]);
            y1[1] = func(x1[1]);
            // Send back the computed result
            MPI_Send(y1, 2, MPI_DOUBLE, 0, TAG_WORK, MPI_COMM_WORLD);
        }
    }
    
    else if (myRank = 2)
    {
        while (1)
        {
            MPI_Recv(x2, 2, MPI_DOUBLE, 0, MPI_ANY_TAG, MPI_COMM_WORLD, amp;status);
            if (status.MPI_TAG == TAG_END) break;
            y2[0] = func(x2[0]);
            y2[1] = func(x2[1]);
            // Send back the computed result
            MPI_Send(y2, 2, MPI_DOUBLE, 0, TAG_WORK, MPI_COMM_WORLD);
        }
    }
    
    else if (myRank = 3)
    {
        while (1)
        {
            MPI_Recv(x3, 2, MPI_DOUBLE, 0, MPI_ANY_TAG, MPI_COMM_WORLD, amp;status);
            if (status.MPI_TAG == TAG_END) break;
            y3[0] = func(x3[0]);
            y3[1] = func(x3[1]);
            // Send back the computed result
            MPI_Send(y3, 2, MPI_DOUBLE, 0, TAG_WORK, MPI_COMM_WORLD);
        }
    }
    return sum;
}
  

Проблема в том, что я больше не получаю выходные данные. Боюсь, я создал тупиковую ситуацию, но я не могу обнаружить, где. Могу ли я получить отзывы об этом методе?

источник: https://doc.itc.rwth-aachen.de/display/VE/PPCES 2012

Комментарии:

1. = является оператором присваивания в C. Используется == для проверки равенства.

2. Не могли бы вы быть так любезны, чтобы удовлетворить мое любопытство и сообщить мне, откуда вы получили исходный код? Я спрашиваю, потому что он датируется 2012 годом и взят из части MPI PPCES 2012 — ежегодного учебного пособия для пользователей кластера RWTH Университета Аахена. Большая часть логики кода взята у других до меня, но очистка кода и длинные комментарии, безусловно, мои… Кстати, ответ на ваш вопрос содержится в решениях, которые должны быть доступны на веб-сайте PPCES. То же самое относится и к некоторым другим вашим вопросам о MPI 🙂

3. Приятно слышать, что другие находят материалы из PPCES полезными. Я надеюсь, что ваш инструктор (ы) дал ребятам из RWTH совет по шляпе, поскольку они оказывают сообществу большую услугу, делая все материалы доступными для всех в Интернете.

4. Проблема исходного кода заключается в том, что каждый раз, когда рабочий элемент отправляется на рабочий ранг, контроллер ожидает получения результата обратно, прежде чем отправлять другой рабочий элемент. Это ничем не отличается от последовательного выполнения, просто вычисление выгружается на другой ранг. Вам нужно изменить его таким образом, чтобы позволить нескольким рядам работать с рабочими элементами одновременно и не оставлять пробелов в работе. Отправка задач во все ранги на каждой итерации лишь немного улучшит ситуацию, поскольку более быстрым рангам все равно придется ждать самого медленного в текущей итерации.

5. Типичная реализация этого шаблона «контроллер-рабочий» (ранее известного как «ведущий-ведомый») заключается в том, что контроллер сначала отправляет задачи всем рабочим, а затем входит в цикл, в котором он прослушивает результаты от любого ранга рабочего. Как только он получает результат, контроллер проверяет, есть ли еще рабочие элементы для обработки, и если это так, он отправляет новый рабочий элемент тому же рабочему, от которого он получил результат. Если больше нет работы, контроллер отправляет сообщение об остановке.

Ответ №1:

Если вы хотите получить прибыль от наличия 8 ядер (это только пример), лучшее, что вы можете сделать (и самое простое), это разделить ваш интегральный интервал на восемь частей (вы можете сделать разделение произвольно, чтобы дать каждому одинаковый объем работы, это зависит от вас)а затем вычислите независимо каждый интеграл (с тем же циклом, который у вас был для одного потока) в каждом потоке.

Этот подход не изменяет исходное вычисление и делает вычисления полностью независимыми друг от друга (так что нет никакого конфликта ресурсов вообще)

Наконец, вам нужно только добавить восемь интегралов, чтобы получить желаемый результат.

Если вы думаете о таких вещах, как развертывание циклов для обеспечения большего параллелизма, то вам лучше доверять своему компилятору, который способен параллельно использовать свой оптимизатор, чтобы получать прибыль от более чем 32 регистров, которые сегодня есть у обычного процессора, и вы, скорее всего, не сделаете это лучше.

Предлагаемый здесь подход преобразует ваш интеграл в 8 различных интегральных вычислений, каждое с разными параметрами и разными значениями, а вычисления в одном потоке не зависят от вычислений в других, поэтому даже в потоковом ядре, основанном на конвейерах, вам никогда не придется переупорядочивать или усложнять инструкции, потому что это очень удобно.легко добавлять инструкции другого потока в конвейер, чтобы не создавать пузырьки. Если у вас 8 ядер, на самом деле более 8 потоков, вычисляющих что-либо, не представляют никакой выгодной задачи.