Biuletyn nr 37

Biuletyn KDM
1 | 2 | 3 | 4 | 5
6 | 7 | 8 | 9 | 10
11 | 12 | 13 | 14
15 | 16 | 17 | 18
19 | 20 | 21 | 22
23 | 24 | 25 | 26
27 | 28 | 29 | 30
31 | 32
Lista biuletynów

Biuletyn nr 37 (26 listopada 2013)

Spis treści

Zarządzanie procesami i wątkami w zadaniach obliczeniowych na Boreasz-u

Autor: Maciej Cytowski

Wstęp

Węzeł obliczeniowy systemu Boreasz składa się z 4 procesorów Power7 oraz 128 GB pamięci RAM. Każdy procesor Power7 ma 8 fizycznych rdzeni, zaś każdy z nich potrafi sprzętowo obsłużyć do 4 wątków (SMT - Simultaneous Multi-Threading). Boreasz, spośród systemów obliczeniowych dostępnych w ICM, wyróżnia się również najwyższym współczynnikiem przepustowości do pamięci operacyjnej, w ramach pojedynczego węzła. Jednak, tak jak w przypadku większości współczesnych architektur, pamięć operacyjna węzła Power775 podzielona jest na części (w tym przypadku cztery), do których najszybszy dostęp mają wybrane pojedyncze procesory. Każdy procesor ma dostęp do całej przestrzeni pamięci, jednak najszybszy dostęp ma tylko do przypisanej do niego części.

W artykule tym pokażemy w jaki sposób możemy upewnić się, na których procesorach i rdzeniach uruchamiane są nasze procesy czy wątki w ramach węzła obliczeniowego. Umiejętność ta pozwoli naszym aplikacjom wykorzystać w pełni wysoką przepustowość do pamięci systemu Power775. Wiedza ta przyda się zwłaszcza w przypadku, gdy liczba procesów przypadających na węzeł jest mniejsza niż liczba fizycznych rdzeni. Pokażemy również jak sterować przypisaniem wątków do rdzeni w przypadku aplikacji uruchamianych w modelu łączonym, np. MPI+OpenMP czy MPI+Pthreads.

Teoria

Przypisanie procesów i wątków do fizycznych rdzeni można na systemie Power775 uzyskać w kilka sposobów. W tym artykule postaramy się opisać najbardziej przydatne z nich. Przez cały ten artykuł będziemy zakładać, że wszystkie obliczenia realizowane są za pomocą systemu kolejkowego LoadLeveler oraz, że używane przez nas kody zostały skompilowane za pomocą kompilatorów z rodziny IBM XL.

Pojedynczy węzeł systemu Power775 może być różnie nazywany:

  • octant - nazwa używana przez konstruktorów systemu, która podkreśla, że w ramach pojedynczej półki systemu Power775 znajduje się 8 takich węzłów,
  • MCM - czyli Multi-Chip Module, nazwa używana do określenia modułu, w którym kilka układów elektronicznych (np. procesorów) zatopionych jest w jednoczącym je podłożu,
  • QCM - czyli Quad-Chip Module, odmiana MCM, nazwa podkreślająca, że węzeł Power775 zbudowany jest z 4 procesorów, które są wspólnie połączone w jednoczącym je podłożu.

Uwaga: MCM nie zawsze oznacza to samo!

W przypadku terminologii używanej w systemie kolejkowym LoadLeveler określenie MCM oznacza podstawową jednostkę, której może dotyczyć przypisanie części pamięci (z ang. affinity domain), która zawiera pojedynczy procesor Power775 (8 rdzeni, 32 wątki). Możemy przekonać się o tym wywołując komendę llstatus -M. Wypisuje ona wszystkie dostępne do skonfigurowania domeny MCM. Przykładowo dla węzła c9n9-hf0 wyglądają one tak:

c9n9-hf0
            MCM0
               Available Cpus :<  0-31 >(32)
               Used Cpus      :<  >(0)
               Adapters       :
               Total Tasks    :(0)
            MCM1
               Available Cpus :<  32-63 >(32)
               Used Cpus      :<  >(0)
               Adapters       :
               Total Tasks    :(0)
            MCM2
               Available Cpus :<  64-95 >(32)
               Used Cpus      :<  >(0)
               Adapters       :
               Total Tasks    :(0)
            MCM3
               Available Cpus :<  96-127 >(32)
               Used Cpus      :<  >(0)
               Adapters       :
               Total Tasks    :(0)


