OpenMP pro CPU¶
OpenMP (Open Specification for Multi Processing, www.openmp.org) je rozhraní (application programming interface, API) pro explicitní programování paralelních aplikací na jednom počítači s více logickými procesory (jádry hardwarového procesoru neboli CPU) a sdílenou pamětí. Realizuje vláknovou paralelizaci (multithreading), totiž jeden spuštěný hlavní proces (master thread) na vstupu do paralelní oblasti (parallel region) vytváří skupinu vláken (team), která se dělí o práci a na konci paralelní oblasti po synchronizaci zaniká (fork-join model); dál pokračuje samotný hlavní proces. Vlákna se mohou dělit jak o provádění týchž instrukcí na různých datech (data parallelism), tak i o nezávislé větve programu (functional parallelism). O softwarová vlákna se dělí hardwarová jádra CPU; je obvyklé, že počet vláken odpovídá počtu (volných) jader.
OpenMP programování spočívá především v doplnění (vhodně připraveného) sekvenčního (neboli sériového) zdrojového kódu o direktivy (interpretovatelné komentáře), které navedou překladač k vytvoření vláken a rozdělení práce mezi ně; někdy vyvstává potřeba přidat do zdrojového kódu i volání dotazovacích a konfiguračních funkcí a procedur; typické je nastavování některých vlastností spouštěných programů z vnějšku pomocí proměnných prostředí. Je možno dosáhnout stavu, kdy zdrojový kód OpenMP programu lze bez úprav přeložit jak pro sekvenční, tak pro paralelní běh; specifikace umožňuje (nezaručuje) získání shodných výsledků v obou případech. Úkolem programátora je nalézt paralelizovatelné oblasti sekvenčního kódu, správně rozvrhnout proměnné v paralelní oblasti na sdílené (originály přístupné všem vláknům) a soukromé (kopie rozmnožené pro každé vlákno) a zajistit synchronizaci vláken i přístupu do paměti; jinak se mohou vyskytnout datové dostihy (data race), nejčastější chyba v OpenMP programech.
Specifikace OpenMP jsou formulovány pro jazyky C/C++ a Fortran, jejich hlavní verze se datují roky 1997 (1.0), 2000 (2.0), 2008 (3.0), 2013 (4.0) a 2018 (5.0), překladače se specifikacemi spíše drží krok. OpenMP ve Fortranu implementují např. GNU Compiler Collection gfortran, Intel HPC Toolkit ifx a ifort, Nvidia HPC SDK nvfortran (dřívější překladač Portland Group, pgfortran) a AMD AOCC flang. Snahou posledních specifikací je podpora výpočetních akcelerátorů (GPU); to bylo mezitím realizováno rozhraním OpenACC, implementovaným pro Nvidia GPU např. v Nvidia překladačích. Podkladem a zároveň nízkoúrovňovým konkurentem OpenMP je standard Pthreads (POSIX vlákna).
Syntaxi OpenMP lze čerpat např. z ještě stručné Fortran Syntax Quick Reference Card pro verzi 3.1 nebo z dalších podkladů na stránce OpenMP Specifications.
Paralelizace indexovaného cyklu¶
Sekvenční kód s paralelizovatelným cyklem
integer,parameter :: nmax=10
real a(nmax)
do n=1,nmax; a(n)=n; print '(x,f0.0$)',a(n); enddo
print *
end program
paralelizujeme přidáním páru direktiv !$OMP PARALLEL; !$OMP END PARALLEL pro vyznačení paralelní oblasti, v které bude vytvořena skupina vláken. Takto by ovšem každé vlákno provedlo celý cyklus. Pro rozdělení iterací cyklu mezi vlákna je třeba umístit těsně před paralelizovatelný cyklus direktivu !$OMP DO. Její ukončení !$OMP END DO je vázáno na konec cyklu a je nepovinné.
integer,parameter :: nmax=10
real a(nmax)
!$OMP PARALLEL
!$OMP DO
do n=1,nmax; a(n)=n; print '(x,f0.0$)',a(n); enddo
!$OMP END DO
!$OMP END PARALLEL
print *
end program
Pokud je jako zde paralelní oblast tvořena jediným paralelizovaným cyklem, lze obě direktivy zkombinovat do úspornějšího zápisu:
integer,parameter :: nmax=10
real a(nmax)
!$OMP PARALLEL DO
do n=1,nmax; a(n)=n; print '(x,f0.0$)',a(n); enddo
!$OMP END PARALLEL DO
print *
end program
Bez explicitní volby pro OpenMP překladače překládají zdrojové kódy jen pro sekvenční běh, zde tedy s výpisem prvků pole po řadě:
gfortran 01.f90
ifx 01.f90
nvfortran 01.f90
flang 01.f90
Výpis: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
S volbou pro OpenMP překladače druhý a třetí kód vláknově zparalelizují a výpis prvků pole bude náhodně promíchaný:
(win & lnx) gfortran -fopenmp 02.f90
(win) ifx -Qopenmp 02.f90
(lnx) ifx -qopenmp 02.f90
(lnx) nvfortran -mp 02.f90
(lnx) flang -fopenmp 02.f90
Výpis: 3. 1. 4. 2. 6. 8. 9. 10. 7. 5.
Podepisující se vlákna¶
V následující ukázce použijeme dotazovací funkce OpenMP pro zjištění celkového počtu vláken a pořadového čísla vlákna (počínaje 0). Pro zajištění správné interpretace datového typu návratové hodnoty funkcí je třeba připojit standardní modul omp_lib. Paralelní běh vypisuje čísla všech vláken v náhodném pořadí.
use omp_lib
!$OMP PARALLEL PRIVATE (n,nt)
nt=OMP_GET_NUM_THREADS() ! zjištění počtu vláken
n=OMP_GET_THREAD_NUM() ! zjištění čísla vlákna
print '(x,i0,"/",i0$)',n,nt ! výpis obou hodnot
!$OMP END PARALLEL
print *
end program
Výpis: 5/8 2/8 4/8 1/8 0/8 7/8 3/8 6/8
Bez připojení modulu omp_lib by se zde návratové hodnoty OpenMP funkcí interpretovaly jako default real obsahující malá reálná čísla a při uložení do default integer proměnných n a nt by byly oříznuty na nuly. Nutnou součástí direktivy PARALLEL je zde explicitní stanovení proměnné n (zde též nt) jako soukromé (přesněji: soukromé v oblasti) klauzulí PRIVATE (n,nt). Bez této klauzule by proměnné byly implicitně sdílené a všechna vlákna by zapisovala své číslo do téhož paměťového místa sdílené proměnné n. Projevily by se tak datové dostihy, některá vlákna by svým podpisem stihla více příkazů print, jiná žádný.
Výpis při neuvedení řádku use omp_lib: 0/0 0/0 0/0 0/0 0/0 0/0 0/0 0/0
Výpis při neuvedení klauzule PRIVATE: 2/8 7/8 7/8 7/8 7/8 7/8 7/8 7/8
Počet vláken vznikajících v paralelní oblasti se implicitně řídí obvykle počtem jader nebo logických procesorů počítače (Windows Task Manager: logical processors). Explicitně můžeme počet vláken nastavit z vnějšku programu pomocí proměnné prostředí OMP_NUM_THREADS nebo ve zdrojovém kódu klauzulí NUM_THREADS directivy PARALLEL.
Nastavení počtu vláken a spuštění paralelizovaného programu:
(win) gfortran -fopenmp 03.f90 && set OMP_NUM_THREADS=4 && a
(lnx) gfortran -fopenmp 03.f90 && export OMP_NUM_THREADS=4 && ./a.out
(nebo rovnou) OMP_NUM_THREADS=4 ./a.out
Řešení překladu kódu obsahujícího OpenMP funkce pro sekvenční běh je mezi různými překladači poněkud různorodé. ifort (aktuálně nikoliv ifx) překládá s volbou -Qopenmp-stubs, nvfortran OpenMP funkcím rozumí i bez volby -mp a gfortran potřebuje dodat explicitní knihovnu openmp-stubs. Problém sekvenčního překladu s OpenMP funkcemi lze řešit i vlastním modulem:
module omp_lib_stubs_minimalistic
contains
integer function OMP_GET_NUM_THREADS() result (r)
r=1
end function
integer function OMP_GET_THREAD_NUM() result (r)
r=0
end function
end module
! use omp_lib ! pro paralelizovaný běh
use omp_lib_stubs_minimalistic ! pro sekvenční běh
!$OMP PARALLEL PRIVATE (n,nt)
nt=OMP_GET_NUM_THREADS()
n=OMP_GET_THREAD_NUM()
print '(x,i0,"/",i0$)',n,nt
!$OMP END PARALLEL
print *
end program
Výpis: 0/1
Explicitní sdílitelnost proměnných¶
V následující ukázce necháme vlákna podepisovat se do prvků (implicitně sdíleného) pole, zjistíme, kolik které vlákno stihlo podepsat prvků a shlédneme i rozložení podpisů mezi prvky. (Implicitně sdílená) proměnná nt vynese informaci o počtu vláken ven z paralelní oblasti.
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
allocate (a(nmax))
!$OMP PARALLEL
nt=OMP_GET_NUM_THREADS()
!$OMP DO
do n=1,nmax; a(n)=OMP_GET_THREAD_NUM(); enddo
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
V paralelní oblasti ukázky se vyskytují čtyři jména: nt, n, nmax a a. Indexy cyklů jsou v OpenMP vždy (předurčeně, tj. nezměnitelně) soukromé (private), každé vlákno má svou kopii; sdílitelnost indexu n tak není třeba specifikovat. Jméno nmax zde není proměnná, ale symbolická konstanta, kterých se OpenMP deklarace netýkají. Ostatní proměnné v paralelní oblasti – pole a a skalár nt – jsou defaultně sdílené (shared). Pro pole je sdílení obvykle správně, jednotlivé iterace cyklu se často starají o odpovídající prvky pole a vlákna tak při rozdělení iterací cyklu dostanou ke zpracování exkluzivní část pole. Pro skaláry je implicitní sdílení naopak často nesprávné, zvláště když jsou vázány na aktuální hodnotu indexu; je pak nezbytné takové skaláry deklarovat jako soukromé. V ukázce má ovšem skalár nt ve všech vláknech tutéž hodnotu, může zůstat sdílený. Sdílené proměnných zvýrazníme klauzulí SHARED direktivy PARALLEL. Klauzuli PRIVATE pro výčet soukromých proměnných jsme už použili v předcházející ukázce, zde na ni nic nezbylo. Pro neopominutí explicitní deklarace proměnných užitých v paralelní oblasti se hodí klauzule DEFAULT (NONE), potlačující implicitní pravidla.
V ukázce zapisují všechna vlákna do sdílené proměnné nt tutéž hodnotu. Taková situace není sice nebezpečná, ale vhodnější je zapsat do proměnné tuto hodnotu jen jediným vláknem. Blok paralelní oblasti vyznačený párem direktiv SINGLE a END SINGLE provede jediné vlákno – to, které k bloku dorazí jako první; alternativně lze použít i direktivy MASTER a END MASTER k vyznačení bloku, který má být proveden nultým vláknem. Přiřazovací příkaz tedy umístíme mezi direktivy SINGLE a END SINGLE. Tamtéž by patřil i případný výpis sdílené proměnné uvnitř paralelní oblasti. Koncové direktivy END PARALLEL, END DO, END SINGLE a podobné (nikoliv však END MASTER) jsou implicitním synchronizačním bodem – vlákna u nich na sebe čekají. Pokud to – jako zde – není třeba (nt se použije až vně paralelní oblasti), lze synchronizaci potlačit klauzulí NOWAIT koncových direktiv.
Klauzule IF direktivy PARALLEL podmiňuje vytvoření paralelní oblasti, např. zde minimálním počtem iterací cyklu.
Přidělování iterací cyklu vláknům ovlivňuje klauzule SCHEDULE (kind,chunk_size) direktivy DO. Obvyklou implicitní hodnotou je (STATIC), kdy je každému vláknu přidělena úměrná část po sobě jdoucích iterací cyklu (zde nmax/nt, je-li nmax dělitelné nt). Bývá žádoucí procházet v jednotlivých vláknech prvky pole sekvenčně; z toho důvodu bývá nevhodné zadávat malé hodnoty volitelného argumentu chunk_size, např. (STATIC,1). Vyzkoušíme také hodnotu klauzule (DYNAMIC), kdy jsou iterace přidělovány vláknům v pořadí, v jakém se vlákna hlásí o práci. To snižuje prodlevu rychlejších vláken u synchronizačního bodu na konci cyklu. Velká hodnota chunk_size, např. nmax/nt, zde přibližuje reakci k odezvě na (STATIC), malá hodnota, např. 1, zvyšuje režii rozdílení práce; vhodnou hodnotou bude něco mezi. Z tohoto pohledu může být optimální volba (GUIDED), kdy jsou vláknům dynamicky přidělovány nejprve spíše větší počty iterací, později menší.
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
allocate (a(nmax))
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt) IF (nmax>10)
!$OMP SINGLE
nt=OMP_GET_NUM_THREADS()
! print *,nt
!$OMP END SINGLE NOWAIT
!$OMP DO SCHEDULE (GUIDED) ! možnosti: STATIC, DYNAMIC, GUIDED
do n=1,nmax; a(n)=OMP_GET_THREAD_NUM(); enddo
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Výpis při SCHEDULE (STATIC): 125 125 125 125 125 125 125 125
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. ... 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.
Výpis při SCHEDULE (STATIC,1): 125 125 125 125 125 125 125 125
0. 1. 2. 3. 4. 5. 6. 7. 0. 1. 2. 3. 4. 5. 6. 7. 0. 1. 2. 3. 4. 5. ... 5. 6. 7. 0. 1. 2. 3. 4. 5. 6. 7.
Výpis při SCHEDULE (DYNAMIC): 0 203 0 182 122 189 150 154
5. 6. 4. 1. 5. 5. 5. 5. 5. 5. 5. 5. 5. 5. 5. 1. 5. 1. 5. 1. 5. 1. ... 4. 6. 5. 1. 3. 7. 5. 6. 1. 3. 4.
Výpis při SCHEDULE (DYNAMIC,1): 0 180 164 0 193 122 204 137
6. 5. 7. 2. 4. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 6. 4. 6. ... 4. 1. 5. 4. 1. 7. 5. 2. 6. 2. 7.
Výpis při SCHEDULE (DYNAMIC,10): 70 190 160 0 290 130 0 160
4. 4. 4. 4. 4. 4. 4. 4. 4. 4. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 1. 1. ... 0. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.
Výpis při SCHEDULE (GUIDED): 190 0 395 31 0 230 44 110
2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. ... 2. 2. 6. 6. 3. 6. 3. 2. 3. 2. 3.
Paralelizace přiřazení mezi poli a cyklu forall¶
Fortranská array language umožňuje nahradit námi použitý sekvenční cyklus do-enddo pro přiřazení do prvků pole jednak přiřazovacím příkazem pro celé pole (array assignment), jednak nesekvenčním cyklem forall-endforall. Oba postupy lze podle specifikace OpenMP paralelizovat direktivou WORKSHARE. Praktický pokus (rok 2024) bohužel ukazuje, že gfortran paralelizuje jen přiřazovací příkaz, nikoliv forall, a ifx takto neparalelizuje vůbec; nvfortran paralelizuje obojí. Pokus rovněž dokládá, že překladače nasazují na direktivu WORKSHARE pouze statický scheduling.
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
allocate (a(nmax))
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt) PRIVATE (it)
nt=OMP_GET_NUM_THREADS() ! počet vláken
it=OMP_GET_THREAD_NUM() ! číslo vlákna
!$OMP WORKSHARE
a(1:nmax/2)=it ! přiřazení do pole
forall (n=nmax/2+1:nmax); a(n)=it; end forall ! cyklus forall
!$OMP END WORKSHARE
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Výpis pro gfortran: 563 63 63 63 62 62 62 62 (první polovina pole zpracována staticky, druhá jediným - zde nultým - vláknem)
Výpis pro ifx: 1000 0 0 0 0 0 0 0 (obě poloviny pole zpracovány jediným - většinou nultým - vláknem)
Výpis pro nvfortran: 126 126 126 126 124 124 124 124 (obě poloviny pole zpracovány staticky)
Paralelizace větví programu¶
Přiřazovací příkaz a cyklus forall v předchozí ukázce můžeme pojmout jako dvě nezávislé větve programu, které lze provést paralelně. K tomu účelu nabízí OpenMP direktivu SECTIONS. Každá z vyznačených větví je provedena jedním vláknem; zde tedy využijeme nejvýše dvě vlákna.
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
allocate (a(nmax))
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt) PRIVATE (it)
nt=OMP_GET_NUM_THREADS()
it=OMP_GET_THREAD_NUM()
!$OMP SECTIONS
!$OMP SECTION
a(:nmax/2)=it ! první větev
!$OMP SECTION
forall (n=nmax/2+1:nmax); a(n)=it; end forall ! druhá větev
!$OMP END SECTIONS
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Výpis pro gfortran: 500 0 0 0 0 0 0 500
Výpis pro ifx: 0 0 0 500 0 0 0 500
Výpis pro nvfortran: 0 0 500 500 0 0 0 0
Proměnné soukromé ve vlákně¶
Doposud jsme pracovali s jedinou paralelní oblastí v programu. Při potřebě více paralelních oblastí můžeme narazit na vlastnost PRIVATE proměnných (soukromých v oblasti), že jejich paměťová místa, tedy i hodnoty, zanikají s ukončením paralelní oblasti. Následující ukázka to doloží:
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
allocate (a(nmax))
it=-1
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt) PRIVATE (it)
nt=OMP_GET_NUM_THREADS()
it=OMP_GET_THREAD_NUM()
!$OMP END PARALLEL ! SHARED nt přežije, PRIVATE it zanikne
!$OMP PARALLEL DEFAULT (NONE) SHARED (a) FIRSTPRIVATE (it)
!$OMP DO
do n=1,nmax; a(n)=it; enddo
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Hodnoty it nastavené v první paralelní oblasti zaniknou spolu s ní. Druhá oblast osadí prvky a(n) hodnotami ze soukromých proměnných it, s klauzulí FIRSTPRIVATE (it) inicializovanými venkovní hodnotou -1, s klauzulí PRIVATE (it) s nedefinovanými hodnotami. Než předložíme řešení, vyzkoušíme si potřebný aparát podrobněji.
Soukromé proměnné, jejichž paměťová místa spolu s hodnotami v nich mohou existovat po celou dobu existence programu, se nazývají soukromé ve vlákně a deklarují se direktivou THREADPRIVATE. Udržení jejich definovanosti je třeba deklarovat i ve fortranském popisu datového typu proměnné pomocí atributu save nebo pomocí inicializace hodnoty (čímž se atribut save aktivuje implicitně). Sdílitelnost těchto proměnných se pak při vstupu do paralelní oblasti už nespecifikuje. Jejich hodnoty se buď při vstupu do paralelní oblasti naklonují z hlavního procesu užitím klauzule COPYIN, nebo se nastavují v paralelní oblasti v každém vlákně podle potřeby, nebo si nesou fortranskou inicializaci z popisu typu (ukázáno po řadě na proměnných p, q, r):
real,save :: p,q
real :: r=1.
!$OMP THREADPRIVATE (p,q,r)
p=1.
!$OMP PARALLEL DEFAULT (NONE)
q=1
print *,p,q,r ! p je 1 v nultém vlákně, q a r jsou 1 ve všech vláknech
!$OMP END PARALLEL
!$OMP PARALLEL DEFAULT (NONE) COPYIN (p)
print *,p,q,r ! p, q a r jsou 1 ve všech vláknech
!$OMP END PARALLEL
end program
Ukázku s dvěma paralelními oblastmi nyní můžeme upravit (tedy doplnit fortranskou deklaraci it s atributem save, přidat direktivu THREADPRIVATE a vypustit klauzule PRIVATE a FIRSTPRIVATE) tak, aby se v druhé oblasti do prvků pole zapsala čísla vláken uložená v it:
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
integer,save :: it
!$OMP THREADPRIVATE (it)
allocate (a(nmax))
it=-1
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt)
nt=OMP_GET_NUM_THREADS()
it=OMP_GET_THREAD_NUM()
!$OMP END PARALLEL
!$OMP PARALLEL DEFAULT (NONE) SHARED (a)
!$OMP DO
do n=1,nmax; a(n)=it; enddo
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Klauzule COPYIN (it) v druhé oblasti by náš úspěch zhatila, hodnoty it všech vláken by se přepsaly nulou zapsanou do it hlavního procesu v první oblasti.
Explicitní úlohy (direktiva TASK)¶
Doposud předvedené paralelizační direktivy slouží k paralelizaci cyklů a jejich ekvivalentů z fortranské array language (DO a WORKSHARE), případně k paralelizaci různých větví programu (SECTIONS). Obecnější algoritmické konstrukce může paralelizovat aparát explicitních úloh (explicit tasks), realizovaný především direktivou TASK. Vezmeme naši předchozí ukázku s podepisováním vláken do prvků pole v indexovaném cyklu a nahradíme direktivu DO direktivou TASK. Jejím standardním doprovodem je direktiva SINGLE, která vyznačuje oblast, v níž jediné vlákno vytváří úlohy definované direktivou TASK, o které se pak dělí všechna vlákna. V případě cyklů je analogií direktivy DO direktiva SINGLE ohraničující celý cyklus, jehož tělo je ohraničeno direktivou TASK.
use omp_lib
integer,parameter :: nmax=1000
real,allocatable :: a(:)
integer,save :: it
!$OMP THREADPRIVATE (it) ! pro použití v explicitní úloze
allocate (a(nmax))
!$OMP PARALLEL DEFAULT (NONE) SHARED (a,nt)
it=OMP_GET_THREAD_NUM()
!$OMP SINGLE ! vytváření (seznamu) úloh jedním vláknem
nt=OMP_GET_NUM_THREADS()
do n=1,nmax
!$OMP TASK ! definování explicitních úloh
a(n)=it ! OMP_GET_THREAD_NUM()
!$OMP END TASK
enddo
!$OMP END SINGLE
!$OMP END PARALLEL
print '(*(x,i0))',(count(a==it),it=0,nt-1)
print '(*(x,f0.0))',a
end program
Chceme-li podepisovat pomocí proměnné it, musíme ji deklarovat jako THREADPRIVATE; default direktivy TASK pro soukromé proměnné je FIRSTPRIVATE, což by do všech úloh vneslo hodnotu it z vlákna provádějícího oblast SINGLE. Doplňme, že sdílené proměnné vcházejí do oblasti TASK defaultně jako SHARED. Pokud bychom tedy chtěli explicitně vyznačit defaultní sdílitelnost použitou v ukázce, psali bychom !$OMP TASK DEFAULT (NONE) SHARED (a) FIRSTPRIVATE (i).
Ukázka také dokládá, že vytváření úloh jedním vláknem a jejich provádění kterýmkoliv z připravených vláken má blízko k dynamickému rozdělování práce direktivou DO. Ne však zcela, v naší ukázce provádí vlákno vytvářející úlohy typicky méně úloh než ostatní, což u rozdílení práce pomocí DO SCHEDULE (DYNAMIC) není tak zřetelné.
Redukce¶
Redukční operací se rozumí operace, která z více hodnot extrahuje (nakonec) jediný výsledek. Mezi redukční funkce ve fortranské array language patří např. sum, product, minval, maxval. Nahlédneme, jak v OpenMP efektivně redukovat hodnoty získávané v jednotlivých vláknech do společného výsledku. Budeme paralelizovat následující ukázku se sumací:
real(8) s
nmax=1000000
s=0.
do n=1,nmax
s=s+n
enddo
print *,nmax,s,real(nmax,8)*(nmax+1)/2 ! výpis sumace a kontrolní výpočet vzorcem
end program
Je zřejmé, že s nemůže být obyčejnou soukromou proměnnou, hodnoty by zanikly při odchodu z paralelní oblasti. Pokud by s bylo sdílené, v příkazu s=s+n by hrozily datové dostihy – jedno vlákno by mohlo načíst s pro zpracování na levé straně příkazu a chystat se zapsat výsledek zpět do s, mezitím by druhé vlákno mohlo oba kroky stihnout rychleji; první vlákno by pak přepsalo s svým příspěvkem, nezohledňujícím akci druhého vlákna. Lze si vyžádat, aby celý příkaz s=s+n smělo v daný moment provádět pouze jediné vlákno, ostatní aby případně počkaly. Tím ovšem potlačíme paralelizaci našeho cyklu – takové řešení je sice možné, ale neefektivní. V následující ukázce používáme CRITICAL blok, do kterého má přístup v daný moment jediné vlákno, a poněkud rychlejší alternativu značenou direktivou ATOMIC pro vybrané operace s hardwarovou podporou bezpečného provedení. Při uvedeném nastavení délky součtu v nmax a počtu opakování sumací mmax se ukázka buď sekvenčně nebo paralelně s datovými dostihy provede za méně než sekundu, s direktivou ATOMIC pak za 30 sekund a s blokem CRITICAL za 45 sekund. Vidíme tedy, že pokud bychom potřebovali redukci v paralelní oblasti, s uvedenými postupy bude počítat buď rychle a špatně nebo dobře a pomalu.
real(8) s,ss
nmax=1000000
mmax=1000
ss=0.
do m=1,mmax
s=0.
!$OMP PARALLEL DO DEFAULT (NONE) SHARED (nmax,s)
do n=1,nmax
!!$OMP CRITICAL ! blok CRITICAL
!!$OMP ATOMIC ! direktiva ATOMIC
s=s+n
!!$OMP END CRITICAL
enddo
!$OMP END PARALLEL DO
ss=ss+s
enddo
print *,nmax,s,real(nmax,8)*(nmax+1)/2
print *,mmax,ss,mmax*real(nmax,8)*(nmax+1)/2
end program
Efektivní řešení spočívá v užití klauzule REDUCTION, kterou se vyznačí redukční proměnná a příslušná operace. K vnější proměnné se tím vytvoří soukromé klony s vhodnou inicializací (např. 0 pro +, 1 pro *), vlákna redukují své dílčí výsledky do těchto soukromých proměnných a nakonec se provede redukční operace na těchto soukromých mezivýsledcích s uložením výsledku do vnější proměnné. Použití následujícího řešení v předchozí ukázce by snížilo dobu běhu zpět pod sekundu.
real(8) s
nmax=1000000
s=0.
!$OMP PARALLEL DO DEFAULT (NONE) SHARED (nmax) REDUCTION (+:s)
do n=1,nmax
s=s+n
enddo
!$OMP END PARALLEL DO
print *,nmax,s,real(nmax,8)*(nmax+1)/2
end program
Pohodlnější to být nemůže. Když se pokusíme o totéž s využitím explicitních úloh, musíme si pomoci THREADPRIVATE proměnnou a úpravou fortranského zdroje. Redukční proměnná se chová jako soukromá v oblasti a jako taková by z úlohy nevynesla hodnoty; redukční proměnná přitom nemůže být THREADPRIVATE. Je tak třeba proměnnou zdvojit – kumulovat do THREADPRIVATE proměnných ss a ty pak přenést do klonů redukční proměnné s k závěrečné sumaci.
real(8) s
real(8),save :: ss
!$OMP THREADPRIVATE (ss)
nmax=1000000
s=0.
!$OMP PARALLEL DEFAULT (NONE) SHARED (nmax) REDUCTION (+:s)
ss=0.
!$OMP SINGLE
do n=1,nmax
!$OMP TASK DEFAULT (NONE) FIRSTPRIVATE (n)
ss=ss+n
!$OMP END TASK
enddo
!$OMP END SINGLE
s=ss
!$OMP END PARALLEL
print *,nmax,s,real(nmax,8)*(nmax+1)/2
end program
Fortranský cyklus do concurrent¶
Norma 2008 obohatila Fortran o paralelizovatelný cyklus do concurrent. Záměrem bylo dodat konstrukci vhodnou k vyjádření paralelizovatelnosti do samotného jazyka, bez potřeby externích direktiv, a také překonat omezení dosavadních fortranských řešení (array assignments a cyklus forall). V aktuální syntaxi (Fortran 2018) lze v záhlaví cyklu do concurrent specifikovat sdílitelnost – ve Fortranu lokalitu – proměnných, a také (Fortran 202x) redukci.
integer,allocatable :: a(:)
integer nmax,n,t
integer(8) s
nmax=1000000
allocate (a(nmax))
s=0
do concurrent (n=1:nmax) default (none) shared (a) local (t) reduce (+:s)
t=n
a(n)=t
s=s+a(n)
! print *,n,t,s
enddo
print *,nmax,s,int(nmax,8)*(nmax+1)/2
end program
V uvedené podobě ukázku dnes (2024) přeloží jen ifx/ifort a nvfortran; gfortran dosud nerozumí lokalitním klauzulím. ifx/ifort paralelizují cyklus pomocí volby pro OpenMP a nvfortran pomocí specifické volby -stdpar=multicore, gfortran – bez lokalitních klauzulí – jen cestou autoparalelizace (viz dále). Pokud bychom ze zvědavosti přesunuli proměnnou t z klauzule local do shared, Intel (s vypnutou optimalizací) by na to reagoval chybným výpočtem, Nvidia nikoliv, patrně kvůli upřednostnění vlastní analýzy cyklu. Podobně, pokud bychom vynechali klauzule default a reduce, Intel by chyboval, Nvidia nikoliv. V cyklu do concurrent se připouští i příkaz print, jímž pak lze datové dostihy (chybně sdílené) proměnné t pozorovat s ifort na vlastní oči; bohužel však nikoliv s ifx, který výpisy nehezky promíchá.
Sdílitelnost proměnných v procedurách¶
V paralelní oblasti může být volána procedura (funkce nebo podprogram). Její zdrojový kód nedává možnost popsat sdílitelnost proměnných explicitně, je třeba respektovat implicitní pravidla. Formální argumenty procedur předané odkazem přebírají atribut sdílení od skutečných argumentů, lokální a modulové THREADPRIVATE proměnné jsou THREADPRIVATE, lokální proměnné s inicializací popisem nebo s atributem save jsou SHARED, ostatní PRIVATE, a modulové proměnné jsou SHARED.
module mMod
integer :: m=-1 ! module variable m
contains
subroutine subr(t,s,p,f)
integer :: t,p,f,s,lp,ls=-1
lp=0; ls=0 ! local private lp, local shared ls
!$OMP BARRIER
!$OMP MASTER
s=1; p=1; f=1; lp=1; ls=1; m=1
!$OMP END MASTER
!$OMP BARRIER
print '(a11,*(x,i2))','subroutine:',t,s,p,f,lp,ls,m
end subroutine
end module
use mMod
use omp_lib
integer,save :: t ! threadprivate t
integer s,p,f,lp,ls
!$OMP THREADPRIVATE (t)
print '(a11,*(x,a2))','','t','s','p','f','lp','ls','m'
s=-1; p=-1; f=-1; lp=-1; ls=-1 ! shared s, private p, firstprivate f
!$OMP PARALLEL SHARED (s) PRIVATE (p) FIRSTPRIVATE (f) NUM_THREADS (4)
t=OMP_GET_THREAD_NUM()
p=0
call subr(t,s,p,f)
print '(a11,*(x,i2))','parallel:',t,s,p,f,lp,ls,m
!$OMP END PARALLEL
print '(a11,*(x,i2))','serial:',t,s,p,f,lp,ls,m
end program
t s p f lp ls m
subroutine: 0 1 1 1 1 1 1
parallel: 0 1 1 1 -1 -1 1
subroutine: 3 1 0 -1 0 1 1
parallel: 3 1 0 -1 -1 -1 1
subroutine: 1 1 0 -1 0 1 1
parallel: 1 1 0 -1 -1 -1 1
subroutine: 2 1 0 -1 0 1 1
parallel: 2 1 0 -1 -1 -1 1
serial: 0 1 -1 -1 -1 -1 1
Výsledky dokládají, že
modulové proměnné jsou sdílené (nulté vlákno nastavilo hodnotu
1do proměnnémpro všechna vlákna),lokální proměnné procedur s inicializací popisem, resp. s atributem
save, jsou sdílené (nulté vlákno nastavilo hodnotu1do proměnnélspro všechna vlákna), jinak jsou soukromé (nulté vlákno nastavilo hodnotu1do proměnnélpjen sobě),argumenty procedury sdílené vně jsou sdílené i v proceduře (nulté vlákno nastavilo hodnotu
1do proměnnéspro všechna vlákna),argumenty procedury soukromé vně jsou soukromé i v proceduře (nulté vlákno nastavilo hodnotu
1do proměnnýchpafjen sobě),na konci procedury není implicitní synchronizační bod (výpisy
subroutineaparalleljsou promíchány),první explicitní synchronizační bod
BARRIERje nutný kvůli dvěma po sobě jdoucím updatům sdílené proměnnéls, jinak by ve výpisu stála někdy0, jindy1,druhá direktiva
BARRIER(za nesynchronizující direktivouMASTER) je nutná kvůli po sobě jdoucímu updatu a výpisu sdílených proměnnýchs,lsam, jinak by ve výpisu stály někdy1, jindy předchozí hodnoty.
Autoparalelizace a vektorizace¶
Kromě běžně používaných optimalizací mohou překladače nabízet autoparalelizaci a vektorizaci. Autoparalelizace se typicky pokouší podobně jako OpenMP paralelizovat cykly jejich rozdělením mezi softwarová vlákna přidělená hardwarovým jádrům (thread-level parallelism, TLP). Vektorizace nahrazuje skalární aritmetické instrukce vektorovými SIMD (Single Instruction, Multiple Data) instrukcemi, tedy prováděním téže instrukce na několika datech naráz (data-level parallelism, DLP). SIMD instrukce na Intel procesorech zahrnují několik generací: SSE (Streaming SIMD Extensions, 1999+) instrukce mají 128bitové (16bytové) registry a provádějí tak operace na 4 single-precision nebo 2 double-precision operandech, AVX a AVX2 (Advanced Vector Extensions, 2011+) zvedly velikost registrů na dvojnásobek: 8x SP nebo 4x DP a umí i s integer typy, a na některých procesorech (např. Skylake-X, Cascade Lake-X) jsou dostupné instrukce AVX-512 s 512bitovými registry. Pro úplnost, hovoří se také o instrukčním paralelizmu (instruction-level parallelism, ILP), čímž se často myslí instrukční pipeling, realizovaný také v hardwaru.
Autoparalelizaci na bázi OpenMP rozvinuly hlavně Intel a GNU překladače. Je na vyžádání: ifort nabízí volbu (Windows) -Qparallel, resp. (Linux) -parallel, počet vláken se nastavuje proměnnou prostředí OMP_NUM_THREADS. Modernější ifx autoparalelizační volbu (zatím? 2024) nemá. V GNU překladačích se používá volba -ftree-parallelize-loops=N, kde N je počet vláken, zde tedy zafixovaný už při překladu; bez optimalizační volby nebo jen s -O1 se neautoparalelizuje.
Vektorizují dnes všechny běžné překladače, a to už v defaultu. Např. default volba Intelu zní (Windows) -Qvec, resp. (Linux) -qvec, negovat lze variantou -Qno-vec, resp. -qno-vec. GNU překladače mají default volbu -ftree-vectorize, negovatelnou pomocí -fno-tree-vectorize.
Rozdělení iterací cyklu mezi vlákna, a tedy i autoparalelizace, se často nasazuje na vnějších cyklech, vektorizace se provádí na souvislých částech pole, pak na vnitřních cyklech. Předvedeme na ukázce, která na prvky matice osazené náhodnými čísly opakovaně nasazuje odmocňování, tedy přibližuje hodnoty prvků k limitě 1.
MODULE mTicToc
use omp_lib
private; public tic,toc
real(8) time
CONTAINS
subroutine tic(); time=omp_get_wtime(); end subroutine
real(8) function toc(); toc=omp_get_wtime()-time; end function
END MODULE
program testNestedLoops
use mTicToc
implicit none
integer :: nmax=10000
integer n,m,k
real(8),allocatable :: a(:,:)
allocate (a(nmax,nmax))
call random_number(a)
call tic()
!$OMP PARALLEL DO SCHEDULE (GUIDED)
do n=1,nmax
do m=1,nmax
do k=1,100; a(m,n)=sqrt(a(m,n)); enddo
enddo
enddo
!$OMP END PARALLEL DO
print *,'parallel outer loop:',a(nmax,nmax),toc()
call tic()
do n=1,nmax
!$OMP PARALLEL DO SCHEDULE (GUIDED)
do m=1,nmax
do k=1,100; a(m,n)=sqrt(a(m,n)); enddo
enddo
!$OMP END PARALLEL DO
enddo
print *,'parallel inner loop:',a(nmax,nmax),toc()
end program
Sumační benchmark¶
S následující ukázkou porovnáme doby běhu fortranských konstrukcí do-enddo, forall-endforall, přiřazení mezi poli (array assignment) a do concurrent-enddo. Hlavním efektem bude, zda vůbec je v překladači OpenMP paralelizace dané konstrukce zapracována a je-li případně k dispozici alternativa. Tou může být autoparalelizace, v praxi spočívající v automatickém rozeznání paralelizovatelných částí programu a jejich automatické vláknové paralelizaci. Ve Windows porovnáme překladače GCC gfortran a Intelu ifx/ifort, v Linuxu přidáme ještě překladače Nvidia nvfortran a AMD flang. Na hodinkách uvidíme také efekt statického a dynamického schedulingu.
Benchmark počítá harmonická čísla \(H_n=\sum_{i=1}^n 1/i\) do prvků pole a(n), jedno nezávisle na druhém, celková práce bude tedy kvadraticky závislá na maximálním n; zvolíme nmax=200000 pro řádově sekundové běhy. Numerický výsledek můžeme namátkově kontrolovat vzorcem \(H_n=\ln n+\gamma+1/(2n)-\varepsilon_n\), kde \(\gamma\) je Eulerova konstanta a \(\varepsilon_n\) klesá rychleji než \(1/(8n^2)\).
Dobu běhu (wall-clock time) měříme v matlabovském tic-toc stylu pomocí modulu mTicToc stojícího na OpenMP funkci omp_get_wtime. Obdobnou funkčnost by poskytl i standardní fortranský podprogram system_clock.
MODULE mTicToc ! Usage: call tic(); ...; print *,toc()
use omp_lib
private; public tic,toc
real(8) time
CONTAINS
subroutine tic(); time=omp_get_wtime(); end subroutine
real(8) function toc(); toc=omp_get_wtime()-time; end function
END MODULE
program testParallelLoops
use mTicToc
implicit none
integer,parameter :: wp=8
integer :: nmax=200000
integer n
real(wp),allocatable :: a(:)
character(20) :: fmt='(a20,2x,f0.9,f7.2)'
allocate (a(nmax))
print fmt,'by formula:',log(real(nmax,wp))+.577215664901533_wp+.5_wp/nmax,0.
call tic()
!$OMP PARALLEL DO
do n=1,nmax; a(n)=f(n); enddo
!$OMP END PARALLEL DO
print fmt,'parallel do static:',a(nmax),toc()
call tic()
!$OMP PARALLEL DO SCHEDULE (GUIDED)
do n=1,nmax; a(n)=f(n); enddo
!$OMP END PARALLEL DO
print fmt,'parallel do guided:',a(nmax),toc()
call tic()
!$OMP PARALLEL WORKSHARE
forall (n=1:nmax); a(n)=f(n); endforall
!$OMP END PARALLEL WORKSHARE
print fmt,'forall:',a(nmax),toc()
call tic()
!$OMP PARALLEL WORKSHARE
a=f([(n,n=1,nmax)]) ! nvfortran n/a
!$OMP END PARALLEL WORKSHARE
print fmt,'array assignment:',a(nmax),toc()
call tic()
do concurrent (n=1:nmax); a(n)=f(n); enddo
print fmt,'do concurrent:',a(nmax),toc()
contains
elemental function f(n) result (r)
real(wp) r
integer,intent(in) :: n
integer :: m
r=0.
do m=1,n
r=r+1._wp/m
enddo
end function
end program
gfortran a OpenMP. Explicitní možnost dynamického rozdílení iterací cyklu pomocí klauzule SCHEDULE přináší při nerovnoměrné práci jednotlivých vláken znatelnou úsporu doby běhu, zde čtvrtinovou. Direktiva WORKSHARE se jeví jako neúčinná, cyklus do concurrent běží také jen sériově. Volby -O3 -march=native optimalizují přeložený kód pro hardware překládajícího stroje.
(win & lnx) gfortran -fopenmp -O3 -march=native
-----------------------------------------------
wall-clock time
result Windows Linux
by formula: 12.783290810 0.00 0.00
parallel do static: 12.783290810 3.86 3.91
parallel do guided: 12.783290810 2.91 2.13
forall: 12.783290810 16.47 16.06
array assignment: 12.783290810 16.78 16.06
do concurrent: 12.783290810 16.64 16.07
gfortran a autoparalelizace. Autoparalelizace pomocí volby -ftree-parallelize-loops=N (s počtem vláken N pevně předepsaným při překladu) zde působí na všechny varianty. Srovnáním s OpenMP variantou lze usoudit, že práce je rozdělována staticky. Kombinace voleb -fopenmp -ftree-parallelize-loops=N upřednostňuje OpenMP direktivy, tedy neparalelizuje WORKSHARE, ale paralelizuje do-enddo i do concurrent. Bez optimalizační volby nebo jen s -O1 se neautoparalelizuje.
(win & lnx) gfortran -ftree-parallelize-loops=N -O3 -march=native
-----------------------------------------------------------------
by formula: 12.783290810 0.00 0.00
parallel do static: 12.783290810 3.81 3.91
parallel do guided: 12.783290810 3.58 3.91
forall: 12.783290810 3.58 3.90
array assignment: 12.783290810 3.80 3.90
do concurrent: 12.783290810 3.73 3.90
ifx (obdobně ifort) a OpenMP. Dynamický scheduling direktivy PARALLEL DO přináší při nerovnoměrné práci vláken úsporu doby běhu. Direktiva WORKSHARE se jeví jako neúčinná. Cyklus do concurrent běží (bez direktiv) paralelně, jeho scheduling je statický. Volby (Windows) -O3 -QxHost, resp. (Linux) -O3 -xHost, optimalizují přeložený kód pro hardware překládajícího stroje.
(win) ifx -Qopenmp -O3 -QxHost; (lnx) ifx -qopenmp -O3 -xHost
-------------------------------------------------------------
by formula: 12.783290810 0.00 0.00
parallel do static: 12.783290810 2.03 1.98
parallel do guided: 12.783290810 1.50 1.08
forall: 12.783290810 8.39 8.04
array assignment: 12.783290810 8.38 8.04
do concurrent: 12.783290810 2.09 1.97
ifort a autoparalelizace. Autoparalelizace pomocí volby (Windows) -Qparallel, resp. (Linux) -parallel, zde působí na všechny varianty, práce je rozdělována staticky. Počet vláken je řízen proměnnou prostředí OMP_NUM_THREADS. Moderní překladač ifx autoparalelizační volbu (zatím? 2024) nemá.
(win) ifort -Qparallel -O3 -QxHost; (lnx) ifort -parallel -O3 -xHost
--------------------------------------------------------------------
by formula: 12.783290810 0.00 0.00
parallel do static: 12.783290810 2.12 1.97
parallel do guided: 12.783290810 2.05 1.97
forall: 12.783290810 1.95 1.97
array assignment: 12.783290810 1.93 1.97
do concurrent: 12.783290810 2.00 1.97
nvfortran a OpenMP. Dynamické rozdílení iterací cyklu do-enddo pomocí klauzule SCHEDULE je i zde nejrychlejší. Direktiva WORKSHARE paralelizuje cyklus forall, ale nesnese se (nepřeloží se) s array assignment. Pro (CPU) paralelizaci cyklu do concurrent je určena volba -stdpar=multicore; počet vláken pro do concurrent řídí proměnná prostředí ACC_NUM_CORES.
(lnx) nvfortran -mp -stdpar=multicore -O3 -march=native
OMP_NUM_THREADS=N ACC_NUM_CORES=N ./a.out
-------------------------------------------------------
by formula: 12.783290810 0.00
parallel do static: 12.783290810 2.00
parallel do guided: 12.783290810 1.06
forall: 12.783290810 2.00
do concurrent: 12.783290810 2.00
flang a OpenMP. Dynamické rozdílení iterací cyklu do-enddo pomocí klauzule SCHEDULE je i zde nejrychlejší. Direktiva WORKSHARE se nesnáší s array assignment a neparalelizuje cyklus forall. Cyklus do concurrent se neparalelizuje.
(lnx) flang -fopenmp -O3 -march=native
--------------------------------------
by formula: 12.783290810 0.00
parallel do static: 12.783290810 3.93
parallel do guided: 12.783290810 2.13
forall: 12.783290810 16.07
do concurrent: 12.783290810 16.09
Co do rychlosti tedy zvítězil fortranský indexovaný cyklus do-enddo s OpenMP direktivou DO a klauzulí SCHEDULE (GUIDED). Navíc, toto řešení spolehlivě paralelizují všechny testované překladače. Jiné konstrukce fortranské array language mohou být v praxi pomocí OpenMP neparalelizovatelné; pomoci může autoparalelizace, které jsou ovšem schopny jen některé překladače. Cesty k paralelizaci cyklu do concurrent jsou rozličné: OpenMP pro ifx/ifort, autoparalelizace pro gfortran, specifická volba překladače pro nvfortran a žádné řešení pro flang.
Benchmark Mandelbrot¶
V následujícím benchmarku porovnáme paralelizaci pomocí direktiv OpenMP a konkurenčního systému OpenACC. Úloha zahrnuje dva vnořené cykly do-enddo s voláním funkce; i tento benchmark zatěžuje více procesor a méně paměťový systém, je compute-bound, nikoliv memory-bound. Na vnější cyklus nasadíme standardní OpenMP kombinovanou direktivu PARALLEL DO a také obdobnou OpenACC direktivu PARALLEL LOOP. Zatímco OpenMP direktivy mají podporu v mnohých překladačích, podporu OpenACC lze nalézt jen u Nvidia a GNU. Snaha o prosazení OpenMP pro paralelizaci na grafických procesorech (GPU) – tzv. offloading – však zatím nalezla ohlas především u Intelu (Offload OpenMP with Intel tools) a jen pro jeho (nepříliš výkonné) GPU. OpenACC mělo paralelizaci na GPU jako primární cíl, což se podařilo realizovat v dnešních Nvidia překladačích (Nvidia OpenACC Getting Started Guide) pro jejich (špičkové) GPU. nvfortran umí navíc překlad s OpenACC direktivami směrovat i na CPU. S GNU překladači je vyvíjena alternativa usilující o překlad OpenMP a OpenACC direktiv pro Nvidia a AMD GPU (offloading support in GCC).
Doby běhu této ukázky (viz komentář v záhlaví ukázky) stvrzují následující spíše obecné vlastnosti:
běhy přenesené direktivami na GPU mohou být podstatně rychlejší než na CPU, od několikanásobku po řád i dva; nejsnáze lze takto zrychlit compute-bound programy,
Nvidia GeForce GPU mají řádově vyšší výkon v
real(4)než vreal(8)– podstatně dražší Nvidia Tesla GPU jsou silné i vreal(8),Nvidia OpenACC bývá na GPU rychlejší než GNU OpenACC a podporuje více typů Nvidia GPU,
Nvidia OpenACC bývá na CPU pomalejší než OpenMP ekvivalent.
Přidejme postřeh, že autoparalelizace ukázky vede s ifort k poloviční rychlosti běhu než OpenMP paralelizace a že gfortran zde s autoparalelizací neuspěje.
Hlavní poselství ovšem zní, že OpenMP běh na CPU (Intel i9-9900K) se zde – při real(4) přesnosti – daří zrychlit přechodem k OpenACC na obdobně drahé GPU (Nvidia RTX 2070) 20krát. Soudobý špičkový desktopový procesor (Intel i9-14900K) je 2,5krát rychlejší než soutěžící CPU, řádově levnější Nvidia GPU (Nvidia GT 1030) je 2,5krát pomalejší než soutěžící GPU, a na této ukázce by tato entry-level GPU byla stále 3krát rychlejší než top CPU.
! Testing points of the Mandelbrot set (no output, a total count only)
! nvfortran 23.11
! CPU OpenMP: nvfortran -mp -O3 -march=native mandelbrot.f90
! CPU OpenACC: nvfortran -acc=multicore -O3 -march=native mandelbrot.f90
! GPU OpenACC: nvfortran -acc -O3 -march=native mandelbrot.f90
! more flags and variables: -gpu=ccall, -Minfo=accel, NVCOMPILER_ACC_TIME, NVCOMPILER_ACC_NOTIFY, ACC_NUM_CORES
! gfortran 11.4
! CPU OpenMP: gfortran -fopenmp -O3 -march=native mandelbrot.f90
! GPU OpenACC: gfortran -fopenacc -O3 -march=native mandelbrot.f90
! more flags and variables: -foffload=nvptx-none, -fopenacc-dim=:4:32, -fopt-info-optimized-omp, GOMP_DEBUG
! Ubuntu: apt install gfortran-11 gcc-11-offload-nvptx
! Wall-clock times for CPU i9-9900K, GPU GeForce RTX 2070 and GeForce GT 1030, imax*jmax=1e9 points
! CPU 8 cores/16 threads nvfortran OpenMP real(4) 5.3 s real(8) 5.3 s
! -- " -- nvfortran OpenACC 11.4 s 12.5 s
! -- " -- gfortran OpenMP 5.3 s 5.3 s
! GPU 2560 cores nvfortran OpenACC .27 s 2.2 s
! -- " -- gfortran OpenACC .48 s 2.9 s
! GPU 384 cores nvfortran OpenACC .71 s 12.5 s
! -- " -- gfortran OpenACC 1.5 s 15.9 s
module mConst
integer,parameter :: wp=4 ! working precision
real(wp),parameter :: &
xmin=-2._wp, & ! x,y-bounds
xmax=0.6_wp, &
ymin=-1.2_wp, &
ymax=1.2_wp
integer,parameter :: &
imax=100000, & ! x,y-resolution
jmax=10000, &
nmax=100 ! max. number of iterations
end module
program Mandelbrot
use mConst
implicit none
real(wp) x,y
complex(wp) c
integer i,j,id
integer counter
! open (newunit=id,file='mandelbrot.dat')
counter=0
! --- OpenMP for CPU
!$OMP PARALLEL DO PRIVATE (x,y,c,i) REDUCTION (+:counter) SCHEDULE (GUIDED)
! --- OpenMP for GPU
!!$OMP TARGET MAP (TOFROM:counter)
!!$OMP TEAMS DISTRIBUTE PARALLEL DO PRIVATE (x,y,c,i) REDUCTION (+:counter)
! --- OpenACC for GPU
!$ACC DATA COPY (counter)
!$ACC PARALLEL LOOP PRIVATE (x,y,c,i) REDUCTION (+:counter)
do j=0,jmax-1
y=ymin+(ymax-ymin)*j/(jmax-1)
do i=0,imax-1
x=xmin+(xmax-xmin)*i/(imax-1)
c=cmplx(x,y,wp)
if (MandelbrotOut(c)>nmax) counter=counter+1
! write (id,'(f8.5,f9.5,i5)') x,y,MandelbrotOut(c)
enddo
enddo
!$ACC END PARALLEL LOOP
!$ACC END DATA
!!$OMP END TEAMS DISTRIBUTE PARALLEL DO
!!$OMP END TARGET
!$OMP END PARALLEL DO
! close (id)
print *,100._wp*counter/(imax*jmax),' %'
contains
function MandelbrotOut(c) result (result)
!$OMP DECLARE TARGET
!$ACC ROUTINE SEQ
integer result
complex(wp),intent(in) :: c
complex(wp) z
integer n
result=nmax+1 ! not diverging
z=0.
do n=1,nmax
z=z*z+c
if (z%re**2+z%im**2>4) then
result=n; exit ! diverging
endif
enddo
end function
end program
Verze: 8. 4. 2024