FJFI ČVUT · AIPV · Státnice 2026 · Kryštof Krejčí

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.

12okruhů
~40klíčových pojmů
~20vizualizací
1obor: AIPV

Seznam otázek

  1. 01Paměťový subsystémSRAM/DRAM, cache, lokalita, sekvenční vs. náhodný přístup
  2. 02Sekvenční architektury a vektorizacePipelining, out-of-order, branch prediction, SIMD, unrolling
  3. 03Architektury se sdílenou pamětíCache coherence (MESI), false sharing, OpenMP, atomické instrukce
  4. 04Architektury s distribuovanou pamětíTopologie sítí, ts + m·tw, MPI, deadlock, komunikační operace
  5. 05GPU a CUDASM, blok, warp, SIMT, divergence, coalesced access, efektivní kód
  6. 06Paralelní redukce a prefix-sumStromová redukce, nákladová optimalita, segmentovaný prefix sum
  7. 07Paralelní quicksortSdílená vs. distribuovaná verze, citlivost na pivot, hyperquicksort
  8. 08Radix sort — LSD a MSDCount + prefix-sum, false sharing, MSD a datová lokalita
  9. 09Bitonic sortBitonická posloupnost, bitonic split, síť komparátorů, log²n hloubka
  10. 10Cannonův algoritmus a GaussBloky √p × √p, rotace bloků, GEM, diagonalizace
  11. 11Tridiagonální soustavyThomas, cyklická redukce, úplná cyklická redukce
  12. 12Paralelní grafové algoritmyBFS jako SpMV, polookruhy, Dijkstra vs. Bellman-Ford
Otázka 01

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:

Definice

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).

Definice

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í

Cache — vyrovnávací paměti

Cache je malá rychlá SRAM mezi CPU a RAM. Velikost ~1/1000 RAM. Moderní procesory mají tři úrovně:

Pravidlo: čím menší číslo, tím menší ale rychlejší cache.

Definice — Lokality

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

TypJak fungujeCenaPraxe
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-associativeKompromis: 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

⚠ Sekvenční vs. náhodný přístup — klíčový obrázek

Tohle profesor u zkoušky chce nakreslit:

Sekvenční vs. náhodný přístup velikost dat (log) takty / element sekvenční prefetching pracuje náhodný cache misses → RAM (~100ns) L1 L2 L3
1.1Náhodný přístup strmě roste, jakmile dataset přeroste velikost L1/L2/L3.

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.

3. Critical stride — zákeřný hardwarový jev

Critical stride: pokud je velikost matice násobkem mocniny 2 (např. 512, 1024, 2048), všechny prvky jednoho sloupce se mapují do stejné cache sady → cache se neustále přepisuje. Výpočet trvá řádově déle.
Ř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.

Tři pravidla pro programátora:
  1. Data v paměti uspořádej tak, aby se k nim přistupovalo sekvenčně.
  2. Když nelze, rozděl výpočet na menší bloky, které se vejdou do cache (temporal locality).
  3. Načtená data v cache využij co nejvíc — než je vykopnou nová.
Zkouškový bod

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).
Otázka 02

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

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):

5-stupňová pipeline (čas →) t1t2 t3t4 t5t6 t7 I1 IF ID OF IE OW I2 IF ID OF IE OW I3 IF ID OF IE OW V taktu t3 jsou v pipelině současně 3 instrukce v různých fázích. IF — fetch · ID — decode · OF — operand fetch · IE — execute · OW — write back
2.1Pět fází instrukce — v každém taktu zpracovává CPU jinou fázi pěti různých instrukcí.

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

  1. Datová závislost — instrukce čeká na výsledek předchozí, který ještě není hotový.
  2. Závislost na zdrojích — dvě instrukce chtějí stejný registr / paměťové místo.
  3. 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í

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 %.

Riziko špatného odhadu: u zcela náhodných dat (např. třídění náhodných čísel s if-větví) předpoví špatně v 50 % případů → pipeline se neustále plní špatnými instrukcemi a flushuje. Program běží násobně pomaleji, ačkoliv dělá totéž. Proto se delší pipeline (~30 fází Netburst) ukázaly jako slepá ulička.

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).