Jak widać wyżej, LoadLeveler dzieli każdy węzeł systemu Boreasz na 4 domeny MCM. Każda domena zawiera jeden 8-rdzeniowy procesor Power7. Należy również zwrócić uwagę na fakt, że każdy MCM według LoadLeveler-a posiada 32 CPUs. Znów terminologia używana przez LoadLeveler-a może być myląca. Ilość CPU nie oznacza tutaj ani ilości fizycznych procesorów, ani rdzeni. Odnośi się ona do liczby sprzętowych wątków (z ang. hardware threads), które może obsłużyć dany MCM lub węzeł. W tym przypadku liczba ta (32 CPUs/MCM) bierze się z wyliczenia: 1 Power7 * 8 rdzeni Power7 * 4 wątki sprzetowe = 32 CPUs. Każdu rdzeń (core) składa się zatem z czterech CPU:

core0  = cpu0   + cpu1   + cpu2   + cpu3
core1  = cpu4   + cpu5   + cpu6   + cpu7
core2  = cpu8   + cpu9   + cpu10  + cpu11
core3  = cpu12  + cpu13  + cpu14  + cpu15 
..       
core31 = cpu124 + cpu125 + cpu126 + cpu127

Reszta artykułu opierać się będzie na przykładach, w których pokażemy jak dokonywać przypisywania procesów i wątków do poszczególnych fizycznych zasobów węzła Power775. Przykładem, którego użyjemy będzie kod napisany z wykorzystaniem MPI oraz OpenMP, którego głównym elementem jest nieskończona pętla.

<syntaxhighlight lang="c" line start="1">

  1. include <stdio.h>
  2. include "mpi.h"
  3. include <omp.h>

int main(int argc, char *argv[]) {

 int numprocs, rank;
 int thread = 0, numthreads = 1;
 MPI_Init(&argc, &argv);
 MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
 #pragma omp parallel default(shared) private(thread, numthreads)
 {
   int i=0;
   int a;
   numthreads = omp_get_num_threads();
   thread = omp_get_thread_num();
   printf("Hello from thread %d out of %d from process %d out of %d\n",
          thread, numthreads, rank, numprocs);
   /* Nieskonczona petla */
   while(i==0) {
     a=2+2;
   }
 }
 MPI_Finalize();
 return 0;

} </syntaxhighlight>


Program będziemy uruchamiać na systemie Boreasz w kilku trybach:

  • tylko MPI z ustawioną zmienną środowiskową OMP_NUM_THREADS=1,
  • tylko OpenMP z pojedynczym procesem MPI i wieloma wątkami OpenMP,
  • łączonego MPI i OpenMP z kilkoma procesami MPI, każdy po kilka wątków OpenMP.

Program kompilujemy na systemie w następujący sposób:

mpcc -q64 -O3 -qarch=pwr7 -qtune=pwr7 -o affinity.x affinity.c

Uruchamianie rozpoczniemy od domyślnego skryptu kolejkowego:

#@ job_name = affinity 
#@ output = affinity.out
#@ error = affinity.err
#@ account_no = GRANT_ID
#@ class = kdm
#@ node = 1 
#@ tasks_per_node = 8
#@ wall_clock_limit = 00:02:00
#@ network.MPI = sn_all,not_shared,US,HIGH
#@ notification = never
#@ environment = COPY_ALL
#@ job_type = parallel
#@ queue
setenv OMP_NUM_THREADS 1
mpiexec ./affinity.x

Czas wykonania zadania (ze względu na nieskończoną pętlę w programie) wyznaczony jest przez walltime określony w skrypcie LoadLeveler-a (w tym wypadku 2 minuty). Jest to czas wystarczający aby zorientować się jak nasze zadanie wykonuje się na systemie. Aby zorientować się w sposobie przydzielenia zasobów będziemy wykorzystywać polecenie llq -l JOBID. Na końcu raportu wyświetlana jest informacja o przydzielonych zasobach:

