В последнее время я наблюдал странный эффект, когда измерял производительность моего параллельного приложения с использованием модуля многопроцессорности и mpi4py в качестве средств связи.
Приложение выполняет эволюционные алгоритмы на наборах данных. Большинство операций выполняются последовательно, за исключением оценки. После применения всех эволюционных операторов все люди должны получать новые значения пригодности, которые выполняются во время оценки. В основном это просто математический расчет, выполненный в списке float (python). Перед оценкой набор данных рассеивается либо разбросом mpi, либо пулом Pool.map, затем идет параллельная оценка, а затем данные возвращаются через сбор mpi или снова в механизм Pool.map.
Моя тестовая платформа - это виртуальная машина (виртуальная машина), работающая под управлением Ubuntu 11.10 с Open MPI 1.4.3 на Core i7 (4/8 ядер), 8 ГБ оперативной памяти и SSD-накопитель.
То, что я нахожу, действительно удивительно, заключается в том, что я получаю приятное ускорение, однако, в зависимости от инструмента связи, после определенного порога процессов производительность ухудшается. Это можно проиллюстрировать на рисунках ниже.
y ось - время обработки
x ось - nr процессов
цвета - размер каждого человека (nr поплавков)
1) Использование модуля многопроцессорности - Pool.map
2) Использование mpi - Scatter/Gather
3) Обе фотографии друг на друга
Сначала я думал, что он вызывает гиперпотоки, потому что для больших наборов данных он становится медленнее после достижения 4 процессов (4 физических ядра). Однако он также должен быть виден в многопроцессорном корпусе, и это не так. Еще одна догадка заключается в том, что методы коммуникации mpi гораздо менее эффективны, чем python, однако мне трудно поверить.
Есть ли у кого-нибудь объяснение этих результатов?
ДОБАВЛЕНО:
Я начинаю верить, что это ошибка Hyperthreading. Я тестировал свой код на машине с ядром i5 (2/4 ядра), а производительность хуже с 3 или более процессами. Единственное объяснение, которое приходит мне на ум, заключается в том, что i7, который я использую, не имеет достаточного количества ресурсов (кэш?) Для вычисления оценки одновременно с Hyperthreading и требует запланировать более 4 процессов для работы на 4 физических ядрах.
Однако интересно то, что, когда я использую mpi htop, показано полное использование всех 8 логических ядер, что должно предполагать, что приведенное выше утверждение неверно. С другой стороны, когда я использую Pool.Map, он не полностью использует все ядра. Он использует один или два до максимума, а остальные только частично, опять же не знаю, почему он ведет себя таким образом. Завтра я приложу скриншот, показывающий это поведение.
Я не делаю ничего необычного в коде, это очень просто (я не даю весь код не потому, что он секретный, а потому, что ему нужны дополнительные библиотеки, такие как DEAP. Если кто-то действительно интересуется проблема и готов к установке DEAP, я могу подготовить короткий пример). Код для MPI немного отличается, потому что он не может иметь дело с контейнером для населения (который наследуется из списка). Конечно, есть некоторые накладные расходы, но ничего серьезного. Помимо кода, который я покажу ниже, остальная часть этого же текста.
Pool.map:
def eval_population(func, pop):
for ind in pop:
ind.fitness.values = func(ind)
return pop
# ...
self.pool = Pool(8)
# ...
for iter_ in xrange(nr_of_generations):
# ...
self.pool.map(evaluate, pop) # evaluate is really an eval_population alias with a certain function assigned to its first argument.
# ...
MPI - Scatter/Gather
def divide_list(lst, n):
return [lst[i::n] for i in xrange(n)]
def chain_list(lst):
return list(chain.from_iterable(lst))
def evaluate_individuals_in_groups(func, rank, individuals):
comm = MPI.COMM_WORLD
size = MPI.COMM_WORLD.Get_size()
packages = None
if not rank:
packages = divide_list(individuals, size)
ind_for_eval = comm.scatter(packages)
eval_population(func, ind_for_eval)
pop_with_fit = comm.gather(ind_for_eval)
if not rank:
pop_with_fit = chain_list(pop_with_fit)
for index, elem in enumerate(pop_with_fit):
individuals[index] = elem
for iter_ in xrange(nr_of_generations):
# ...
evaluate_individuals_in_groups(self.func, self.rank, pop)
# ...
ДОБАВЛЕНО 2: Как я уже упоминал ранее, я провел несколько тестов на своей машине i5 (2/4 ядра), и вот результат:
Я также нашел машину с 2 хэонами (2x 6/12 ядер) и повторил эталон:
Теперь у меня есть 3 примера такого же поведения. Когда я запускаю вычисления в большем количестве процессов, чем физические ядра, он начинает ухудшаться. Я считаю, потому что процессы на одном физическом ядре не могут выполняться одновременно из-за нехватки ресурсов.