Sady vektorových rozšíření
  • 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

  1. Špatné zarovnání paměti — registry se nejlíp plní z adres zarovnaných na svou šířku. Nezarovnané čtení je dražší.
  2. 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).
  3. Složité podmínky uvnitř cyklu — pokud se za jistých okolností opouští smyčka, nedá se snadno „rozbalit" do vektoru.
Zkouškový bod

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).
Otázka 03

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:

SMP / UMA — sdílená RAM, vlastní cache CPU 0 CPU 1 CPU 2 CPU 3 L1/L2 L1/L2 L1/L2 L1/L2 sběrnice (BUS) RAM (sdílená)
3.1Sdílená RAM, ale každé jádro má svojí lokální cache — odsud cache-coherence problém.

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.

Definice — MESI

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

False sharing: protokol nehlídá jednotlivé proměnné, ale celé cache lines (64 bytů). Když jádro A mění proměnnou 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:

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:

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é

Scheduling — jak rozdělit iterace

ScheduleJak rozdělujeKdy použít
staticPředem rovnoměrně, případně po blocích velikosti chunkiterace stejně náročné
dynamicBloky velikosti chunk rozdávané za běhu — rychlejší vlákno bere dalšínevyvážená zátěž
guidedJako dynamic, ale velikost bloku se exponenciálně zmenšujenevyvážené, kompromis mezi static a dynamic
runtimeUrčí se až ze systémové proměnné OMP_SCHEDULEvývoj, ladění

Synchronizace

// 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];
Pravidlo: zacni bez atomických instrukcí (jen redukcemi); pokud nestačí, přidej atomic; critical jen když opravdu nutno. Počet vláken nastav přibližně podle počtu jader.
Zkouškový bod

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.
Otázka 04

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í

TopologieSpojeníNákladyPoznámka
Úplně propojenákaždý s každýmΘ(p²)ideální, ale nestavitelná pro velké p
Hvězdavše přes centrální uzelΘ(p)střed = úzké hrdlo
Lineární řetězec / kruhsousedi v 1DΘ(p)průměr Θ(p), problém u velkých dat
Mřížka (mesh) / torus2D nebo 3D mřížkaΘ(p)průměr Θ(√p), Cray T3D, BlueGene
Hyperkrychleuzel = 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:

Definice — Parametry komunikace
  • 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 · tw

kde m je velikost zprávy. Tedy: startup penalizace + lineární přenos.

Hlavní poučka pro programátora: protože ts je velké, vyplatí se posílat málo velkých zpráv místo mnoha malých. Posláním 100 zpráv po 1 KB zaplatíš 100× ts; poslání jedné zprávy o 100 KB jen 1× ts.

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.

Deadlock: uzel A čeká na potvrzení od B; uzel B ve stejnou chvíli čeká na potvrzení od A. Oba zamrznou. Řešení: dohodnuté pořadí (sudé Send první, liché Recv první) nebo 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:

Kolektivní komunikační operace Broadcast (1 → všem) A · · · A A A A Reduce (všichni → 1, asociativní op) a b c d a⊕b⊕c⊕d · · · Scatter (rozdej kusy pole) A B C D A B C D Gather (sebrat zpět) A B C D A B C D All-to-all (transpozice) A B C D A B C D A B C D A B C D každý posílá každému jiná data
4.1Pět základních kolektivních operací MPI — broadcast, reduce, scatter, gather, all-to-all.
  1. Broadcast (one-to-all): jeden uzel rozešle identickou kopii dat všem ostatním.
  2. 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.
  3. Scatter: jeden uzel rozkrájí velké pole a každému pošle jeho kus.
  4. Gather: opak — všichni pošlou kousek jednomu, ten poskládá zpět.
  5. 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).

Zkouškový bod

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.
Otázka 05

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

Architektura GPU (CUDA Device) Device (GPU) SM 0 (Streaming Multiprocessor) jádra (CUDA cores) shared memory + registry SM 1 shared memory + registry SM 2 … N … desítky SM shared memory + registry Globální paměť GPU (VRAM / HBM) PCI-Express (úzké hrdlo) CPU + RAM (Host)
5.1Device (GPU) = N multiprocesorů (SM). Každý SM má vlastní cores, sdílenou paměť a registry. Všechny SM sdílí globální paměť. Spojení s CPU jen přes pomalou PCIe.