--------------------------------------------------------------------------------
Node
----

  Name            : 
  Requirements    : 
  Preferences     : 
  Node minimum    : 1
  Node maximum    : 1
  Node actual     : 1
  Allocated Hosts : c2n21-hf0::

  Master Task
  -----------

     Executable   : /home/users/login/affinity.ll
     Exec Args    : 
     Num Task Inst: 1
     Task Instance: c2n21-hf0:-1:,

  Task
  ----

     Num Task Inst: 8
     Task Instance: c2n21-hf0:0:,
     Task Instance: c2n21-hf0:1:,
     Task Instance: c2n21-hf0:2:,
     Task Instance: c2n21-hf0:3:,
     Task Instance: c2n21-hf0:4:,
     Task Instance: c2n21-hf0:5:,
     Task Instance: c2n21-hf0:6:,
     Task Instance: c2n21-hf0:7:,
--------------------------------------------------------------------------------

Przy domyślnym sposobie uruchomienia zadania wyświetlana jest wyłącznie informacja o hostname węzła, bez przypisanych zadaniu domen MCM. Oznacza to brak kontroli nad przypisaniem domen MCM do procesów zadania. Teoretycznie w takim układzie możliwa jest sytuacja, w której proces alokuje bufory w przestrzeni pamięciowej znacznie od siebie odległej, w rozumieniu szybkości dostępu. W kolejnych krokach poniżej zaprezentujemy sposób kontrolowanego przydziału zasobów do procesów zadania.

Rozwiązania w przykładach

Zadanie równoległe MPI (8 procesów na węzeł)

Sposób 1 - przypisanie poprzez ustawienia LoadLevler-a

Na sam początek zaprezentujemy sposób na włączenie kontroli nad przypisaniem procesów do domen MCM. Aby to uzyskać należy w skrypcie kolejkowym dodać następującą linię:

#@ rset = rset_mcm_affinity

Po uruchomieniu zadania, po dodaniu powyżej zaprezentowanego przełącznika dotyczącego MCM, w raporcie wykonania wygenerowanym poleceniem llq -l JOBID zobaczymy:

     Num Task Inst: 8
     Task Instance: c1n17-hf0:0:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:1:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:2:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:3:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:4:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:5:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:6:,MCM0:CPU<  0-31 >
     Task Instance: c1n17-hf0:7:,MCM0:CPU<  0-31 >

Z informacji powyżej wynika, że każdy z 8 procesów zadania został skierowany do domeny MCM0, która zawiera pojedynczy procesor Power7 i przypisaną mu pamięć RAM. Każdemu procesowi został przypisany jeden z CPU o oznaczeniach od cpu0 do cpu31. Posiadamy teraz więcej informacji, jednak:

  • procesy uruchomione zostały na pojedynczej domenie MCM,
  • nie wiemy na których dokładnie rdzeniach i CPU (w rozumieniu LoadLeveler-a) uruchomione zostały procesy.

Aby rozrzucić procesy MPI po dostępnych w ramach węzła procesorach i domenach MCM zdefiniujemy dodatkowe opcje przydziału za pomocą linii:

#@ mcm_affinity_options = opcja1 opcja2

Lista opcji:

  • opcja1=task_mcm_allocation może być jednym z:
    • mcm_accumulate - procesy są umieszczane na tym samym MCM jeśli to możliwe
    • mcm_distribute - procesy są rozmieszczane po różnych MCM na zasadach round-robin
  • opcja2=memory_affinity może być jednym z:
    • mcm_mem_none - przypisanie przestrzeni pamięci nie jest wymagane
    • mcm_mem_pref - przypisanie przestrzeni pamięci jest preferowane
    • mcm_mem_req - przypisanie przestrzeni pamięci jest wymagane

Aby rozrzucić 8 procesów MPI po różnych MCM ustawimy:

#@ rset = rset_mcm_affinity
#@ mcm_affinity_options = mcm_distribute mcm_mem_req

Teraz możemy zaobserwować następujące przypisanie:

     Num Task Inst: 8
     Task Instance: c10n25-hf0:0:,MCM0:CPU<  0-31 >
     Task Instance: c10n25-hf0:1:,MCM1:CPU<  32-63 >
     Task Instance: c10n25-hf0:2:,MCM2:CPU<  64-95 >
     Task Instance: c10n25-hf0:3:,MCM3:CPU<  96-127 >
     Task Instance: c10n25-hf0:4:,MCM0:CPU<  0-31 >
     Task Instance: c10n25-hf0:5:,MCM1:CPU<  32-63 >
     Task Instance: c10n25-hf0:6:,MCM2:CPU<  64-95 >
     Task Instance: c10n25-hf0:7:,MCM3:CPU<  96-127 >

