Ostatnio zaobserwowałem dziwny efekt, gdy zmierzyłem wydajność mojej równoległej aplikacji przy użyciu modułu wieloprocesorowego i mpi4py jako narzędzi komunikacyjnych.Równoległe zastosowanie w Pythonie staje się znacznie wolniejsze w przypadku używania modułu mpi zamiast wieloprocesowego
Aplikacja wykonuje algorytmy ewolucyjne na zestawach danych. Większość operacji jest wykonywana sekwencyjnie, z wyjątkiem oceny. Po zastosowaniu wszystkich operatorów ewolucyjnych, wszystkie osoby muszą otrzymać nowe wartości sprawności, które są wykonywane podczas oceny. Zasadniczo jest to matematyczne obliczenie wykonywane na liście zmiennych (python). Przed oceną zestaw danych jest rozproszony przez rozproszenie mpi lub Pythona, następnie przechodzi do oceny równoległej, a później dane wracają poprzez mechanizm mpi's lub mechanizm Pool.map.
Moja platforma testowa to maszyna wirtualna (virtualbox) z systemem Ubuntu 11.10 z Open MPI 1.4.3 na Core i7 (4/8 rdzenie), 8 GB pamięci RAM i napęd SSD.
To, co uważam za naprawdę zaskakujące, to to, że uzyskuję dobre przyspieszenie, jednak w zależności od narzędzia komunikacji, po pewnym progu procesów, wydajność staje się gorsza. Można to zilustrować na poniższych zdjęciach.
oś Y - czas przetwarzania
oś x - Nr procesów
kolory - wielkość każdego (Nr pływaków)
1) z modułów w systemie wieloprocesorowym - Pool.map
2) za pomocą MPI - Scatter/Zebrać
3) Oba obrazy na górze każdego innego
W pierwszej chwili myślałem, że to wina HyperThreading, bo za duże zestawy danych staje się wolniejszy po osiągnięciu 4 procesy fizyczne (4 rdzenie). Jednak powinno to być również widoczne w przypadku wieloprocesowości, a nie jest. Moim kolejnym przypuszczeniem jest to, że metody komunikacji mpi są znacznie mniej skuteczne niż metody Pythona, jednak trudno mi w to uwierzyć.
Czy ktoś ma jakieś wyjaśnienie tych wyników?
DODANO:
Zaczynam wierzyć, że to wina HyperThreading po wszystkim. Przetestowałem swój kod na maszynie z rdzeniem i5 (2/4 rdzenie), a wydajność jest gorsza w przypadku 3 lub więcej procesów. Jedyne wyjaśnienie, które przychodzi mi na myśl, to to, że i7, którego używam, nie ma wystarczającej ilości zasobów (pamięci podręcznej?), Aby obliczać ocenę równolegle z Hyperthreading i musi zaplanować więcej niż 4 procesy do uruchomienia na 4 fizycznych rdzeniach.
Jednak interesujące jest to, że kiedy używam mpi htop, pokazuje się pełne wykorzystanie wszystkich 8 rdzeni logicznych, co powinno sugerować, że powyższe stwierdzenie jest nieprawidłowe. Z drugiej strony, kiedy używam Pool.Map, nie wykorzystuje ona w pełni wszystkich rdzeni. Używa jednego lub dwóch do maksimum, a reszta tylko częściowo, znowu nie ma pojęcia, dlaczego zachowuje się w ten sposób. Jutro dołączę zrzut ekranu pokazujący to zachowanie.
Nie robię nic nadzwyczajnego w kodzie, to naprawdę proste (nie podaję całego kodu nie dlatego, że jest tajne, ale dlatego, że potrzebuje dodatkowych bibliotek, takich jak DEAP do zainstalowania .Jeśli ktoś jest naprawdę zainteresowany problem i gotowe do zainstalowania DEAP I mogę przygotować krótki przykład). Kod dla MPI jest nieco inny, ponieważ nie może zajmować się zbiornikiem populacji (który dziedziczy z listy). Oczywiście jest trochę na głowie, ale nic poważnego. Oprócz kodu, który pokazuję poniżej, reszta jest taka sama.
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 - Rozproszenie/Zbierz
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)
# ...
dodano 2: Jak już wspomniano wcześniej zrobiłem kilka testów na moim komputerze i5 (2/4 rdzenie) i tutaj jest wynik:
również znaleźć urządzenia 2 Xeony (2 x 6/12 rdzeni) i powtarza odniesienia:
teraz mam 3 przykłady samym zachowanie. Kiedy przeprowadzam moje obliczenia w większej liczbie procesów niż fizyczne rdzenie, zaczyna się to pogarszać. Uważam, że dzieje się tak, ponieważ procesy na tym samym rdzeniu fizycznym nie mogą być wykonywane jednocześnie z powodu braku zasobów.
Implementacje MPI zwykle mają wiele różnych algorytmów dla operacji zbiorowych takich jak rozproszenie i zbieranie. Większość bibliotek ma własną heurystykę, aby wybrać najlepszy algorytm, ale czasami kończy się niepowodzeniem. Niektóre biblioteki umożliwiają wymuszanie tych algorytmów, np. w Open MPI można to osiągnąć przekazując argumenty MCA do 'mpiexec'. Pomoże nam, jeśli powiesz nam, z jakiej implementacji MPI korzystasz. –
Używam Open MPI, dodanego do pytania. – Michal
Byłoby wspaniale, gdyby można było podać więcej kontekstu, na przykład pokazać części kodu, w którym dane są dystrybuowane (obie implementacje). Zwykle rozpraszają i zbierają w skali Open MPI bardzo dobrze z liczbą procesów, gdy wszystkie są uruchomione na jednym węźle. –