Vlákna, bloky, warpy, grid

Klíčová asymetrie: na CPU nemá smysl spouštět víc vláken než jader. Na GPU naopak — typicky 256 vláken na blok, i když jen 32 (warp) běží hardwarově. SM rychle přepíná mezi warpy, aby maskoval latenci paměti.

Divergence warpů

Co když warp narazí na if (cond) {A} else {B}, kde půlka vláken má cond=true?

  1. Vlákna nemohou dělat různé věci najednou.
  2. SM uspí polovinu warpu, druhá provede A.
  3. Pak naopak — první polovina provede B, druhá čeká.
  4. 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

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

  1. 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ě.
  2. Optimalizovat přístup do globální paměti — využívat coalesced access, kde to jde.
  3. Omezit divergenci vláken — minimalizovat if/else uvnitř warpu, raději věci uspořádat tak, aby v jednom warpu byla stejná větev.
  4. Zvolit vhodnou velikost bloku — typicky 128–512, mocniny dvou. Dotaz occupancy spočítá efektivitu.
  5. 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).

Zkouškový bod

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).
Otázka 06

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

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.

Stromová redukce (n = 8, součet) 3 7 2 5 1 8 4 6 10 7 9 10 17 19 36 log₂8 = 3 paralelní kroky
6.1V každé hladině polovina vláken sečte dvojici. Výsledek 36 po 3 paralelních krocích.

Složitost (n = p)

Theorem — Redukce s n procesory
TS(n) = Θ(n) TP(n) = Θ(log n) S(n) = Θ(n / log n) — zrychlení E(n) = Θ(1 / log n) — efektivita klesá k 0 C(n) = p · TP = Θ(n log n) > Θ(TS)

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:

  1. Sekvenční fáze: každý z p procesorů zredukuje sekvenčně svůj blok n/p prvků → čas Θ(n/p).
  2. Paralelní fáze: stromová redukce p mezivýsledků → čas Θ(log p).
Theorem — Nákladově optimální redukce
TP(n, p) = Θ(n/p + log p) C(n, p) = p · TP = Θ(n + p 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

Definice

Inkluzivní prefix sum — pole s₁, …, sₙ kde:

si = a₁ ⊕ a₂ ⊕ … ⊕ ai

Exkluzivní 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.

  1. 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.
  2. Paralelní fáze: z posloupnosti S₁, …, Sp napočítáme exkluzivní prefix sum Σk — paralelně.
  3. Korekce: každý procesor přičte ke svým vnitřním prefixům konstantu Σk.
Theorem
TP(n, p) = Θ(n/p + log p)

Stejné jako u nákladově optimální redukce — pro n = Ω(p log p) je algoritmus nákladově optimální.

Segmentovaný prefix sum

Definice

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á

Zkouškový bod

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.
Otázka 07

Paralelní quicksort

Paralelní quicksort pro architektury se sdílenou a distribuovanou pamětí, analýza efektivity paralelního quicksortu.

Quicksort — sekvenční verze

Definice

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í

Quicksort se sdílenou pamětí

Nesetříděná posloupnost je rozdělená mezi p vláken. Každé vlákno i drží podposloupnost.

  1. Volba pivota — jedno vlákno (např. master) vybere pivota a uloží ho do sdílené proměnné. Všichni si ho přečtou.
  2. Lokální partition — každé vlákno i rozdělí své prvky na Si (menší) a Li (větší).
  3. Přeskládání do pomocného pole — pomocí dvou sdílených ukazatelů s = 0 a l = 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).
  4. Každé vlákno zkopíruje své S do smaller a své L do larger.
  5. Pole se prohodí, vlákna si nově rozdělí prvky úměrně počtu a celý postup se opakuje.