Z powyższej informacji wynika, że procesy 0-owy i 4-ty wylądowały na MCM0, procesy 1-szy i 5-ty na MCM1, itd.

Dodatkowo, możemy zarządać jak dużo rdzeni lub wątków sprzętowych przypisanych jest zadaniu poprzez dwa dodatkowe ustawienia.

Aby ustawić liczbę rdzeni napiszemy:

#@ task_affinity = core(x)

gdzie x oznacza ilość rdzeni przypisanych pojedynczemu procesowi.

Aby ustawić liczbę sprzętowych wątków napiszemy:

#@ task_affinity = cpu(y)

gdzie y oznacza ilość sprzętowych wątków przypisanych pojedynczemu procesowi.

Powyższych dwóch ustawień nie należy używać razem. W ramach ćwiczenia, spróbujemy wykonać przypisanie MCM przy x=2 w pierwszym przypadku i y=2 w drugim.

Po pierwsze ustawiamy więc:

#@ rset = rset_mcm_affinity
#@ mcm_affinity_options = mcm_distribute mcm_mem_req
#@ task_affinity = core(2)

i otrzymujemy:

     Num Task Inst: 8
     Task Instance: c1n1-hf0:0:,MCM0:CPU<  0-7 >
     Task Instance: c1n1-hf0:1:,MCM1:CPU<  32-39 >
     Task Instance: c1n1-hf0:2:,MCM2:CPU<  64-71 >
     Task Instance: c1n1-hf0:3:,MCM3:CPU<  96-103 >
     Task Instance: c1n1-hf0:4:,MCM0:CPU<  8-15 >
     Task Instance: c1n1-hf0:5:,MCM1:CPU<  40-47 >
     Task Instance: c1n1-hf0:6:,MCM2:CPU<  72-79 >
     Task Instance: c1n1-hf0:7:,MCM3:CPU<  104-111 >

Po drugie ustawiamy:

#@ rset = rset_mcm_affinity
#@ mcm_affinity_options = mcm_distribute mcm_mem_req
#@ task_affinity = cpu(2)

i otrzymujemy:

     Num Task Inst: 8
     Task Instance: c1n1-hf0:0:,MCM0:CPU<  0-1 >
     Task Instance: c1n1-hf0:1:,MCM1:CPU<  32-33 >
     Task Instance: c1n1-hf0:2:,MCM2:CPU<  64-65 >
     Task Instance: c1n1-hf0:3:,MCM3:CPU<  96-97 >
     Task Instance: c1n1-hf0:4:,MCM0:CPU<  2-3 >
     Task Instance: c1n1-hf0:5:,MCM1:CPU<  34-35 >
     Task Instance: c1n1-hf0:6:,MCM2:CPU<  66-67 >
     Task Instance: c1n1-hf0:7:,MCM3:CPU<  98-99 >

Możliwość przypisywania liczby wątków sprzętowych do procesów MPI będzie wykorzystywana później w przypadku zadań łączonych MPI i OpenMP.

Zaprezentowany tutaj sposób może wydawać się dość skomplikowany. Jego przewaga nad następnym zaprezentowanym narzędziem polega na tym, że rozwiązanie to jest całkowicie zintegrowane z systemem kolejkowym LoadLeveler, co pozwala nam w wygodny sposób podglądać efekt naszych ustawień.

Sposób 2 - przypisanie poprzez narzędzie launch

Użycie narzędzia launch jest dużo prostsze niż poprzednio prezentowana metoda. Aby uzyskać poprawne przypisanie wystarczy użyć następującego skryptu:

#@ job_name = affinity 
#@ output = affinity.out
#@ error = affinity.err
#@ account_no = GRANT_ID
#@ class = kdm
#@ node = 1 
#@ tasks_per_node = 8
#@ wall_clock_limit = 00:02:00
#@ network.MPI = sn_all,not_shared,US,HIGH
#@ notification = never
#@ environment = COPY_ALL
#@ job_type = parallel
#@ queue
setenv OMP_NUM_THREADS 1
module load launch
setenv TARGET_CPU_LIST -1
setenv MEMORY_AFFINITY MCM
mpiexec launch ./affinity.x

