Paralelní algoritmy
& architektury
Kompletní vypracování dvanácti státnicových okruhů — od paměťového subsystému a vektorizace, přes sdílenou a distribuovanou paměť, GPU a CUDA, až po paralelní třídění, lineární algebru a grafové algoritmy.
Seznam otázek
- 01Paměťový subsystémSRAM/DRAM, cache, lokalita, sekvenční vs. náhodný přístup→
- 02Sekvenční architektury a vektorizacePipelining, out-of-order, branch prediction, SIMD, unrolling→
- 03Architektury se sdílenou pamětíCache coherence (MESI), false sharing, OpenMP, atomické instrukce→
- 04Architektury s distribuovanou pamětíTopologie sítí, ts + m·tw, MPI, deadlock, komunikační operace→
- 05GPU a CUDASM, blok, warp, SIMT, divergence, coalesced access, efektivní kód→
- 06Paralelní redukce a prefix-sumStromová redukce, nákladová optimalita, segmentovaný prefix sum→
- 07Paralelní quicksortSdílená vs. distribuovaná verze, citlivost na pivot, hyperquicksort→
- 08Radix sort — LSD a MSDCount + prefix-sum, false sharing, MSD a datová lokalita→
- 09Bitonic sortBitonická posloupnost, bitonic split, síť komparátorů, log²n hloubka→
- 10Cannonův algoritmus a GaussBloky √p × √p, rotace bloků, GEM, diagonalizace→
- 11Tridiagonální soustavyThomas, cyklická redukce, úplná cyklická redukce→
- 12Paralelní grafové algoritmyBFS jako SpMV, polookruhy, Dijkstra vs. Bellman-Ford→
Paměťový subsystém
Statické a dynamické paměti, typy dynamických pamětí, vyrovnávací paměti, sekvenční a náhodný přístup do paměti, optimalizace přístupu do paměti z pohledu programátora.
Proč nás paměť vůbec brzdí — Memory Wall
Výkon procesorů rostl historicky mnohem rychleji než rychlost operační paměti RAM. CPU dnes umí v jednom taktu vykonat víc instrukcí, než mu paměť stihne dodat dat — tento rozevírající se nůžkový jev se nazývá memory wall (nebo von Neumann bottleneck). Rychlost paměti omezují dvě veličiny:
Latence — doba, než paměťový modul vůbec najde první požadovaná data. Klíčová při čtení malých kousků z různých míst. Za poslední dekády se zlepšila jen velmi málo (~50–100 ns u DRAM).
Propustnost (bandwidth) — rychlost přenášení nalezených bloků. Klíčová při souvislém čtení velkých dat. Stále se zlepšuje díky širším sběrnicím a paralelnímu čtení z více čipů (DDR5 ~50 GB/s).
Statické vs. dynamické paměti
SRAM (statická)
- 1 bit = 6 tranzistorů
- velmi rychlá, žádné obnovovací cykly
- drahá, fyzicky velká
- použití: CPU cache (L1, L2, L3)
DRAM (dynamická)
- 1 bit = 1 tranzistor + 1 kondenzátor
- vyžaduje obnovovací cykly (kondenzátor se vybíjí)
- během obnovení nelze pracovat
- levná, menší, méně spotřeby — hlavní RAM
Adresování DRAM — řádek a sloupec
Adresa se dělí na RAS (row address strobe — index řádku) a CAS (column address strobe — index sloupce). Při čtení několika buněk ze stejného řádku stačí poslat jeden RAS a několik CAS → hardware sám zvýhodňuje sekvenční přístup v malých blocích.
Typy dynamických pamětí
- SDR SDRAM (Single Data Rate) — synchronní DRAM, taktovaná pomaleji než CPU.
- DDR SDRAM (Double Data Rate) — přenáší data na nástupné i sestupné hraně hodin → dvojnásobek dat za takt. Frekvence i takt zůstávají stejné, ale efektivně čteme ze dvou čipů.
- DDR2 → DDR5 — postupné zvyšování propustnosti širšími sběrnicemi a vyšším počtem paralelních čipů (banks). Latence se mění minimálně.
Cache — vyrovnávací paměti
Cache je malá rychlá SRAM mezi CPU a RAM. Velikost ~1/1000 RAM. Moderní procesory mají tři úrovně:
- L1 — nejmenší, nejrychlejší, na jádře. Typicky 32–64 KB.
- L2 — větší, sdílená v rámci jádra nebo dvojice jader. ~256 KB – 1 MB.
- L3 — sdílená napříč všemi jádry, řády MB.
Pravidlo: čím menší číslo, tím menší ale rychlejší cache.
Prostorová lokalita (spatial locality) — program nepřistupuje do paměti náhodně, ale pracuje s menšími ucelenými bloky dat.
Časová lokalita (temporal locality) — program data zpracovává postupně; jakmile dokončí jeden blok, přesune se na další a k původnímu se již nevrací.
Prefetching a cache line
CPU si proaktivně načítá data, o která ještě nepožádalo — odhadne z lokality program a načte celé bloky (cache line, typicky 64 bytů). Důsledek: čtení jednoho intu z RAM vždy přitáhne celou cache line s 16 sousedy zdarma.
Mapování cache — tři architektury
| Typ | Jak funguje | Cena | Praxe |
|---|---|---|---|
| Plně asociativní | Blok lze uložit kamkoliv; adresa = Tag + Offset. Každá cache line má svůj komparátor. | velmi drahá | nepoužívá se |
| Přímé mapování | Každý blok má jediné pevné místo. Pokud kolize, přepíše se. | levná | jednoduchá, ale špatná u skoků |
| Set-associative | Kompromis: cache rozdělena na množiny, blok jde do libovolné cache line v dané množině. Typicky 4-, 8- nebo 16-cestná. | rozumná | standard |
Write-through vs. write-back
- Write-through — zápis propisuje jak do cache, tak okamžitě do RAM. Bezpečné, ale pomalé.
- Write-back — zápis jen do cache; do RAM se zapíše až při vyřazení cache line. Rychlé, ale potřebuje protokol pro konzistenci (MESI).
⚠ Sekvenční vs. náhodný přístup — klíčový obrázek
Tohle profesor u zkoušky chce nakreslit:
- Sekvenční přístup: křivka taktů zůstává plochá a nízká — prefetcher má data připravená v cache.
- Náhodný přístup: jakmile dataset překročí kapacitu cache, počet taktů strmě stoupá — každý prvek vyvolá cache miss a CPU čeká stovky taktů na RAM.
Optimalizace z pohledu programátora
1. Sčítání matic po řádcích vs. po sloupcích
Matice jsou v C/C++ uloženy po řádcích. Iterace po řádcích = sekvenční přístup. Iterace po sloupcích = skok přes celý řádek za každým prvkem → totální cache miss. Rozdíl bývá řád.
// dobré: row-major iterace
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
C[i][j] = A[i][j] + B[i][j];
// špatné: sloupcová iterace skáče po n prvcích
for (int j = 0; j < n; j++)
for (int i = 0; i < n; i++)
C[i][j] = A[i][j] + B[i][j];
2. Násobení matic — transpozice nebo bloky
Klasické C = A·B potřebuje matici B procházet po sloupcích.
- Transpozice B: umožní obě matice číst sekvenčně, ale zvýší paměťové nároky.
- Blokové násobení (tiling): rozdělíme matice na malé bloky, které se vejdou do L1 cache, a každý blok výsledku spočítáme z bloků operandů. Nepotřebuje extra paměť a typicky vyhrává.
3. Critical stride — zákeřný hardwarový jev
Řešení: zvětšit matici o jeden řádek/sloupec (1024 → 1025) nebo přidat padding. Tím se rozbije pravidelnost mapování a výkon se vrátí na třetinu.
4. Pole struktur vs. ukazatel na velká data
Když vyhledáváme podle klíče, nepotřebujeme tahat celé velké hodnoty do cache. Náhrada BigData data za BigData* data ve struktuře způsobí, že se do cache vejde víc klíčů a velká data se tahají až tehdy, když jsou opravdu potřeba.
- Data v paměti uspořádej tak, aby se k nim přistupovalo sekvenčně.
- Když nelze, rozděl výpočet na menší bloky, které se vejdou do cache (temporal locality).
- Načtená data v cache využij co nejvíc — než je vykopnou nová.
Co u zkoušky chce profesor
- Umět nakreslit graf sekvenční vs. náhodný přístup v závislosti na velikosti dat.
- SRAM vs. DRAM — počty tranzistorů, obnovovací cykly, kde se kde používá.
- Cache mapování (plně asociativní × přímé × set-associative) — proč je plně asoc. drahá.
- Lokality (prostorová + časová), prefetching, cache line.
- Critical stride — co to je a jak ho rozbít (padding, +1 prvek).
Sekvenční architektury a vektorizace
Zpracování instrukcí procesorem, pipelining, zpracování instrukcí mimo pořadí, předvídání podmínek, vektorová rozšíření procesorů, optimalizace zpracování kódu procesorem z pohledu programátora.
CISC vs. RISC
- CISC (Complex Instruction Set) — bohatá instrukční sada, různé délky instrukcí, jedna instrukce může dělat víc věcí najednou. Klasické x86.
- RISC (Reduced Instruction Set) — málo, jednoduché, stejně dlouhé instrukce, lépe se zpracovávají v pipeline. Moderní x86 vnitřně CISC instrukce překládá na RISC mikroinstrukce.
Pipelining — zřetězené zpracování
Pipelining je forma paralelizace na úrovni CPU. Zpracování jedné instrukce se rozdělí na několik jednodušších kroků (jako výrobní linka):
Délka pipeline se liší architekturou: Pentium P5 = 5 fází, Core ~14, Netburst (extrémní) 20–31. Delší pipeline = vyšší frekvence, ale větší ztráta při špatném odhadu skoku.
Závislosti (hazardy) v pipeline
- Datová závislost — instrukce čeká na výsledek předchozí, který ještě není hotový.
- Závislost na zdrojích — dvě instrukce chtějí stejný registr / paměťové místo.
- Závislost na podmínce (větvení) — průměrně každých 5–6 instrukcí, CPU neví, kterou větví jít.
Out-of-order & spekulativní zpracování
- Out-of-order execution — CPU (nebo už překladač) přehazuje pořadí instrukcí tak, aby se vyhnul blokacím datovými závislostmi.
- Register renaming — CPU flexibilně přejmenovává registry, aby uvolnil falešné závislosti.
- Spekulativní zpracování — když narazí na podmíněný skok, CPU uhádne větev a začne ji preventivně počítat. Pokud trefí, super; pokud ne, musí celou pipeline vyprázdnit (flush) a začít znovu — desítky taktů pryč.
Branch prediction — předvídání podmínek
CPU má speciální Branch Target Buffer (BTB) — malá cache (zhruba velká jako L1) ukládající historii skoků. Na základě stavového automatu (2bitový čítač) odhaduje pravděpodobnost, kam skok povede. Úspěšnost u dobře předvídatelného kódu ~99 %.
Superskalární zpracování
Pokud má program na sobě nezávislé instrukce, moderní CPU má víc pipeline (typicky 2–4) a dokáže provádět víc instrukcí současně. Aby to fungovalo, instrukce musí být dobře seřazené už od překladače.
Knihovna PAPI (Performance API) umožňuje vyčíst z procesoru statistiky o úspěšnosti predikce a využití pipeline.
Loop unrolling — rozbalení smyček
Každý cyklus má skrytou podmínku — kontrolu konce iterace. CPU musí v každém kole tipovat, jestli skok zpět nastane → úzké místo predikce.
// klasická smyčka — n podmínek
for (int i = 0; i < size; i++)
sum += data[i];
// rozbalená 4× — n/4 podmínek
for (int i = 0; i < size; i += 4) {
sum += data[i];
sum += data[i+1];
sum += data[i+2];
sum += data[i+3];
}
Výhody
- 4× méně testů podmínky konce
- Méně špatných predikcí skoku
- Pipeline plynule chrlí instrukce
- Ručně dělané: až 8× zrychlení
Rizika
- Kód narůstá — víc instrukcí
- Hrozí zahlcení instrukční cache
- U složitých těl cyklu způsobí zpomalení
- Smysl jen u krátkých těl cyklu
Provedení: ručně (přímo v kódu, případně C++ šablonami) nebo překladačem (gcc -funroll-loops). V testech ruční bývá lepší než překladačové.
Vektorizace (SIMD)
Procesory mají speciální širší registry (128 / 256 / 512 bitů) a nové sady instrukcí. Jedna instrukce (např. addps — add packed single) sečte najednou několik čísel. Architektuře se říká SIMD (Single Instruction, Multiple Data).
- MMX — historické 64bitové (jen celá čísla).
- SSE → SSE4 — 128 bitů, float i int.
- AVX / AVX2 — 256 bitů.
- AVX-512 — 512 bitů, 16 floatů najednou.
Linuxem detekuješ cat /proc/cpuinfo. Zarovnání dat na násobek registru bylo nutné u SSE, moderní CPU si nezarovnaná data zvládnou bez extra penalizace.
Kdo vektorizuje: typicky překladač s -mavx, -march=native apod. — programátor zasahuje jen pokud překladač zaváhá.
Co brání vektorizaci
- Špatné zarovnání paměti — registry se nejlíp plní z adres zarovnaných na svou šířku. Nezarovnané čtení je dražší.
- Aliasing — překladač neví, jestli se pointery v cyklu (např.
b[i] += a[i]) v paměti nepřekrývají. Pak vektorizaci zakáže pro jistotu (řeší se kvalifikátorem__restrict). - Složité podmínky uvnitř cyklu — pokud se za jistých okolností opouští smyčka, nedá se snadno „rozbalit" do vektoru.
Co u zkoušky chce profesor
- Pipelining + nakreslit diagram 5 fází (IF/ID/OF/IE/OW) a vysvětlit, proč mají kratší pipeline lepší recovery při špatné predikci.
- Superskalární + spekulativní zpracování — co to je, jak BTB tipuje.
- Unrolling — co to je, kdy se vyplatí, kdy ne. Vědět, že ruční unroll (do 16×) může předehnat
-funroll-loops. - Vektorizace — co to je (registry + SIMD instrukce), kdo to dělá (překladač), co tomu brání (alignment, aliasing, podmínky).
- Tabulky výsledků z hodin spíš ano (grafy ne).
Architektury se sdílenou pamětí
Cache-coherence problem, sdílené proměnné, programování architektur se sdílenou pamětí.
SMP / UMA — model sdílené paměti
Klasický vícejádrový procesor (notebook, server do ~64 jader). Všechny CPU jsou si rovny a mají stejně rychlý přístup do jedné globální RAM:
- UMA (Uniform Memory Access) — přístup k jakékoliv adrese trvá stejně.
- SMP (Symmetric Multiprocessor) — víc CPU, jedna RAM, jeden BUS.
Cache-coherence problem
Každé jádro má vlastní L1/L2 cache. Když dvě jádra načtou stejnou proměnnou X = 7 a jedno ji změní na 2, druhé o tom neví a počítá se zastaralou (stale) hodnotou. Hardware to musí řešit.
MESI invalidate protocol — každá cache line má jeden ze čtyř stavů: Modified, Exclusive, Shared, Invalid.
- Shared — víc jader ji má stejnou.
- Modified — jedno jádro ji změnilo, ostatní její kopie jsou neplatné.
- Invalid — kopie už není aktuální, musí se znovu načíst.
Když jádro píše do Shared cache line, pošle ostatním invalidate signál. Sleduje to snoopy cache system (každá cache poslouchá BUS).
Existuje i alternativní update protocol, který místo zneplatnění rozesílá nové hodnoty — ale spotřebuje víc sběrnice. V praxi vyhrál invalidate.
⚠ False sharing
Y a jádro B mění proměnnou Z, ale obě leží v stejné cache line, hardware stále zneplatňuje cache line jednomu druhému, i když si logicky nelezou do zelí. Invalidation storm.
Obrana:
- Padding mezi proměnnými různých vláken — alokovat tak, aby každá proměnná byla na své cache line.
- Klauzule
reductionv OpenMP — mezivýsledky v privátních proměnných každého vlákna, slučují se na konci. - Mezivýsledky vláken neukládat blízko sebe.
U moderních CPU je dopad mírný (desítky procent), ne tak fatální jako dříve, ale stále se vyhneme.
Atomické instrukce
Místo složitých kritických sekcí lze pro jednoduché operace použít atomické instrukce. Jsou implementované přímo v procesoru a nelze je přerušit:
- CAS (compare-and-swap) — porovnej a vyměň.
- TS (test-and-set) — testuj a nastav.
- RMW (read-modify-write) — atomické přečtení, úprava, zápis.
Atomické instrukce mají nižší režii než kritická sekce, ale s rostoucím počtem vláken jejich cena roste (kontence na lince).
OpenMP — standard pro programování sdílené paměti
Bavíme se o vláknech (threads), ne procesech. OpenMP přidává pragmy nad C/C++/Fortran a překladač z toho udělá paralelní kód.
#pragma omp parallel for num_threads(4)
for (int i = 0; i < n; i++)
a[i] = b[i] + c[i];
Klauzule pro proměnné
shared— jedna sdílená kopie všech vláken (implicitní pro globální).private— každé vlákno má vlastní nezávislou kopii, neinicializovanou.firstprivate— private + počáteční hodnota se okopíruje z vnějšku.lastprivate— private + hodnota z poslední iterace se po skončení zapíše do původní proměnné.reduction(op:var)— privátní mezivýsledky se na konci sloučí asociativní operací. Eliminuje race condition i false sharing.
Scheduling — jak rozdělit iterace
| Schedule | Jak rozděluje | Kdy použít |
|---|---|---|
static | Předem rovnoměrně, případně po blocích velikosti chunk | iterace stejně náročné |
dynamic | Bloky velikosti chunk rozdávané za běhu — rychlejší vlákno bere další | nevyvážená zátěž |
guided | Jako dynamic, ale velikost bloku se exponenciálně zmenšuje | nevyvážené, kompromis mezi static a dynamic |
runtime | Určí se až ze systémové proměnné OMP_SCHEDULE | vývoj, ladění |
Synchronizace
#pragma omp critical— kritická sekce, jen jedno vlákno najednou.#pragma omp atomic— atomická aktualizace jedné proměnné (rychlejší než critical).#pragma omp barrier— bariéra: žádné vlákno nepokračuje, dokud nedorazí všechna.#pragma omp single/master— kód provede jen jedno / master vlákno.
// příklad: bezpečná suma přes redukci
double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++)
sum += a[i] * b[i];
Co u zkoušky chce profesor
- Cache coherence = MESI invalidate protokol, popsat na příkladu dvou jader a sdílené proměnné.
- False sharing — co to je, proč vzniká (cache line, ne proměnná!) a jak se mu vyhnout (padding, reduction).
- Rozdíl mezi vláknem a procesem, OpenMP používá vlákna.
- Klauzule OpenMP: private, firstprivate, lastprivate, reduction + schedule (static, dynamic, guided).
- Atomické instrukce — 3 typy (CAS, TS, RMW), kdy nahradit kritickou sekci.
Architektury s distribuovanou pamětí
Komunikace v architekturách s distribuovanou pamětí, komunikační operace, programování architektur s distribuovanou pamětí.
Proč distribuovaná paměť
Sdílená paměť narazí na limit kolem ~64 procesorů — paměťový subsystém přestane stíhat všechny požadavky a cache coherence problém narůstá kvadraticky. Řešení: každý procesor má vlastní lokální RAM a procesory komunikují přes síť (interconnection network). Historicky: Cray T3D (1993).
Místo jednoho jádra běží nezávislé procesy (každý má vlastní adresový prostor). Komunikace probíhá výhradně message passing — předáváním zpráv. Standard: MPI.
Topologie sítí
| Topologie | Spojení | Náklady | Poznámka |
|---|---|---|---|
| Úplně propojená | každý s každým | Θ(p²) | ideální, ale nestavitelná pro velké p |
| Hvězda | vše přes centrální uzel | Θ(p) | střed = úzké hrdlo |
| Lineární řetězec / kruh | sousedi v 1D | Θ(p) | průměr Θ(p), problém u velkých dat |
| Mřížka (mesh) / torus | 2D nebo 3D mřížka | Θ(p) | průměr Θ(√p), Cray T3D, BlueGene |
| Hyperkrychle | uzel = bitový řetězec, soused = liší se v 1 bitu | Θ(p log p) | průměr log p, výborné, ale složité |
| Stromy (fat tree) | hierarchie přepínačů | Θ(p) | moderní klastry (Infiniband) |
Matematika komunikace
Doba odeslání zprávy se rozkládá na tři složky:
- ts (startup time) — inicializace, vytvoření hlavičky paketu, softwarová režie. Velký.
- th (hop time) — čas, než router určí cestu a předá data dál.
- tw (per-word time) — fyzický přenos jednoho slova (závisí na propustnosti).
Packet routing s pipeliningem
Zpráva se posílá jako vláček paketů. Jakmile první paket projde routerem, ten ho hned posílá dál a přijímá další — paralelně. Vzdálenost mezi uzly (l) se tak stává téměř zanedbatelnou a celý vzorec se zjednoduší:
tCOMM ≈ ts + m · twkde m je velikost zprávy. Tedy: startup penalizace + lineární přenos.
MPI — programovací model
MPI (Message Passing Interface) je knihovna a standard. Programátor explicitně volá Send/Recv. Místo vláken: nezávislé procesy (každý má svůj rank).
#include <mpi.h>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
// ... práce podle ranku ...
MPI_Finalize();
}
Blokující vs. neblokující komunikace
Blokující bez bufferu
MPI_Send pošle žádost a čeká, až druhý uzel oznámí připravenost přijímat. Pak teprve pošle data. Bezpečné, ale ⇒ riziko deadlocku.
MPI_Sendrecv.
Blokující s bufferem
Data se zkopírují do bufferu síťové karty a Send hned vrací. Žádné čekání → žádný deadlock, ale kopírování je extra režie a buffer se může přeplnit.
Neblokující
MPI_Isend, MPI_Irecv — síťové kartě se řekne „začni posílat", proces hned pokračuje (např. počítá nezávislé věci). Později MPI_Wait/MPI_Test zkontroluje dokončení. Nejefektivnější — překrývá komunikaci s výpočtem.
Komunikační operace — kreslit a popsat
Tyto chce profesor nakreslit a popsat:
- Broadcast (one-to-all): jeden uzel rozešle identickou kopii dat všem ostatním.
- Reduce (all-to-one): všichni pošlou data jednomu, cestou se asociativní operací (suma, min, max) slučují na síťových prvcích → strom redukce.
- Scatter: jeden uzel rozkrájí velké pole a každému pošle jeho kus.
- Gather: opak — všichni pošlou kousek jednomu, ten poskládá zpět.
- All-to-all (personalized): každý posílá každému jinak specifická data — síťový ekvivalent transpozice matice. Nejnáročnější operace na síť.
Existují i kombinované varianty: MPI_Allreduce (reduce + broadcast výsledku všem), MPI_Allgather (gather + broadcast), MPI_Scan (paralelní prefix-sum).
Co u zkoušky chce profesor
- Vzorec tCOMM ≈ ts + m·tw a důvod, proč hop time mizí (pipelining paketů).
- Deadlock u blokujícího Send/Recv bez bufferu — jak vzniká a jak se mu vyhnout.
- Rozdíl blokující bez bufferu × blokující s bufferem × neblokující.
- Umět nakreslit broadcast a reduce a popsat, co se na síti děje. Circular shift se učit nemusíme.
- Topologie a jejich průměr / náklady.
GPU a CUDA
Popis architektury GPU, spojení s CPU a s globální pamětí, programování GPU pomocí nástroje CUDA, algoritmy vhodné pro běh na GPU.
Filozofie CPU vs. GPU
CPU — málo silných jader
- 4–24 výkonných jader
- velká cache, sofistikované predikce
- optimalizováno pro sekvenční kód s podmínkami
- nízká latence jednotlivých operací
GPU — tisíce hloupých jader
- tisíce malých výpočetních jednotek
- žádné spekulativní zpracování větví
- obrovská propustnost paměti (HBM ~3 TB/s)
- vhodné pro masivně paralelní bezvětevné výpočty
Důvod existence GPU: rendering 3D scén nemá datovou závislost mezi pixely/trojúhelníky → ideální pro masivní paralelizaci. Tato architektura se ukázala jako vynikající i pro maticové operace a neuronové sítě.
Hardwarový model — co umět namalovat
Vlákna, bloky, warpy, grid
- Vlákno (thread) — jednotka výpočtu. Vlákno = void funkce běžící na jednom CUDA core.
- Warp — skupina 32 vláken, která zpracovává vždy stejnou instrukci (SIMT — Single Instruction Multiple Threads). To je hardwarová jednotka.
- Blok (block) — softwarová skupina vláken (typicky 256, max 1024). Celý blok běží na jednom SM a vlákna v něm sdílí shared memory a mohou se synchronizovat.
- Grid — kolekce všech bloků jednoho kernelu. Bloky mezi sebou nemohou komunikovat ani se synchronizovat.
Divergence warpů
Co když warp narazí na if (cond) {A} else {B}, kde půlka vláken má cond=true?
- Vlákna nemohou dělat různé věci najednou.
- SM uspí polovinu warpu, druhá provede
A. - Pak naopak — první polovina provede
B, druhá čeká. - Doba běhu = doba A + doba B → ztráta výkonu na polovinu (nebo víc).
Proto se v CUDA kódu vyhýbáme rozsáhlým if/else uvnitř kernelu.
Paměťový model GPU
- Globální paměť — VRAM/HBM, viditelná všem vláknům, ~3 TB/s ale latence stovky taktů.
- Sdílená paměť (shared memory) — uvnitř SM, ~100× rychlejší než globální, viditelná jen v bloku. Programátor ji explicitně řídí.
- Registry — per-vlákno, nejrychlejší.
- Konstantní paměť — read-only, cachovaná.
Coalesced memory access — sloučené přístupy
Pokud 32 vláken warpu přistupuje na 32 sousedních adres globální paměti, GPU sloučí čtení do jediné transakce. Pokud přistupují roztroušeně (misaligned), vyžádá si to vícero transakcí a sběrnice se ucpe.
Memory banks ve sdílené paměti
Shared memory je rozdělena na banky. Pokud dvě vlákna sahají do stejné banky, vzniká bank conflict a přístupy se serializují. Programátor by měl zařídit, aby každé vlákno sahalo do jiné banky.
Programování v CUDA
__global__ void vectorAdd(float* a, float* b, float* c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + b[i];
}
// host (CPU):
cudaMalloc(&dA, n*sizeof(float));
cudaMemcpy(dA, hA, n*sizeof(float), cudaMemcpyHostToDevice);
// spuštění: 256 vláken na blok, ceil(n/256) bloků
vectorAdd<<<(n+255)/256, 256>>>(dA, dB, dC, n);
cudaMemcpy(hC, dC, n*sizeof(float), cudaMemcpyDeviceToHost);
CUDA podporuje C++ šablony, lambda funkce (částečně), printf z kernelu, ale ne všechny knihovny.
Pravidla pro efektivní kód na GPU
- Minimalizovat přenos CPU ↔ GPU — PCIe je úzké hrdlo. Data nahraj na začátku, výsledek vrať na konci, mezi tím komunikuj minimálně.
- Optimalizovat přístup do globální paměti — využívat coalesced access, kde to jde.
- Omezit divergenci vláken — minimalizovat
if/elseuvnitř warpu, raději věci uspořádat tak, aby v jednom warpu byla stejná větev. - Zvolit vhodnou velikost bloku — typicky 128–512, mocniny dvou. Dotaz
occupancyspočítá efektivitu. - Velké úlohy — GPU se vyplatí jen u objemných výpočtů. Pro malé úlohy režie přenosu spolyká celý zisk.
Pokud potřebujeme intenzivně komunikovat mezi CPU a GPU, existují hybridní karty se sdílenou pamětí (NVIDIA Grace Hopper).
Co u zkoušky chce profesor
- Hardwarové schéma: umět nakreslit Device → SM → bloky → vlákna. Warp = 32 vláken SIMT. Bloky mezi sebou nekomunikují.
- Paměťový model: globální + sdílená + registry, kde co je rychlé.
- Coalesced access + divergence warpů — vysvětlit dopady.
- Čtyři zásady efektivního kódu — minimalizace PCIe, optimalizace globální paměti, omezení divergence, volba velikosti bloku.
- Kdy se GPU vyplatí: velké, bezvětevné, datově nezávislé úlohy (matice, neuronky, image processing).
Paralelní redukce a prefix-sum
Popis algoritmu paralelní redukce, analýza efektivity, nákladově optimální paralelní redukce, prefix-sum a nákladově optimální prefix-sum, segmentovaný prefix-sum.
Paralelní redukce — definice
Paralelní redukce — máme pole prvků a₁, …, aₙ a asociativní operaci ⊕. Redukcí dostaneme jeden prvek:
a = a₁ ⊕ a₂ ⊕ … ⊕ aₙOperace musí být asociativní (komutativita není nutná). Příklady: součet, součin, min, max, skalární součin, normy, porovnání.
Sekvenční implementace je triviální for cyklus a má složitost TS(n) = Θ(n).
Stromová paralelní redukce
Paralelní verze využívá binární strom: v každé hladině polovina vláken sečte dvojici sousedů. Po log₂n krocích máme výsledek.
Složitost (n = p)
Algoritmus není nákladově optimální — cena je o faktor log n vyšší než sériový čas.
Nákladově optimální redukce
Klíč: méně procesorů (p < n). Postup:
- Sekvenční fáze: každý z p procesorů zredukuje sekvenčně svůj blok n/p prvků → čas Θ(n/p).
- Paralelní fáze: stromová redukce p mezivýsledků → čas Θ(log p).
Pro n = Ω(p log p) platí C = Θ(n) = Θ(TS) — algoritmus je nákladově optimální. Cenou je, že nevyužíváme všechny procesory naráz (max ~n/log n).
V OpenMP přímou podporu má klauzule reduction(+:sum); v MPI MPI_Reduce a MPI_Allreduce.
Prefix sum — definice
Inkluzivní prefix sum — pole s₁, …, sₙ kde:
si = a₁ ⊕ a₂ ⊕ … ⊕ aiExkluzivní prefix sum — σ₁ = 0 a σi = a₁ ⊕ … ⊕ ai−1 pro i > 1.
Příklad: vstup [3, 7, 2, 5] → inkluzivní [3, 10, 12, 17], exkluzivní [0, 3, 10, 12].
Paralelní prefix sum
Vypadá jako sekvenční problém (každý prvek závisí na předchozím), ale jde paralelizovat stromově — stejný princip jako redukce, jen si uchováme i mezivýsledky. Složitost stejná jako redukce:
TS = Θ(n), TP = Θ(log n)Algoritmus s n procesory není nákladově optimální — opět faktor log n nadbytek práce.
Nákladově optimální prefix sum
Podobně jako redukce: p < n, blok na proces.
- Sekvenční fáze: každý procesor sekvenčně napočítá vnitřní prefix sum svého bloku Ak. Označme Sk hodnotu posledního prvku tohoto vnitřního prefixu.
- Paralelní fáze: z posloupnosti S₁, …, Sp napočítáme exkluzivní prefix sum Σk — paralelně.
- Korekce: každý procesor přičte ke svým vnitřním prefixům konstantu Σk.
Stejné jako u nákladově optimální redukce — pro n = Ω(p log p) je algoritmus nákladově optimální.
Segmentovaný prefix sum
Segmentovaný prefix sum — najednou napočítáme prefix sum několika nezávislých segmentů. Začátek každého segmentu se označí příznakem fi = 1, jinak 0.
[1, 2, 3 | 4, 5, 6, 7, 8] → [1, 3, 6 | 4, 9, 15, 22, 30]Trik: definujeme operaci ⊕ na párech (hodnota, příznak):
(si, fi) ⊕ (sj, fj) = (fj==0 ? si+sj : sj, fi | fj)Operace je asociativní → můžeme přímo použít standardní paralelní prefix-sum algoritmus.
Kde se prefix-sum používá
- Filter / compaction — z masky (0/1) vytvořit kompaktní pole označených prvků.
- Radix sort — výpočet pozic přihrádek z histogramu (počty → offsety).
- CSR rowPointers — počty nenul v řádcích → ukazatele na začátky řádků.
- Alokace bufferů proměnné velikosti, dynamické fronty BFS.
Co u zkoušky chce profesor
- Stromová redukce — nakreslit, vysvětlit, vypočítat TP = Θ(log n).
- Proč není nákladově optimální a jak ji udělat (sekvenční fáze + stromová na p mezivýsledcích).
- Prefix sum má stejnou složitost jako redukce, optimalizace stejnou metodikou.
- Segmentovaný prefix sum — že existuje a jak se zakóduje (operace na párech, asociativní).
- Použití prefix-sum: kompakce, radix sort, CSR.
Paralelní quicksort
Paralelní quicksort pro architektury se sdílenou a distribuovanou pamětí, analýza efektivity paralelního quicksortu.
Quicksort — sekvenční verze
Quicksort (C. A. R. Hoare, 1962) — algoritmus typu rozděl a panuj. V každém kroku vybere pivota a rozdělí posloupnost na dvě části:
- S — prvky menší než pivot
- L — prvky větší než pivot
Obě části se zpracují rekurzivně. Průměrná složitost: TS(n) = Θ(n log n), nejhorší případ Θ(n²).
Proč je paralelizace netriviální
- První volání = celé pole — máme jen 1 task. Až později se tasků zdvojnásobuje.
- Citlivost na pivot — pokud pivot rozdělí pole nerovnoměrně (např. 90:10), procesory dostanou nerovnoměrnou zátěž.
- Sekvenční quicksort je už dost rychlý — paralelní verze musí zaplatit režii pro převrácení.
Quicksort se sdílenou pamětí
Nesetříděná posloupnost je rozdělená mezi p vláken. Každé vlákno i drží podposloupnost.
- Volba pivota — jedno vlákno (např. master) vybere pivota a uloží ho do sdílené proměnné. Všichni si ho přečtou.
- Lokální partition — každé vlákno i rozdělí své prvky na Si (menší) a Li (větší).
- Přeskládání do pomocného pole — pomocí dvou sdílených ukazatelů
s = 0al = n−1:smaller = atomicAdd(s, |Si|)— atomicky získá pozici pro své menší prvky (S roste zleva).larger = atomicSubtract(l, |Li|)— atomicky získá pozici pro větší (L roste zprava).
- Každé vlákno zkopíruje své S do
smallera své L dolarger. - Pole se prohodí, vlákna si nově rozdělí prvky úměrně počtu a celý postup se opakuje.
atomicAdd/atomicSub jsou sekvenční primitiva (serializují přístup). S rostoucím p roste jejich cena — kontence na jediném slově v paměti.
Quicksort s distribuovanou pamětí
Místo zarážek S/L se použije prefix-sum:
- Pivot — vyberou se na jednom procesu a broadcastem rozešle všem ostatním.
- Lokální partition — každý proces i rozdělí své prvky na Si a Li.
- Prefix-sum pro pozice v globálním poli: si = Σj<i |Sj|, li = Σj<i |Lj|
- Komunikace — proces i pošle své S na pozici si (sem může jít na jiný uzel) a L na pozici li. Typicky
MPI_Alltoallv. - Posloupnost je teď ve dvou polovinách (S vlevo, L vpravo) — zopakuj rekurzivně pro každou polovinu na polovině procesů.
Hyperquicksort — řešení špatného pivota
Hyperquicksort — modifikace, která řeší citlivost na pivot:
- Každý proces si nejprve sekvenčním quicksortem setřídí lokální část.
- Z lokálních dat snadno najde svůj medián.
- Z lokálních mediánů se určí celkový medián = kvalitní pivot.
- Dále se postupuje jako u běžného paralelního quicksortu.
Pivot je teď reprezentativnější → vyvážená zátěž; navíc lokální partition je rychlejší (data jsou setříděná).
Složitost paralelního quicksortu
Algoritmus je nákladově optimální a má lineární zrychlení — pokud máme rozumný pivot.
Co u zkoušky chce profesor
- U quicksortu na sdílené paměti umět vysvětlit zarážky S a L + atomic operace.
- U distribuované verze umět vysvětlit, že místo zarážek se použije prefix-sum.
- Analýza: TP = Θ((n/p) log n), S = p, E = 1, nákladově optimální.
- Citlivost na pivot a jak ji řeší hyperquicksort (lokální medián → globální medián).
Radix sort — LSD a MSD varianta
MSD a LSD varianty radixsortu, paralelizace na architekturách se sdílenou a distribuovanou pamětí.
Definice — radix sort
Radix sort — třídí celočíselné klíče lexikograficky podle jejich zápisu v soustavě o základu R. Místo srovnávání používá histogram + přihrádky.
- LSD (least significant digit) — od nejméně významné číslice. Po každé fázi je pole stabilně setříděné podle té jedné číslice; po všech fázích je hotovo.
- MSD (most significant digit) — od nejvíce významné. Rekurzivně třídí jednotlivé přihrádky. Pro R = 2 je vlastně variantou quicksortu.
Sekvenční složitost: TS(n) = Θ(R · n). Budeme paralelizovat LSD verzi.
Princip jednoho kroku LSD — count + prefix-sum
- Histogram (count): pro každou číslici j spočítám počet prvků s touto číslicí na aktuální pozici.
- Prefix-sum z count dá offsety přihrádek: offset[j] = Σk<j count[k] To je pozice prvního prvku přihrádky j v pomocném poli.
- Přeřazení (scatter): každý prvek se zařadí na pozici
offset[jeho_číslice], offset se inkrementuje. Stejné číslice zachovají pořadí → stabilní třídění. - Opakuj pro každou pozici, od nejméně po nejvíce významnou.
Paralelizace: každý procesor napočítá lokální histogram. Kombinovaným prefix-sumem se z lokálních histogramů určí globální offsety. Pak každý procesor zapíše své prvky na správné pozice.
Radix sort se sdílenou pamětí — tři pokusy
Algoritmus 1 — naivní
Histogramy v poli count[j][pid] — index podle číslice, pak podle vlákna.
count[j][pid] a count[j][pid+1], což jsou sousední paměťové lokace v stejné cache line. Invalidation storm → katastrofa.
Algoritmus 2 — přerovnání indexů
Pole count[pid][j] — primární index je vlákno, takže různá vlákna píší do jiných cache lines.
- ✓ false sharing odstraněn
- ✗ špatná datová lokalita — v každé iteraci se prvky přehazují napříč celým polem, příští iteraci jiné vlákno přebírá data vzdálená v paměti.
Algoritmus 3 — MSD první krok + LSD rekurzivně
Klíčové vylepšení:
- První krok provedu jako paralelní MSD — prvky se rozsejou do přihrádek podle nejvyšší číslice.
- Přihrádky se rozdělí mezi procesory — žádný prvek se už dál nestěhuje mezi vlákny.
- Každý procesor pak třídí jen svou přihrádku standardním LSD.
Naměřené výsledky (16M prvků, SGI Origin 2000)
| Algoritmus | L2 cache misses | Invalidace | Čas |
|---|---|---|---|
| Alg. 1 — naivní | 9 828 950 | 9 725 467 | 51,29 s |
| Alg. 2 — bez false sharing | 1 534 000 | 251 744 | 5,76 s |
| Alg. 3 — MSD první + lokalita | 833 559 | 311 237 | 2,55 s |
20× zrychlení mezi Alg. 1 a Alg. 3 — z toho 80 % je odstranění false sharing, zbytek lepší cache lokalita.
Radix sort s distribuovanou pamětí
- Každý uzel provede lokálně jednu iteraci radix sortu, v
count[pid]má rozmezí přihrádek. MPI_Allgather— všechny uzly získají kompletní polecount.- Z počtů se vypočítá bucketDistribution — celkové velikosti přihrádek a kdo dostane kterou (může se rozdělit i jedna přihrádka mezi dva uzly pro vyvážení).
MPI_Alltoallv— výměna dat: každý uzel pošle své prvky správným uzlům.
Řešení — MSD varianta: po prvním MSD kroku se přihrádky rozdělí mezi uzly a další komunikace už neprobíhá. Výsledek je stejně efektivní jako u sdílené paměti.
Co u zkoušky chce profesor
- Vysvětlit LSD a MSD — od které číslice začínají, kdy která vyhraje.
- Sekvenční LSD pomocí count + prefix-sum — popsat každý ze 4 kroků.
- Paralelizace: lokální histogramy → kombinovaný prefix-sum → každý procesor zapíše své.
- False sharing u
count[j][pid]— proč vzniká, jak ho odstranit (přerovnání nacount[pid][j]). - Proč je MSD lepší pro paměť (spatial locality) a pro distribuovanou paměť (žádná komunikace po prvním kroku).
Bitonic sort
Paralelizace třídících algoritmů: bitonic sort.
Třídící sítě — komparátory
Bitonic sort patří mezi třídící sítě — pevné struktury nezávislé na datech. Základní stavební prvek je komparátor:
Komparátor = zobrazení dvojice (x, y) na uspořádanou dvojici (x', y').
- Rostoucí ⊕: x' = min(x,y), y' = max(x,y) — menší nahoru.
- Klesající ⊖: x' = max(x,y), y' = min(x,y) — větší nahoru.
Bitonická posloupnost
Bitonická posloupnost — posloupnost, která se skládá z jedné rostoucí a jedné klesající podposloupnosti, nebo to lze získat libovolným cyklickým posunutím (rotací).
Příklady: [1, 4, 7, 5, 3, 2] (roste, pak klesá), [5, 3, 1, 2, 4, 7] (klesá, pak roste — bitonická po rotaci), [a, b] pro libovolné a, b (každá dvojice je bitonická).
Bitonické rozdělení (bitonic split)
Z bitonické posloupnosti a = {a₀, …, a2n−1} vytvoří dvě posloupnosti délky n:
smin = {min(a₀,an), min(a₁,an+1), …, min(an−1,a2n−1)} smax = {max(a₀,an), max(a₁,an+1), …, max(an−1,a2n−1)}Implementace: n paralelních komparátorů porovnávajících prvky vzdálené o n.
Klíčová věta o bitonickém rozdělení
Jsou-li smin a smax výsledkem bitonického rozdělení bitonické posloupnosti, pak:
- Obě smin i smax jsou bitonické.
- max(smin) ≤ min(smax).
Důsledek: po jednom kroku máme dvě nezávislé bitonické posloupnosti — můžeme je třídit paralelně, opakovat rekurzivně až k délce 1.
Bitonic sort — síť komparátorů
Stavíme bitonické posloupnosti shora dolů a pak třídíme:
- Délka 1 → bitonická (triviálně).
- Dvě délky 2 (jedna ⊕ rostoucí, jedna ⊖ klesající) → bitonická délky 4.
- Dvě délky 4 → bitonická délky 8…
- Bitonická délky n → setříděná délky n pomocí bitonického merge (log n kroků).
Složitost
V každém kroku pracuje až n/2 komparátorů paralelně. Síť je data-independent — struktura se nemění s daty.
Proč je bitonic sort ideální pro GPU
- Pevná struktura — komparátory se mapují přesně, žádné dynamické rozhodování.
- Žádná divergence warpů — všechna vlákna provádějí stejnou instrukci (komparátor) na různých datech. Ideální SIMT.
- Implementace v sdílené paměti SM je přímočará (pole velikosti bloku).
- Nákladově není optimální (Θ(n log²n) celková práce vs. Θ(n log n) u quicksortu), ale konstanta je malá a hardware to vykompenzuje.
Co u zkoušky chce profesor
- Definice bitonické posloupnosti (rostoucí + klesající nebo po rotaci).
- Definice a smysl bitonického rozdělení + věta o něm (smin, smax bitonické, max(smin) ≤ min(smax)).
- Umět nakreslit síť komparátorů pro n = 8 (nebo aspoň pochopit její strukturu).
- Složitost: TP = Θ(log²n), data-independent, ideální pro GPU.
Cannonův algoritmus a paralelní Gaussova eliminace
Cannonův algoritmus pro násobení matic na architekturách s distribuovanou pamětí, paralelní Gaussova eliminační metoda.
Blokové distribuované násobení matic
Pro matice A, B, C ∈ ℝn×n a p procesů (mocnina dvou) rozdělíme každou matici na √p × √p bloků. Každý blok Ci,j má spočítat jeden proces jako:
Ci,j = Σk=0√p−1 Ai,k · Bk,jPřímočará verze a její problém
Pro p = 9 (tedy 3×3 bloků) vypadá první krok výpočtu:
C0,0 = A0,0B0,0 + A0,1B1,0 + A0,2B2,0 C1,0 = A1,0B0,0 + A1,1B1,0 + A1,2B2,0 C2,0 = A2,0B0,0 + A2,1B1,0 + A2,2B2,0Cannonův algoritmus — rotace bloků
Cannonův algoritmus — změníme pořadí součtu tak, aby v jednom kroku každý proces pracoval s jiným blokem. Posunutím (rotací) řádků A o i a sloupců B o j:
Ci,j = Σk Ai, (k+i+j) mod √p · B(k+i+j) mod √p, jV každém kroku se A bloky posunou doleva po řádcích, B bloky nahoru po sloupcích → každý proces dostane nové operandy od souseda. Komunikace je pravidelná a lokální (po mříži).
Gaussova eliminační metoda (GEM)
GEM převádí soustavu Ax = b na horní trojúhelníkovou:
Ax = b ⟹ Ux = dV k-tém kroku od každého řádku i > k odečteme βi = aik/akk-násobek řádku k. Po n−1 krocích je matice horní trojúhelníková.
Paralelní GEM — přímý chod
- V kroku k vlákno k (pivotní řádek) vydělí svůj řádek pivotem a uloží do sdílené paměti.
- Všechna ostatní vlákna i > k najednou a paralelně odečtou βik-násobek řádku k → jeden paralelní krok.
- Na GPU: vynechat sdílenou paměť pivota a každé vlákno si dělí samo — odpadne čekání.
Lineární zrychlení, nákladově optimální. Nevýhoda: ke konci eliminace ubývá aktivních řádků — některá vlákna se nudí.
Problém zpětné substituce
Řešení — diagonalizace (Gauss-Jordan)
Místo trojúhelníkové matice nulujeme prvky nad i pod diagonálou. Výsledkem je diagonální matice → žádná zpětná substituce (xi = di).
- Provedeme více aritmetických operací (řádově dvojnásobek).
- Ale veškerá práce je plně paralelizovatelná.
- Na superpočítači se vyplatí: čekání procesů je dražší než redundantní práce.
Pro řídké matice se klasický GEM nehodí (vyplnění — fill-in). Místo něj specializované řešiče (PARDISO, SuperLU) nebo iterativní metody (viz otázka 11–12).
Co u zkoušky chce profesor
- Cannonův algoritmus: proč nestačí přímočará dekompozice (konflikty), jak rotace bloků (A doleva, B nahoru) konflikty eliminuje.
- Bloky velikosti √p × √p, komunikace lokální po mříži.
- GEM: přímý chod je paralelní (každý řádek na vlákno), zpětná substituce je sekvenční úzké hrdlo.
- Řešení = diagonalizace (Gauss-Jordan) — víc výpočtů, ale plně paralelní bez zpětné substituce.
- Složitost: TP = Θ(n³/p), lineární zrychlení.
Tridiagonální soustavy
Paralelní algoritmus pro řešení lineárních soustav s tridiagonální maticí soustavy.
Tridiagonální soustava
Soustava Ax = d, kde matice A má nenulové prvky jen na hlavní diagonále a dvou přilehlých:
ci xi−1 + ai xi + bi xi+1 = di, i = 1, …, nkde c₁ = 0 a bn = 0. Vznikají např. při diskretizaci jednodimenzionálních PDR (1D vedení tepla, struny).
Thomasův algoritmus — sekvenční
Thomasův algoritmus — speciální případ GEM pro tridiagonální matici (předpokládá silnou regularitu — všechny přední hlavní podmatice jsou regulární, takže není potřeba pivotování). GEM ji převede na bidiagonální tvar:
xi + μi xi+1 = ρi, xn = ρnZpětná substituce: xk = ρk − μk xk+1 od k = n−1 dolů. Složitost: Θ(n), lineární.
Cyklická redukce — paralelní trick
Cyklická redukce — paralelní algoritmus pro tridiagonální soustavy. Myšlenka: z řádku i eliminujeme oba sousedy (xi−1 a xi+1). Vznikne nová rovnice obsahující jen xi−2, xi, xi+2 — opět tridiagonální, ale poloviční velikosti.
Odvození jednoho kroku
Bereme řádek i a jeho dva sousedy:
- ci−1xi−2 + ai−1xi−1 + bi−1xi = di−1
- cixi−1 + aixi + bixi+1 = di
- ci+1xi + ai+1xi+1 + bi+1xi+2 = di+1
Zvolíme βi = ci/ai−1 a γi = bi/ai+1. Od prostřední rovnice odečteme βi×první a γi×třetí. Eliminujeme xi−1 i xi+1. Výsledek:
ci(1) xi−2 + ai(1) xi + bi(1) xi+2 = di(1)kde:
- ci(1) = −βi ci−1
- ai(1) = ai − βibi−1 − γici+1
- bi(1) = −γi bi+1
- di(1) = di − βidi−1 − γidi+1
Tuto úpravu lze provést na všech sudých řádcích nezávisle a paralelně. Vznikne tridiagonální soustava poloviční velikosti jen pro sudé neznámé x₂, x₄, …, xn. Rekurzivně opakujeme.
Složitost cyklické redukce
Paralelizace není nákladově optimální — v každém kroku se aktivních procesů půlí (po log n krocích už dělá jen 1 proces). Cena: o faktor log n víc práce než Thomas.
Úplná cyklická redukce
Místo zpětné substituce (dalších log n kroků) provedeme redukci v obou směrech najednou:
- V prvním kroku eliminujeme liché řádky → soustava jen pro sudé neznámé.
- Současně eliminujeme sudé řádky → soustava jen pro liché neznámé.
- Vzniknou dvě nezávislé tridiagonální soustavy poloviční velikosti.
- Na každou aplikujeme stejný postup rekurzivně.
- Po ⌊log₂ n⌋ krocích máme n/2 nezávislých 1- až 2-rovnicových soustav → vyřešíme triviálně paralelně.
Co u zkoušky chce profesor
- Tvar tridiagonální soustavy a fakt, že Thomas je čistě sekvenční.
- Princip cyklické redukce — z řádku i a sousedů eliminujeme xi−1, xi+1 → nová tridiagonální soustava poloviční velikosti.
- Umět nakreslit diagram dopředné a zpětné fáze.
- Složitost: TP = Θ(log n), není nákladově optimální.
- Úplná cyklická redukce — vznikají dvě nezávislé soustavy, žádné nečinné procesy.
Paralelní procházení grafu a nejkratší cesta
Paralelní algoritmus pro procházení grafu do šířky (BFS) a výpočet nejkratší cesty z jednoho zdroje (SSSP).
Proč jsou grafy obtížné na paralelizaci
Grafy mají nepravidelnou strukturu (každý uzel má jiný počet sousedů) a typické algoritmy (BFS, DFS) dělají náhodné přístupy do paměti — cache misses na cache misses. Geniální trik celé přednášky:
Polookruh (semiring)
Polookruh (S, ⊕, ⊙, 0, 1) — algebraická struktura se dvěma operacemi. (S, ⊕, 0) komutativní monoid, (S, ⊙, 1) monoid, distributivita, a ⊙ 0 = 0.
Změnou definice ⊕ a ⊙ dostaneme úplně jinou úlohu při stejné maticové aritmetice.
| Polookruh | ⊕ | ⊙ | 0 | 1 | Použití |
|---|---|---|---|---|---|
| Standardní | + | · | 0 | 1 | klasická lin. algebra |
| Booleovský | ∨ (OR) | ∧ (AND) | 0 | 1 | BFS, dosažitelnost |
| Tropický (min-plus) | min | + | ∞ | 0 | SSSP, APSP |
| Max-plus | max | + | −∞ | 0 | nejdelší cesta, plánování |
BFS — průchod do šířky
BFS projde všechny vrcholy v komponentě souvislosti. Začíná s jedním (či více) startovním vrcholem, v každém kroku rozšíří frontu o všechny dosud nenavštívené sousedy.
Algebraická formulace
Buď x vektor s 1 na pozici startovního uzlu a 0 jinde. Operace v booleovském polookruhu:
y = AT(∧.∨) x |i = x₁ ∧ ai,1 ∨ … ∨ xn ∧ ai,nVýsledek y má 1 na pozicích uzlů, do nichž se v jednom kroku dá dostat. Opakovaným násobením AT(∧.∨) šíříme frontu.
SSSP — nejkratší cesta z jednoho zdroje
SSSP (Single-Source Shortest Path) — pro souvislý graf G(V, E) s vahami hran najdi nejkratší cesty ze zvoleného startu do všech ostatních uzlů.
Dijkstrův algoritmus — proč se nehodí
- Inicializuj: d(start) = 0, ostatní ∞.
- Vyber nenavštívený uzel s minimálním d(u).
- Relaxuj všechny jeho sousedy.
- Označ navštívený, opakuj.
Funguje jen pro nezáporné váhy. Složitost O((n+m) log n) s haldou.
Bellman-Ford v tropickém polookruhu
Bellman-Ford je iterativní — v každém kroku zkouší zkrátit cesty přes nějakou hranu. Funguje i pro záporné váhy (ale ne pro záporné cykly). Naivní složitost O(n · m).
(ℝ ∪ {∞}, min, +, ∞, 0). Maticová operace:
[A (min.+) d]i = min(d₁ + ai,1, …, dn + ai,n)Bellman-Ford lze zapsat jako opakované d ← A (min.+) d — tj. SpMV v tropickém polookruhu.
Dijkstra vs. Bellman-Ford — srovnání
| Dijkstra | Bellman-Ford (alg.) | |
|---|---|---|
| Záporné váhy | ne | ano (ne záp. cykly) |
| Paralelizovatelnost | špatná (sekvenční výběr minima) | dobrá (SpMV) |
| Iterací | n kroků | ≤ n−1 iterací |
| Vhodnost pro GPU | nízká | vysoká |
Shrnutí — co se paralelizuje a vyplatí
| Algoritmus | Co paralelizujeme | Vyplatí se? |
|---|---|---|
| BFS | zpracování celé fronty (SpMV v Bool. polookruhu) | závisí na hustotě grafu |
| DFS | — | špatně, téměř sekvenční |
| Dijkstra | relaxace sousedů jednoho uzlu | špatně (min je sekvenční) |
| Bellman-Ford | každá iterace = SpMV (tropický) | dobře — vhodné pro GPU |
| Floyd-Warshall (APSP) | vnitřní dvojsmyčka pro dané w | dobře, O(n) kroků |
Co u zkoušky chce profesor
- Polookruh jako rámec — booleovský pro BFS, tropický (min, +) pro SSSP.
- BFS jako AT(∧.∨)x — umět vysvětlit, proč jedno násobení odpovídá jednomu kroku fronty.
- Dijkstra se špatně paralelizuje — sekvenční výběr minima v priority queue.
- Bellman-Ford v tropickém polookruhu = opakovaná SpMV → výborně paralelní, vhodné pro GPU.
- Pseudokód není třeba; popis slovy.