Sdílená paměť — zarážky S a L pro paralelní partition menší (S) větší (L) s = 0 atomicAdd(|Si|) l = n−1 atomicSub(|Li|) vlákno 0: S₀, L₀ vlákno 1: S₁, L₁ vlákno 2: S₂, L₂ vlákno 3: S₃, L₃
7.1Vlákna si přes atomické operace „nakousnou" místa v pomocném poli — pivotem rozdělené prvky se napojí přesně tam, kam patří.
Bariéra škálování: 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:

  1. Pivot — vyberou se na jednom procesu a broadcastem rozešle všem ostatním.
  2. Lokální partition — každý proces i rozdělí své prvky na Si a Li.
  3. Prefix-sum pro pozice v globálním poli: si = Σj<i |Sj|,   li = Σj<i |Lj|
  4. 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.
  5. 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

Definice

Hyperquicksort — modifikace, která řeší citlivost na pivot:

  1. Každý proces si nejprve sekvenčním quicksortem setřídí lokální část.
  2. Z lokálních dat snadno najde svůj medián.
  3. Z lokálních mediánů se určí celkový medián = kvalitní pivot.
  4. 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

Theorem
TS(n) = Θ(n log n) TP(n, p) = Θ((n/p) · log n) S(n, p) = p,   E(n, p) = 1 C(n, p) = p · TP = Θ(n log n) = Θ(TS)

Algoritmus je nákladově optimální a má lineární zrychlení — pokud máme rozumný pivot.

Zkouškový bod

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).
Otázka 08

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

Definice

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

  1. Histogram (count): pro každou číslici j spočítám počet prvků s touto číslicí na aktuální pozici.
  2. Prefix-sum z countoffsety přihrádek: offset[j] = Σk<j count[k] To je pozice prvního prvku přihrádky j v pomocném poli.
  3. 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í.
  4. 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.

Problém — false sharing: dvě sousední vlákna píší do 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.

Algoritmus 3 — MSD první krok + LSD rekurzivně

Klíčové vylepšení:

  1. První krok provedu jako paralelní MSD — prvky se rozsejou do přihrádek podle nejvyšší číslice.
  2. Přihrádky se rozdělí mezi procesory — žádný prvek se už dál nestěhuje mezi vlákny.
  3. Každý procesor pak třídí jen svou přihrádku standardním LSD.
Proč MSD první krok pomáhá lokalitě: Po MSD obsahuje přihrádka jen prvky se stejnou nejvýznamnější číslicí. Přihrádky jsou menší → vejdou se do cache. Procesor pracuje uvnitř přihrádky, data jsou blízko v paměti, prefetcher to umí. Každá další iterace pracuje se stále kompaktnějšími daty. LSD naopak v každém kroku přehazuje napříč celým polem — nulová lokalita.

Naměřené výsledky (16M prvků, SGI Origin 2000)

AlgoritmusL2 cache missesInvalidaceČas
Alg. 1 — naivní9 828 9509 725 46751,29 s
Alg. 2 — bez false sharing1 534 000251 7445,76 s
Alg. 3 — MSD první + lokalita833 559311 2372,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í

  1. Každý uzel provede lokálně jednu iteraci radix sortu, v count[pid] má rozmezí přihrádek.
  2. MPI_Allgather — všechny uzly získají kompletní pole count.
  3. 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í).
  4. MPI_Alltoallv — výměna dat: každý uzel pošle své prvky správným uzlům.
LSD na distribuované paměti = problém: v každém kroku se prakticky všechna data přesunou mezi uzly. Síť je úzké hrdlo.
Ř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.
Zkouškový bod

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í na count[pid][j]).
  • Proč je MSD lepší pro paměť (spatial locality) a pro distribuovanou paměť (žádná komunikace po prvním kroku).
Otázka 09

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:

Definice — Komparátory

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

Definice

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)

Definice

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í

Theorem

Jsou-li smin a smax výsledkem bitonického rozdělení bitonické posloupnosti, pak:

  1. Obě smin i smax jsou bitonické.
  2. 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:

Bitonic sort — síť komparátorů (n = 8) a₀a₁ a₂a₃ a₄a₅ a₆a₇ F1F2 (2a+2b) F3 (3a+3b+3c) ⊕ rostoucí ⊖ klesající merge ↑
9.1Bitonic sort, n=8: F1 vytvoří bitonické dvojice (↑↓↑↓), F2 sloučí do bitonických čtveřic, F3 (tři kroky) provede finální merge. Celkem log²8 = 6 paralelních kroků.

Složitost

Theorem
TP(n) = Θ(log²n)   paralelních kroků TS(n) = Θ(n log²n)   celková práce

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

Zkouškový bod

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.
Otázka 10

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,j

Pří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,0
Tři různé procesy potřebují v jeden moment bloky B0,0, B1,0, B2,0 a podobně pro A. Vzniká kolize na komunikační síti — všichni chtějí stejná data.

Cannonův algoritmus — rotace bloků

Definice

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, j

V 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).

Cannon: rotace bloků A doleva, B nahoru P0,0 A₀₀ · B₀₀ P0,1 A₀₁ · B₁₁ P0,2 A₀₂ · B₂₂ P1,0 A₁₁ · B₁₀ P1,1 A₁₂ · B₂₁ P1,2 A₁₀ · B₀₂ P2,0 A₂₂ · B₂₀ P2,1 A₂₀ · B₀₁ P2,2 A₂₁ · B₁₂ A bloky:rotace doleva B bloky:rotace nahoru
10.1V jednom kroku Cannona každý proces násobí jinou dvojici bloků — žádné konflikty na sběrnici.

Gaussova eliminační metoda (GEM)

Definice

GEM převádí soustavu Ax = b na horní trojúhelníkovou:

Ax = b   ⟹   Ux = d

V 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

  1. V kroku k vlákno k (pivotní řádek) vydělí svůj řádek pivotem a uloží do sdílené paměti.
  2. Všechna ostatní vlákna i > k najednou a paralelně odečtou βik-násobek řádku k → jeden paralelní krok.
  3. Na GPU: vynechat sdílenou paměť pivota a každé vlákno si dělí samo — odpadne čekání.
Theorem — Složitost
TS(n) = Θ(n³) TP(n, p) = Θ(n³/p) S = p,   E = 1,   C = Θ(n³) = Θ(TS)

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

Back substitution — z horní trojúhelníkové matice musíme dopočítat x zdola nahoru. Každé xi závisí na xi+1, …, xnstriktně sekvenční. Pro paralelní zpracování katastrofa.

Ř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).

LU rozklad: klasický GEM ho produkuje (L = dolní, U = horní). Pro opakované řešení s různými b a stejnou A se hodí — L a U se spočítají jen jednou. Gauss-Jordan ale LU rozklad neprodukuje.

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).

Zkouškový bod

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í.
Otázka 11

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, …, n

kde c₁ = 0 a bn = 0. Vznikají např. při diskretizaci jednodimenzionálních PDR (1D vedení tepla, struny).

Thomasův algoritmus — sekvenční

Definice

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 = ρn

Zpětná substituce: xk = ρk − μk xk+1 od k = n−1 dolů. Složitost: Θ(n), lineární.

Thomasův algoritmus je nejrychlejší přímý řešič pro tridiagonál, ale je striktně sekvenční — každý krok závisí na předchozím. Pro paralelizaci nepoužitelný.

Cyklická redukce — paralelní trick

Definice

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:

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:

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.

Cyklická redukce — 8 neznámých krok 0 x₁ x₂ x₃ x₄ x₅ x₆ x₇ x₈ krok 1 x₂ x₄ x₆ x₈ krok 2 x₄ x₈ dopředná eliminace (paralelní) zpětná substituce (paralelní)
11.1Dopředná fáze (červeně) eliminuje liché neznámé — každý krok plně paralelní. Zpětná fáze (zeleně) dopočítává vyloučené ze sousedů — také paralelní. log₂8 = 3 kroky každá fáze.

Složitost cyklické redukce