W powyższym skrypcie pojawiły się 4 nowości (oznaczone kolorem zielonym). Po pierwsze przed wywołanie programu, a po wywołaniu mpiexec pojawiło się wywołanie narzędzia launch, którego zadaniem jest dokonanie odpowiedniego przypisania procesów MPI. Aby uruchomić narzędzie launch należy najpierw ustawić środowisko poleceniem module load launch. Do poprawnego działania narzędzie launch potrzebuje listy cpu (wątków sprzętowych), które chcemy użyć do realizacji procesów MPI. Może ona wyglądać np. tak:

setenv TARGET_CPU_LIST "0,4,8,16"

co da nam rozlokowanie 4 procesów na miejscach cpu0, cpu4, cpu8 oraz cpu16. Może być to również lista skrócona postaci:

setenv TARGET_CPU_LIST "0,7-3,11-14,23-27:2,46-37:3"

co daje następujące rozlokowanie procesów: cpu0, cpu7, cpu6, cpu5, cpu4, cpu3, cpu11, cpu12, cpu13, cpu14, cpu23, cpu25, cpu27, cpu46, cpu43, cpu40, cpu37.

Powyższe przykłady obrazują tylko jakiego rodzaju przypisań możemy dokonywać. Dużo wygodniej w większości przypadków będzie nam jednak skorzystać z automatycznego przypisania, które uzyskuje się ustawiając:

setenv TARGET_CPU_LIST -1

Dzięki takiej definicji narzędzie launch posługując się ustawieniami skryptu kolejkowego uruchomi zadanie z najbardziej optymalnym przypisaniem.

Dodatkowo ustawiając zmienną środowiskową MEMORY_AFFINITY na wartość MCM włączamy przypisanie poszczególnych domen MCM do naszego zadania i jego procesów.

Zdecydowaną zaletą prezentowanego rozwiązania jest jego prostota. Należy również zauważyć, że dzięki możliwości definiowania własnej listy cpu, posiadamy większą kontrolę nad procesami zadania. Minusem tego rozwiązania jest brak integracji z LoadLevelerem.

Zadanie OpenMP (16 wątków OpenMP na węzeł)

Przypisywanie wątków do zasobów w przypadku zadań wykorzystujące wyłącznie OpenMP nie jest niestety obecnie wspierane przez LoadLevler-a. W związku z tym w tym wypadku będziemy posiłkować się zmiennymi środowiskowymi, które dostępne są do obsługi zadań przygotowywanych za pomocą kompilatorów IBM XL (domyślnych na systemie Boreasz).

Aby zdefiniować sposób rozmieszczenia wątków OpenMP po wątkach sprzetowych skorzystamy ze zmiennej:

setenv XLSMPOPTS startproc=0:stride=2

Całość skryptu kolejkowego będzie zatem wyglądać tak:

#@ job_name = affinity 
#@ output = affinity.out
#@ error = affinity.err
#@ account_no = GRANT_ID
#@ class = kdm
#@ node = 1 
#@ tasks_per_node = 1
#@ wall_clock_limit = 00:02:00
#@ network.MPI = sn_all,not_shared,US,HIGH
#@ notification = never
#@ environment = COPY_ALL
#@ job_type = parallel
#@ queue
setenv OMP_NUM_THREADS 16
setenv XLSMPOPTS startproc=0:stride=2
mpiexec ./affinity.x

Przydział wątków sprzętowych do wątków OpenMP rozpoczęty zostanie od cpu0 (startproc=0), a kolejnym wątkom OpenMP przypisywany będzie co 2 wątek sprzętowy (stride=2) z zapętleniem:

OpenMP0  -> cpu0
OpenMP1  -> cpu2
OpenMP2  -> cpu4
OpenMP3  -> cpu6
OpenMP4  -> cpu8
..
OpenMP13 -> cpu26
OpenMP14 -> cpu28
OpenMP15 -> cpu30

W ten sposób wszystkie zadania OpenMP wylądują na jednym procesorze Power7 wykorzystując połowę z jego wątków sprzętowych.

Niestety na systemie Boreasz, ze względu na brak możliwości logowania na węzły obliczeniowe, nie istnieje sposób w który użytkownik mógłby łatwo sprawdzić działanie tego ustawienia. Zostało ono jednak przetestowane pomyślnie przez administratorów systemu Boreasz.

Zadanie MPI + OpenMP (8 procesów MPI po 4 wątki OpenMP każdy)

