После множества проб и ошибок (и других отзывов) я обнаружил, что наиболее производительное решение использует асинхронные трансляции.
Мой первый подход был вдохновлен ответом Петру с использованием нескольких неблокирующих вызовов MPI_Isend() для распространения данных и только одного единственного вызова MPI_Irecv() (с любым источником) для периодического получения данных. Этот подход оказался довольно медленным, поскольку неблокирующие вызовы MPI_ISend() должны проверяться отправителем с помощью MPI_Test() для каждого отдельного созданного дескриптора MPI_Request. Причина этого в том, что асинхронные операции на самом деле не являются асинхронными в том смысле, что они работают в фоновом режиме, а должны периодически проверяться.
В моем случае проблема также представляет возможность того, что новые решения могут (и будут) найдены несколько раз, что привело к множеству проблем с управлением существующими открытыми дескрипторами MPI_Request, которые приходилось ждать или управлять ими иначе.
Мой второй подход заключался в использовании центрального коммуникатора, предложенного Зуланом. Этот подход был очень быстрым, когда сообщений было немного, но корневой ранг забивался, когда было много найденных решений одновременно, что делало его очень медленным в особых случаях. Самое главное, корневой ранг был уже не таким быстрым, как другие (чего и следовало ожидать), что привело к общему замедлению программы в моем случае.
Мой третий и последний подход заключался в использовании нескольких неблокирующих вызовов MPI_Ibcast(), по одному для каждого возможного ранга, который мог отправить сообщение. Например, это будет означать, что в случае с 3 разными рангами, которые
- ранг 0 имеет два открытых MPI_Ibcasts с корнем 1 и 2
- ранг 1 имеет два открытых MPI_Ibcasts с корнем 0 и 2
- ранг 2 имеет два открытых MPI_Ibcasts с корнем 0 и 1
Когда ранг затем находит решение, он отправляет последний необходимый MPI_Ibcast с собой в качестве корня. На первый взгляд это может показаться похожим на первый подход, однако в этом случае отправителю всегда нужно отслеживать только один дескриптор запроса (тот, что из финального MPI_Ibcast) и периодически проверять его с помощью MPI_Test(). Другие ранги всегда имеют одинаковое количество открытых трансляций (а именно размер мира минус 1), которые можно сохранить в массиве и проверить с помощью MPI_Testany(). Однако сложность с этим подходом заключается в том, что каждая открытая трансляция должна работать на своем собственном коммуникаторе, по сути, требующем столько коммуникаторов, сколько существует рангов.
Итак, мой вывод заключается в том, что второй подход иногда приемлем, что делает его быстрым и жизнеспособным вариантом, когда сообщений не так много. Это было самым простым в обработке и реализации. Третий подход был быстрее при более высокой нагрузке, а также очень упростил завершение моей программы, поскольку всегда есть известное количество открытых соединений. Это была самая сложная и длинная реализация, и она использовала немного больше памяти, чем другие реализации. (Я не знаю причины этого, но, похоже, это как-то связано с управлением буфером вещания и, возможно, с дополнительными коммуникаторами.) Наконец, я не могу не подчеркнуть, что первый подход сначала кажется простым, однако, когда вы действительно хотите отслеживать каждый запрос, то, к сожалению, это сложно и медленно.
08.09.2017