Theorem (p = n)
TS(n) = Θ(n)   (Thomas) TP(n) = Θ(log n) C(n) = Θ(n log n)   > TS

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:

  1. V prvním kroku eliminujeme liché řádky → soustava jen pro sudé neznámé.
  2. Současně eliminujeme sudé řádky → soustava jen pro liché neznámé.
  3. Vzniknou dvě nezávislé tridiagonální soustavy poloviční velikosti.
  4. Na každou aplikujeme stejný postup rekurzivně.
  5. Po ⌊log₂ n⌋ krocích máme n/2 nezávislých 1- až 2-rovnicových soustav → vyřešíme triviálně paralelně.
Výhoda úplné cyklické redukce: všechna vlákna zůstávají aktivní po celou dobu výpočtu (žádné nudící se procesy). Doba běhu klesne na polovinu oproti jednosměrné cyklické redukci. Algoritmus stále není nákladově optimální (Θ(n log n)), ale konstanta je nejmenší.
Zkouškový bod

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.
Otázka 12

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:

Převést grafový algoritmus na maticovou operaci. Adjacenční matici grafu vynásobíme vektorem v vhodném polookruhu — výsledek je obvyklý grafový krok. Maticové násobení (SpMV) se paralelizuje výborně.

Polookruh (semiring)

Definice

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.

Polookruh01Použití
Standardní+·01klasická lin. algebra
Booleovský∨ (OR)∧ (AND)01BFS, dosažitelnost
Tropický (min-plus)min+0SSSP, APSP
Max-plusmax+−∞0nejdelší cesta, plánování

BFS — průchod do šířky

Definice

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,n

Výsledek y má 1 na pozicích uzlů, do nichž se v jednom kroku dá dostat. Opakovaným násobením AT(∧.∨) šíříme frontu.

BFS jako maticové násobení v booleovském polookruhu Graf 0 1 2 3 x (start z uzlu 0) 1 0 0 0 AT(∧.∨) x ⊕=OR, ⊙=AND y (po 1 kroku BFS) 0 1 1 1 uzly 1, 2, 3 dosaženy
12.1Z vektoru fronty (jednička u uzlu 0) jedním SpMV získáme všechny dosažitelné sousedy.
Paralelizovatelnost BFS: tak hladké to není. U hustých grafů je komunikační overhead velký. U řídkých grafů s nízkým stupněm větvení je každá fronta malá → není co paralelizovat. BFS se paralelizuje nejlépe pro grafy středního stupně. Obecně patří mezi hůře paralelizovatelné operace.

SSSP — nejkratší cesta z jednoho zdroje

Definice

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í

  1. Inicializuj: d(start) = 0, ostatní .
  2. Vyber nenavštívený uzel s minimálním d(u).
  3. Relaxuj všechny jeho sousedy.
  4. Označ navštívený, opakuj.

Funguje jen pro nezáporné váhy. Složitost O((n+m) log n) s haldou.

Dijkstra je špatně paralelizovatelný. Krok „vyber uzel s minimální d(u)" je inherentně sekvenční (priority queue). Lze paralelizovat jen relaxaci sousedů zvoleného uzlu — to ale tvoří malou část práce.

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).

Definice — Tropický polookruh

(ℝ ∪ {∞}, 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.

Proč je Bellman-Ford lepší pro paralelizaci: každá iterace je jediná SpMV operace — výborně paralelizovatelná, vhodná i pro GPU. Převedli jsme grafový problém na maticový.

Dijkstra vs. Bellman-Ford — srovnání

DijkstraBellman-Ford (alg.)
Záporné váhyneano (ne záp. cykly)
Paralelizovatelnostšpatná (sekvenční výběr minima)dobrá (SpMV)
Iteracín kroků≤ n−1 iterací
Vhodnost pro GPUnízkávysoká

Shrnutí — co se paralelizuje a vyplatí

AlgoritmusCo paralelizujemeVyplatí se?
BFSzpracování celé fronty (SpMV v Bool. polookruhu)závisí na hustotě grafu
DFSšpatně, téměř sekvenční
Dijkstrarelaxace sousedů jednoho uzlušpatně (min je sekvenční)
Bellman-Fordkaždá iterace = SpMV (tropický)dobře — vhodné pro GPU
Floyd-Warshall (APSP)vnitřní dvojsmyčka pro dané wdobře, O(n) kroků
Zkouškový bod

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.