Плохая производительность с MPI
Я изучаю MPI, и у меня есть вопрос о почти полном отсутствии прироста производительности в простой реализации, представленной ниже.
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int mpirank, mpisize;
int tabsize = atoi(*(argv + 1));
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &mpirank);
MPI_Comm_size(MPI_COMM_WORLD, &mpisize);
unsigned long int sum = 0;
int rcvsize = tabsize / mpisize;
int *rcvbuf = malloc(rcvsize * sizeof(int));
int *tab = malloc(tabsize * sizeof(int));
int totalsum = 0;
if(mpirank == 0){
for(int i=0; i < tabsize; i++){
*(tab + i) = 1;
}
}
MPI_Scatter(tab, tabsize/mpisize, MPI_INT, rcvbuf, tabsize/mpisize, MPI_INT, 0, MPI_COMM_WORLD);
for(int i=0; i < tabsize/mpisize; i++){
sum += *(rcvbuf + i);
}
MPI_Reduce(&sum, &totalsum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
if(mpirank == 0){
printf("The totalsum = %li\n", totalsum);
}
MPI_Finalize();
return 0;
}
Время выполнения вышеуказанной реализации:
$ /usr/bin/time mpirun -np 1 test1 2000000000 The totalsum = 2000000000 13.76user 3.31system 0:17.30elapsed 98%CPU (0avgtext+0avgdata 15629824maxresident)k 0inputs+8outputs (0major+21720minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 1 test1 2000000000
The totalsum = 2000000000
13.78user 3.29system 0:17.31elapsed 98%CPU (0avgtext+0avgdata 15629824maxresident)k 0inputs+8outputs (0major+21717minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 1 test1 2000000000 The totalsum = 2000000000 13.78user 3.32system 0:17.33elapsed 98%CPU (0avgtext+0avgdata 15629828maxresident)k 0inputs+8outputs (0major+20697minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 20 test1 2000000000
The totalsum = 2000000000
218.42user 6.10system 0:12.99elapsed 1727%CPU (0avgtext+0avgdata 8209484maxresident)k 0inputs+17400outputs (118major+82587minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 20 test1 2000000000 The totalsum = 2000000000 216.17user 6.37system 0:12.89elapsed 1726%CPU (0avgtext+0avgdata 8209488maxresident)k 0inputs+17168outputs (126major+81092minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 20 test1 2000000000
The totalsum = 2000000000
216.16user 6.09system 0:12.88elapsed 1724%CPU (0avgtext+0avgdata 8209492maxresident)k 0inputs+17192outputs (111major+81665minor)pagefaults 0swaps
Это дает прирост производительности всего около 25%. Я предполагаю, что узкое место может быть вызвано процессами, которые конкурируют за доступ к памяти. Затем я попробовал то же самое, но без использования памяти для доступа к данным.
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
int main(int argc, char **argv)
{
int mpirank, mpisize;
int tabsize = atoi(*(argv + 1));
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &mpirank);
MPI_Comm_size(MPI_COMM_WORLD, &mpisize);
unsigned long int sum = 0;
for(int i=0; i < tabsize/mpisize; i++){
sum += 1;
}
MPI_Reduce(&sum, &totalsum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
if(mpirank == 0){
printf("The totalsum = %li\n", totalsum);
}
MPI_Finalize();
return 0;
}
что дало следующие результаты:
$ /usr/bin/time mpirun -np 1 test2 2000000000 The totalsum = 2000000000 6.17user 0.11system 0:06.49elapsed 96%CPU (0avgtext+0avgdata 5660maxresident)k 0inputs+8outputs (0major+4005minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 1 test2 2000000000
The totalsum = 2000000000
6.16user 0.12system 0:06.49elapsed 96%CPU (0avgtext+0avgdata 5660maxresident)k 0inputs+8outputs (0major+4007minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 1 test2 2000000000 The totalsum = 2000000000 6.15user 0.11system 0:06.47elapsed 96%CPU (0avgtext+0avgdata 5664maxresident)k 0inputs+8outputs (0major+4005minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 20 test2 2000000000
The totalsum = 2000000000
8.67user 2.41system 0:01.06elapsed 1040%CPU (0avgtext+0avgdata 6020maxresident)k 0inputs+16824outputs (128major+49952minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 20 test2 2000000000 The totalsum = 2000000000 8.59user 2.74system 0:01.05elapsed 1076%CPU (0avgtext+0avgdata 6028maxresident)k 0inputs+16792outputs (131major+49960minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 20 test2 2000000000
The totalsum = 2000000000
8.65user 2.61system 0:01.06elapsed 1058%CPU (0avgtext+0avgdata 6024maxresident)k 0inputs+16792outputs (116major+50002minor)pagefaults 0swaps
Это показывает прирост производительности примерно на 83% и подтверждает мои предположения. Тогда не могли бы вы сказать мне, верны ли мои предположения и если да, то есть ли способы улучшить первую реализацию с доступом к памяти?
Код был запущен на машине с 20 физическими ядрами.
EDIT1: дополнительные результаты первой реализации для 2, 5 и 10 процессов:
$ /usr/bin/time mpirun -np 2 test1 2000000000 The totalsum = 2000000000 24.05user 3.40system 0:14.03elapsed 195%CPU (0avgtext+0avgdata 11724552maxresident)k 0inputs+960outputs (6major+23195minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 5 test1 2000000000
The totalsum = 2000000000
55.27user 3.54system 0:12.88elapsed 456%CPU (0avgtext+0avgdata 9381132maxresident)k 0inputs+4512outputs (26major+31614minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 10 test1 2000000000
The totalsum = 2000000000
106.43user 4.07system 0:12.44elapsed 887%CPU (0avgtext+0avgdata 8599952maxresident)k 0inputs+8720outputs (51major+50059minor)pagefaults 0swaps
РЕДАКТИРОВАТЬ2:
Я поместил MPI_Wtime () для измерения части MPI_Scatter первой реализации следующим образом:
...
for(int i=0; i < tabsize; i++){
*(tab + i) = 1;
}
}
MPI_Barrier(MPI_COMM_WORLD);
double start = MPI_Wtime();
MPI_Scatter(tab, tabsize/mpisize, MPI_INT, rcvbuf, tabsize/mpisize, MPI_INT, 0, MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD);
double end = MPI_Wtime();
for(int i=0; i < tabsize/mpisize; i++){
sum += *(rcvbuf + i);
...
и получили следующие результаты:
$ /usr/bin/time mpirun -np 1 test1 400000000
The MPI_Scatter time = 0.576 (14% of total)
3.13user 0.74system 0:04.08elapsed 95%CPU
$ /usr/bin/time mpirun -np 2 test1 400000000 The MPI_Scatter time = 0.580 (18% of total) 5.19user 0.79system 0:03.25elapsed 183%CPU $ /usr/bin/time mpirun -np 4 test1 400000000
The MPI_Scatter time = 0.693 (22.5% of total)
9.99user 1.05system 0:03.07elapsed 360%CPU
$ /usr/bin/time mpirun -np 5 test1 400000000 The MPI_Scatter time = 0.669 (22.3% of total) 12.41user 1.01system 0:03.00elapsed 446%CPU $ /usr/bin/time mpirun -np 8 test1 400000000
The MPI_Scatter time = 0.696 (23.7% of total)
19.67user 1.25system 0:02.95elapsed 709%CPU
$ /usr/bin/time mpirun -np 10 test1 400000000 The MPI_Scatter time = 0.701 (24% of total) 24.21user 1.45system 0:02.92elapsed 876%CPU $ /usr/bin/time mpirun -np 1 test1 1000000000
The MPI_Scatter time = 1.434 (15% of total)
7.64user 1.71system 0:09.57elapsed 97%CPU
$ /usr/bin/time mpirun -np 2 test1 1000000000 The MPI_Scatter time = 1.441 (19% of total) 12.72user 1.75system 0:07.52elapsed 192%CPU $ /usr/bin/time mpirun -np 4 test1 1000000000
The MPI_Scatter time = 1.710 (25% of total)
24.16user 1.93system 0:06.84elapsed 381%CPU
$ /usr/bin/time mpirun -np 5 test1 1000000000 The MPI_Scatter time = 1.675 (25% of total) 30.29user 2.10system 0:06.81elapsed 475%CPU $ /usr/bin/time mpirun -np 10 test1 1000000000
The MPI_Scatter time = 1.753 (26.6% of total)
59.89user 2.47system 0:06.60elapsed 943%CPU
$ /usr/bin/time mpirun -np 10 test1 100000000 The MPI_Scatter time = 0.182 (15.8% of total) 6.75user 1.07system 0:01.15elapsed 679%CPU $ /usr/bin/time mpirun -np 10 test1 200000000
The MPI_Scatter time = 0.354 (20% of total)
12.50user 1.12system 0:01.71elapsed 796%CPU
$ /usr/bin/time mpirun -np 10 test1 300000000 The MPI_Scatter time = 0.533 (22.8% of total) 18.54user 1.30system 0:02.33elapsed 849%CPU $ /usr/bin/time mpirun -np 10 test1 400000000
The MPI_Scatter time = 0.702 (23.95% of total)
24.38user 1.37system 0:02.93elapsed 879%CPU
$ /usr/bin/time mpirun -np 10 test1 1000000000
The MPI_Scatter time = 1.762 (26% of total)
60.17user 2.42system 0:06.62elapsed 944%CPU
Ответы
Это дает прирост производительности всего около 25%. Я предполагаю, что узкое место может быть вызвано процессами, которые конкурируют за доступ к памяти. (..)
Ваш код в основном связан с коммуникацией и процессором. Более того, по вашим результатам для 2, 5 и 10 процессов:
$ /usr/bin/time mpirun -np 2 test1 2000000000 The totalsum = 2000000000 24.05user 3.40system 0:14.03elapsed 195%CPU (0avgtext+0avgdata 11724552maxresident)k 0inputs+960outputs (6major+23195minor)pagefaults 0swaps $ /usr/bin/time mpirun -np 5 test1 2000000000
The totalsum = 2000000000
55.27user 3.54system 0:12.88elapsed 456%CPU (0avgtext+0avgdata 9381132maxresident)k 0inputs+4512outputs (26major+31614minor)pagefaults 0swaps
$ /usr/bin/time mpirun -np 10 test1 2000000000
The totalsum = 2000000000
106.43user 4.07system 0:12.44elapsed 887%CPU (0avgtext+0avgdata 8599952maxresident)k 0inputs+8720outputs (51major+50059minor)pagefaults 0swaps
Код перестает масштабироваться уже примерно на пяти процессах, маловероятно (на данный момент) для ширины границы памяти, которая будет насыщена.
Затем я попробовал то же самое, но без использования памяти для доступа к данным. (..) Это показывает прирост производительности примерно на 83% и подтверждает мои предположения.
Но вы также удалили MPI_Scatter
звонок. Следовательно, сокращение накладных расходов на связь при сохранении в основном того же объема работы, выполняемой параллельно.
Я профилировал ваш код на своей машине (2 физических ядра; 4 логических). Для измерения времени я использую MPI_Wtime();
следующее:
int main(int argc, char **argv)
{
int mpirank, mpisize;
int tabsize = atoi(*(argv + 1));
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &mpirank);
MPI_Comm_size(MPI_COMM_WORLD, &mpisize);
MPI_Barrier(MPI_COMM_WORLD);
double start = MPI_Wtime();
...
if(mpirank == 0){
printf("The totalsum = %li\n", totalsum);
}
MPI_Barrier(MPI_COMM_WORLD);
double end = MPI_Wtime();
if(mpirank == 0)
printf("Time:%f\n",end-start);
}
для ввода, равного вашему ( т.е. 2000000000), результаты были:
1 process : 25.158740 seconds
2 processes : 19.116490 seconds
4 processes : 15.971734 seconds
Улучшение примерно на 40%, и иерархия памяти моей машины должна намного уступать машине с 20 физическими ядрами.
Давайте теперь значительно уменьшим размер ввода, тем самым уменьшив объем памяти с 2000000000 (8 гигабайт) до всего 250000000 (1 гигабайт), и снова протестируем:
1 process : 1.312354 seconds
2 processes : 1.229174 seconds
4 processes : 1.232522 seconds
Улучшение примерно на 6%; Если бы узким местом были процессы, конкурирующие за память, я бы не ожидал такого снижения скорости после уменьшения объема памяти. Тем не менее, это сокращение можно легко объяснить тем фактом, что, уменьшив размер входных данных, я увеличил соотношение обмена данными на вычисление.
Вернемся к тестам с 2000000000 элементов, но на этот раз измеряем время, затраченное на MPI_Scatter
процедуру связи (ту, которую вы удалили):
2 processes : 7.487354 seconds
4 processes : 8.728969 seconds
Как можно видеть с 2 и 4 процессами, приблизительно 40% ( то есть 7,487354 / 19,116490) и 54% ( то есть 8,728969 / 15,971734) времени выполнения приложения было потрачено в MPI_Scatter
одиночку, соответственно. Вот почему, когда вы удалили этот распорядок, вы заметили улучшение ускорения.
Теперь такой же тест для входа 250000000 (1 гигабайт):
2 processes ::0.679913 seconds (55% of the time)
4 processes : 0.691987 seconds (56% of the time)
Как видите, даже при меньшем объеме памяти, накладные расходы MPI_scatter
оставались в процентном отношении примерно такими же (для 4 процессов). Вывод состоит в том, что чем больше процессов, тем меньше вычислений на процесс и, следовательно, тем выше соотношение обмена данными на одно вычисление - исключая другие накладные расходы, которые могут возникнуть при большем количестве запущенных процессов. Более того, в вашем коде с большим количеством процессов использование памяти не растет линейно, за исключением основного процесса (который содержит все данные) процессы расширения будут иметь разброс данных между ними.
Как правило, хорошая MPI_scatter
реализация будет иметь временную сложность O (n log p) с n
размером входных данных и p
количеством процессов. Следовательно, накладные расходы MPI_scatter
будут увеличиваться быстрее за счет увеличения размера ввода, а затем за счет увеличения количества процессов, участвующих в этом обмене данными. Однако, увеличивая размер ввода, вы получаете больше вычислений на каждый процесс, выполняемый параллельно, тогда как если вы увеличиваете количество процессов, у вас будет меньше вычислений на каждый выполняемый процесс.
Однако имейте в виду, что тесты, которые я провел, не являются самыми точными из-за среды, в которой я работаю, моя реализация MPI может отличаться от вашей и так далее. Тем не менее, я уверен, что если вы проведете те же тесты на своей установке, вы сделаете те же выводы.