Współczesne aplikacje równoległe bardzo często korzystają z tzw. modelu hybrydowego zrównoleglenia MPI + OpenMP. Równoległe procesy MPI rozrzucane są równomiernie po węzłach obliczeniowych, a wewnątrz węzłów zrównoleglone są dodatkowo modelem OpenMP.

Najprostszym i polecanym przez nas sposobem uzyskania odpowiedniego przypisania procesów i wątków do zasobów jest na systemie Boreasz użycie narzędzia hybrid_launch. W przypadku naszego programu uruchamianego z wykorzystaniem 8 procesów MPI po 4 wątki OpenMP każdy można to uzyskać przy użyciu następującego skryptu kolejkowego:

#@ job_name = affinity 
#@ output = affinity.out
#@ error = affinity.err
#@ account_no = GRANT_ID
#@ class = kdm
#@ node = 1 
#@ tasks_per_node = 8
#@ wall_clock_limit = 00:02:00
#@ network.MPI = sn_all,not_shared,US,HIGH
#@ notification = never
#@ environment = COPY_ALL
#@ job_type = parallel
#@ queue
setenv OMP_NUM_THREADS 4
module load hybrid_launch
setenv TARGET_CPU_LIST -1
setenv MEMORY_AFFINITY MCM
mpiexec hybrid_launch ./affinity.x

Rozwiązanie to jest bardzo podobne do wcześniej prezentowanego użycia narzędzia launch w przypadku aplikacji czysto MPI-owej. Również sposób ustawienia zmiennych TARGET_CPU_LIST oraz MEMORY_AFFINITY jest identyczny. Trzy podstawowe różnice to zwiększenie liczby wątków OpenMP przypadających na pojedynczy proces MPI za pomocą ustawienia OMP_NUM_THREADS na wartość 4, ustawienie środowiska poleceniem module load hybrid_launch oraz wywołanie narzędzia hybrid_launch.

Podsumowanie

Do czego znajomość powyżej zaprezentowanych technik może nam się przydać?

W artykule tym zaprezentowane zostały sposoby uzyskania przypisania procesów i wątków do fizycznych wątków sprzętowych, rdzeni, procesorów i w końcu pamięci RAM. Na większości współczesnych systemów HPC umiejętność odpowiedniego zarządzania zasobami i przypisywania ich do zadań jest kluczem do uzyskania wysokiej wydajności. Można to chociażby zobrazować odwołując się ponownie do jednego z otrzymanych błędnych przypisań:

    Num Task Inst: 8
    Task Instance: c1n17-hf0:0:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:1:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:2:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:3:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:4:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:5:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:6:,MCM0:CPU<  0-31 >
    Task Instance: c1n17-hf0:7:,MCM0:CPU<  0-31 >

Aplikacja uruchomiona na węźle obliczeniowym Boreasz-a w powyższy sposób nie będzie miała możliwości wykorzystania w pełni wysokiej wydajności podystemu pamięci architektury Power775. Poniżej prezentujemy wyniki testów wydajności podsystemu pamięci wykonane przez nas na pojedynczym węźle systemu Boreasz za pomocą benchmarku STREAM (http://www.cs.virginia.edu/stream/). Test wykonany został za pomocą równoległej wersji STREAM, uruchamiany był za pomocą 16 procesów MPI i bez zrównoleglenia na wątki OpenMP.

Pierwszy pomiar wykonany został przy domyślnych ustawieniach zadania (bez przypisania procesów do zasobów). Oto jego wyniki:

Function        Rate (MB/s)            Avg time          Min time          Max time
Copy:           116275.0994             .4941             .4403             .6497
Scale:          116350.9490             .4940             .4400             .6497
Add:            130075.1594             .6273             .5904             .8608
Triad:          130020.7660             .6303             .5907             .8615

Następnie test został powtórzony, ale po dokonaniu przypisania za pomocą narzędzie launch. Oto wyniki testu z przypisaniem:

Function        Rate (MB/s)            Avg time          Min time          Max time
Copy:           167813.3267             .3056             .3051             .3080
Scale:          166948.7635             .3068             .3067             .3072
Add:            189688.6034             .4049             .4049             .4050
Triad:          189434.0413             .4058             .4054             .4077

Jak widzimy wyniki są o około 30% lepsze niż w przypadku testu bez przypisania.

Zaprezentowane powyżej wyniki pokazują jak istotne dla wydajności aplikacji (w szczególności tych intensywnie wykorzystujących podsystem pamięci) jest dokonanie odpowiedniego przypisania procesów i wątków równoległych do zasobów węzła obliczeniowego. Warto spróbować zastosować opisane techniki we własnych obliczeniach.


Testy skalowalności Neuron-a na systemach ICM

Autor: Helena Głąbska (IBD PAN, ICM UW)

Wstęp

Na komputerach ICM prowadzone są od pewnego czasu obliczenia z dziedziny neurobiologii obliczeniowej. O zakresie tych badań można przeczytać m.in. w 3-cim numerze Biuletynu POWIEW. Głównym narzędziem obliczeniowym jest aplikacja Neuron, która zainstalowana została na systemach: Halo2, Notos oraz Nostromo. W niniejszym artykule prezentujemy wyniki skalowalności oprogramowania na systemach ICM. Na podstawie wyników formułujemy również wnioski, które mogą być przydatne w planowaniu własnych obliczeń.

Opis testowego modelu

Rys.1 Cała kolumna korowa modelu Trauba.
Rys.2 10% komórek korowych modelu Trauba.

Model Trauba został opublikowany w 2005 roku [1] . Jest to największy ogólnodostępny model pętli korowo-wzgórzowej, obejmuje kolumnę korową połączoną z grupą komórek wzgórza. Pełny model zawiera 3560 komórek (3360 w korze, 200 komórek wzgórzowych) , składających się przeciętnie z ok. 70 – ciu segmentów. Każdy z segmentów zawiera wiele (5 – 15) mechanizmów błonowych. W modelu występuje 14 różnych typów komórek (12 w korze 2 we wzgórzu).

Testowa symulacja zawiera 1s przykładowej aktywności sieci. Testy zostały wykonane zarówno dla pełnej sieci (Rys. 1) jak dla „małej sieci” - 10 % komórek (Rys. 2). Ponadto wykonano trzy rodzaje testów, uwzględniając różny sposób rozdzielenia pracy na poszczególne procesory:

  • Random Robin - komórki przypisywane do poszczególnych procesorów za pomocą algorytmu random Robin
  • Rozkład zbalansowany - przy rozdziale komórek na procesory brana jest pod uwagę liczbę segmentów i mechanizmów błonowych w poszczególnych komórkach, w taki sposób by jak najbardziej zrównoważyć liczbę obliczeń na każdym z procesorów (jeśli na którymś procesorze jest więcej komórek, będą to te których symulacja wymaga mniej obliczeń)
  • Multisplit - możliwość rozdzielenia jednej komórki na kilku procesorach, w celu zbilansowanie obliczeń. Można ograniczyć liczbę „rozbić” komórki za pomocą parametru „msoptfactor”. Dla msoptfactor = 0, żadne rozbicie nie jest dozwolone, dla msoptfactor = 1 nie wprowadzamy żadnych ograniczeń na liczbę rozbić. W większości testów przyjełam msoptfactor = 0.8 , co np. w przypadku „małej sieci” ( 356 komórek) dało mi 15668 składowych.

Wyniki skalowalności

Poniżej przedstawiamy wykresy skalowalności modelu Trauba na 3 wybranych systemach obliczeniowych ICM.

Halo2

Skalowanie na Halo2, mała sieć - 356 komórek.
Skalowanie na Halo2, duża sieć - 3560 komórek.

Notos

Skalowanie na Notosie, mała sieć - 356 komórek.
Skalowanie na Notosie, duża sieć - 3560 komórek.

Nostromo

Skalowanie na Nostromo, mała sieć - 356 komórek.
Skalowanie na Nostromo, duża sieć - 3560 komórek.

Podsumowanie i wnioski

W ramach podsumowania prezentujemy wykresy zbiorcze porównujące wyniki uzyskane na poszczególnych maszynach.

Zbiorcze porównianie skalowania na maszynach ICM, mała sieć - 356 komórek.
Zbiorcze porównianie skalowania na maszynach ICM, duża sieć - 3560 komórek.

Główne wnioski, które można sformułować na podstawie zaprezentowanych wyników:

  • Przy małej sieci, pojedyncze węzły na halo2 są szybsze niż na notosie i na nostromo. Wyniki te nie są do końca zbieżne z teoretyczną wydajnością obliczeniową rdzeni trzech opisywanych systemów. Rdzenie obliczeniowe Nostromo są nieznacznie mocniejsze od rdzeni Halo2, w związku z tym gorsze wyniki Nostromo tłumaczyć należy gorszym stopniem zoptymalizowania aplikacji do tego systemu. Deweloperzy Neurona wciąż pracują jednak nad nowymi wersjami oprogramoania na system Blue Gene/Q dla projektu Human Brain Project, możemy się zatem spodziewać nowszych i bardziej zoptymalizowanych wersji w niedalekiej przyszłości. Rdzenie Notos-a są faktycznie najsłabsze, co widać w uzyskanych wynikach.
  • Przy pełnej sieci wyniki czasowe są bardzo zbliżone, a systemy Notos i Nostromo prezentują lepsze skalowanie obliczeń. Dla dużego modelu ilość wymaganej komunikacji jest większa i dlatego obserwujemy zbliżenie wyników czasowych na rdzeń wszystkich systemów.
  • Symulacje skalują się dobrze (na początku liniowo, potem trochę wolniej) na wszystkich komputerach, o ile przy algorytmach „random Robin „ i „rozkład zbalansowany” spełniony jest warunek : liczba komórek >= liczba procesorów.
  • W przypadku Halo2 czas obliczeń zależał widocznie od konkretnych węzłów obliczeniowych. Dla małej sieci przy 4 powtórzeniach czas obliczeń nierzadko wzrastał ok 30% - 40% (np. dla 16, 32, 64 rdzeni przy wersji „random Robin”, 64, 256 rdzeni dla „rozkładu zbalansowanego” oraz 16, 64 rdzeni przy wersji „multisplit”), sięgając nawet 77 % („rozkład zbalansowany” na 512 rdzeniach). Dla pełnej sieci zazwyczaj wzrastał o nie więcej niż 20 – 30%, ale przy 512 rdzeniach różnica wyniosła ok 41% dla „random Robin” i 54% dla „rozkładu zbalansowanego”. Efekt ten może być związany z niejednorodną architekturą Halo2. Taktowanie procesorów AMD klastra różni się bowiem pomiędzy węzłami (2.0 GHz,2.2 GHz, 2.4GHz). Zjawisko to nie występuje na komputerach Notos i Nostromo. Wyniki czasowe na tych systemach są bardziej powtarzalne.
  • Dla komputera Notos wyniki testów dla topologii TORUS i MESH (dane nie zamieszczone w artykule) były praktycznie takie same.
  • Przy pełnej sieci (3560 komórek) dla liczby rdzeni nie większej niż 512 żaden z 3 algorytmów („random Robin” , rozkład zbalansowany”, „multisplit”) nie wypadł znacząco lepiej. Dla większej liczby rdzeni ( 1024, 2048, 4096) algorytmy : „rozkład zbalansowany”, „multisplit” okazały się być wydajniejsze. Dla „małej sieci” (356) sytuacja wygląda ciekawiej, możemy dokładniej przetestować jak algorytmy sprawują się gdy liczba komórek jest znacząco mniejsza niż liczba rdzeni. Pierwsze różnice można dostrzec gdy liczba rdzeni osiąga wartość 64, „random Robin” , wypada w tym wypadku gorzej od pozostałych dwóch, jeszcze lepiej to widać dla 128 i 256 rdzeni. Dla liczby rdzeni 512 i więcej czas obliczeń przy algorytmach „random Robin” i „rozkład zbalansowany” ustalają się na stałym poziomie, co jest zgodne z przwidywaniami. Każda komórka została przydzielona do innego procesora, w sytuacji gdy nie dopuszczamy do rozbicia pojedynczej komórki, część rdzeni jest bezczynna. W przypadku wersji „multisplit” skalowanie utrzymało się aż do 2048 rdzeni, czyli gdy mamy ponad 5 razy więcej rdzeni niż komórek, ale wyniki mogą się trochę różnić w zależności od wartości parametru msoptfactor.

Bibliografia

[1] Traub RD, Contreras D, Cunningham MO, Murray H, LeBeau FE, Roopun A, Bibbig A, Wilent WB, Higley MJ, Whittington MA, Single-column thalamocortical network model exhibiting gamma oscillations, sleep spindles, and epileptogenic bursts. J Neurophysiol. 2005 Apr;93(4):2194-232