Pokročilé architektury počítačů Přednáška 7 CUDA První paralelní aplikace Martin Milata
Obsah SIMD versus SIMT Omezení a HW implementace Způsob zpracování warp skupiny vláken CUDA - pohled programátora Připomenutí výpočetního modelu Práce s pamětí Kompilace a Runtime Optimalizace programu Důsledky výpočetního modelu Přístup do paměti
SIMD versus SIMT Single-Instruction, Multiple-Data (SIMD) Jedna instrukce je vykonávána na množině dat (obvykle vektor) stejným způsobem Neumožňuje divergenci cest při zpracování (jeden PC, jednotná sdílená sada registrů,...) Vektorové procesory (zpracování multimediálních dat), GPU Rozšíření x86 architektury: MMX, 3DNow!, SSEx, AVX, AltiVec Single-Instruction, Multiple-Thread (SIMT) Architektura navržená k vytváření, spravování, plánování a provádění skupin vláken (warp) Všechny vlákna ve warp startují na stejné adrese. V dalším provádění a případné divergenci cest jsou vzájemně nezávislé Rozdíl oproti čistě SIMD architektuře Vyžaduje přítomnost PC a oddělení sady registrů per vlákno
Single-Instruction, Multiple-Thread (SIMT) Při tvorbě programu se předpokládá nezávislost běhu všech vláken CUDA části programu Každé vlákno obsahuje stejné prováděné tělo instrukcí Kód vláken může obsahovat větvení programu a tím způsobit divergenci cest při provádění Vlákna jsou nezávislá s vlastním PC, registry,... divergence je možná Úplná nezávislost a oddělenost zpracování vláken není v CUDA architektuře z hardwarového pohledu dodržena Vlákna jsou ke zpracování předávána ve warp skupinách Instrukce warp provádí stream processor (SP) obdoba funkční jednotky Z pohledu SW jsou plně nezávislá s tím, že HW omezení ve warp jsou před programátorem skryta a řešena transparentně při vykonávání
Single-Instruction, Multiple-Thread (SIMT) Přepínání kontextů vláken Veškeré informace kontextu vlákna jsou udržována po dobu jeho běhu přímo v HW (program counter, registry, ) Přepnutí kontextu vlákna lze provést při vydávání instrukce má zanedbatelnou latenci Plánovač může warp instrukce plánovat nezávisle na jejich příslušnosti k různým warp skupinám vláken (přepne kontext). Instrukce volí na základně jejich připravenosti k provádění Omezení v rámci warp Zpracování je více podobné SIMD Warp obsahuje 32 instrukcí vydaných pro SP (stream processor) Zpracování 32 souvisejících datových polí je dané
SIMT a warp instrukce 32 datových položek je zpracováváno v rámci jedné warp instrukce SIMD přístup na úrovni HW pro jednu warp instrukci Každé vlákno disponuje vlastními zdroji definujícími jeho kontext (PC, registry, ), jeho instrukce je ale vydaná současně s 31 dalšími vlákny v rámci jedné warp instrukce Teoreticky možná divergence 32 nezávislých cest Společné vykonávání všech vláken po dobu jednotné cesty kódem programu (všechny vlákna zpracovávají v jeden okamžik stejnou instrukci) Nejefektivnější provádění warp instrukce
SIMT a warp instrukce Divergence cest v rámci warp Cesta vykonávání programu jednotlivých vláken se rozdělí (větvením programu) Warp instrukce není proveditelná najednou nutná serializace divergujících cest Střídavé omezení vykonávání vláken různých cest Snaha o minimalizaci divergence na úrovni warp instrukcí Výkonnostní dopad Koherence řídícího toku Warp vlákna jdou stejnou cestou vykonávání programu
Co je to CUDA? Compute Unified Device Architecture Umožňuje použít standardní programovací jazyk C k realizaci výpočtů na GPU Nevyžaduje znalosti standardního grafického API nebo hlubší znalost obecné problematiky programování pro/na GPU Jednoduchý start s viditelným reálným výkonnostním přínosem NVIDIA provádí návrh a vývoj potřeba NVIGIA GPU (GeForce 8xxx/Tesla/Quadro) Stabilní, dostupné (bezplatné), dokumentované a podporované řešení Podporováno jak pro Windows tak pro Linux
Proč CUDA? Jedná se prakticky o nejmodernějších API pro tzv. stream computing CUDA lze považovat za lídra v této oblasti Dostupnost CUDA enable zařízení Umožňuje využívat maximální výkon HW díky známé struktuře, výkonu a typů pamětí zpřístupnění různých typů pamětí programátorovi zaměření pro specifický HW, který je možné efektivně využívat Alternativní GPGPU programování umožňuje efektivní práci s GPU jen pomocí standardního grafického API Garantuje podporu pro budoucí verze HW
Máte CUDA enabled zařízení? Seznam není aktuální. Dnes rozsahem překračuje možnosti jedné stránky. Převzato z: http://www.nvidia.com/object/cuda_gpus.html (z roku 2010)
Výpočetní model GPU je v roli koprocesoru CPU počítače Určeno pro výpočet relativně přímočarého a masivně paralelizovatelného kódu Mnohokrát prováděné funkce nad nezávislými daty - např. tělo smyčky for Kompilát funkce (těla smyčky) pro grafický procesor se nazývá kernel Kernel se na GPU provádí jako množina vláken, které v omezené míře sdílejí alokované prostředky globální paměť, paměti konstant a textur, sdílená paměť v rámci bloku vláken CPU i GPU využívá, spravuje a přistupuje ke své paměti. Data mohou být mezi oběma prostory kopírována Přímý přístup do cizí paměti není možný Kopírování dat mezi pamětmi je časově náročná operace Vyžaduje účast sběrnice mezi CPU a GPU (obvykle PCI Express)
Grid, blok a vlákno Kernel umístěn ve výpočetním gridu Výpočetní grid se dále dělí na bloky vláken Jednotlivá vlákna provádějí operace kernel funkce Dimenze gridu a bloku je aplikačně konfigurovatelná Grid může být definován jako 1, 2 nebo 3 dimenzionální Maximální velikost je závislá na velikosti paměti a složitosti kernelu Každý blok má jednoznačný identifikátor v rámci gridu (block ID) každé vlákno má jednoznačný identifikátor v rámci bloku (thread ID)
Příklad: Sčítání matic // Kernel definition void MatAdd (float* A, float* B, float* C, int N) { for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j){ C[i][j]=A[i][j]+B[i][j]; int main() {... // Kernel invocation MatAdd(a, b, c, N); // Kernel definition global void MatAdd (float* A, float* B, float* C, int N) { int i = blockidx.x * blockdim.x + threadidx.x; int j = blockidx.y * blockdim.y + threadidx.y; if (i < N && j < N) C[i][j] = A[i][j] + B[i][j]; int main() {... // Kernel invocation dim3 threadsperblock(16, 16); dim3 numblocks(n / threadsperblock.x, N / threadsperblock.y); MatAdd<<<numBlocks, threadsperblock>>>(a,b,c);
Přístup k paměti Oddělené paměťové prostory hlavní paměť GPU paměti Explicitní alokace a dealokace GPU paměti cudamalloc() a cudafree() Ukazatelé do GPU paměti Kopírování dat mezi hlavní a GPU pamětí Pomalá operace cudamemcpy() Optimalizace práce s pamětí Minimalizace pomalých operací Použití pamětí využívající cache nebo rychlých pamětí
API Design Minimální rozšíření jazyka C Kvantifikátory funkcí a proměnných Direktiva provádění CUDA kernelu Vestavěné datové typy Runtime knihovny Komponenty pro uživatelský přístup a řízení GPU (ovládání částí programu běžící na CPU) Komponenty pro práci na GPU Společné komponenty Vestavěné datové typy, podmnožina standardních C knihoven Zdrojový kód musí být kompilován CUDA kompilátorem (nvcc)
Kompilátor NVCC CUDA kernel je obvykle uložen v souboru s příponou.cu NVCC používá standardní kompilátor k překladu kódu, který má být zpracován CPU NVCC nepodporuje mnohá rozšíření Mnoho STL jako např. iostream nelze použít Jak na velké projekty? Pomocí NVCC se provede kompilace kernelu a jeho volání z CPU (soubor.cu) Hlavičkový soubor s voláním kernelu z CPU se použije ve zdrojovém kódu
GPU Runtime Rozšíření dostupná pouze na GPU Méně přesné akcelerované matematické funkce syncthreads() pozastaví vykonávání dokud všechny vlákna v bloku nedosáhnou synchronizačního bodu Konverzní funkce s rozšířenými možnostmi zaokrouhlování Funkce konverze a reinterpretace datových typů Funkce pro práci s texturami Atomické funkce Garantují provedení read-modify-write atomické operace s atributem v paměti bez možnosti interference s jiným vláknem (operand je výlučně zamčen). Pouze zařízení revize 1.1 a vyšší
CPU Runtime Rozšíření dostupná na CPU Správa zařízení Práce s profily zařízení, správa multi-gpu prostředí,... Správa paměti cudamalloc(), cudamemcpy(), cudafree(),... Správa Textur Práce s pamětí textur z pohledu CPU,... Interoperabilita s OpenGL a DirectX Mapování globální paměti na OpenGL buffery Asynchronní souběžné provádění Řízení je vráceno na CPU dříve než GPU dokončí provádění Low-level (driver) API
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Stanovení velikosti vektoru Počet prvků vektoru (10) Nastavení počtu vláken bloku Maximální počet vláken v bloku (4) Důsledek je popis velikosti gridu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); Počet prvků / Počet vláken bloku for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Definice CUDA kernelu Realizuje paralelní výpočet druhých mocnin prvků vektoru na GPU float *a ukazatel do paměti GPU zařízení int N skutečný počet prvků argument předaný hodnotou int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Definice CUDA kernelu Realizuje paralelní výpočet druhých mocnin prvků vektoru na GPU float *a ukazatel do paměti GPU zařízení int N skutečný počet prvků argument předaný hodnotou int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Alokace a inicializace vektoru v hlavní paměti ( paměť CPU ) Vektor a_h velikosti N float hodnot je k dispozici v paměti CPU size jako celková velikost pole pro uložení N float hodnot bude použita i při následné alokaci GPU paměti int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Alokace a kopírování hodnot vektoru do paměti GPU Vektor a_d velikosti N float hodnot je alokován v paměti GPU Při kopírování pamětí je specifikován a_d cíl a a_h zdroj přenosu, size velikost kopírované oblasti a cudamemcpyhosttodevice směr přenosu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Výpočet počtu bloků vláken, do kterých bude provádění rozprostřeno Proměnná n_blocks definuje počet bloků v rámci gridu v 1D uspořádání Případný nenulový zbytek celočíselného podílu počtu prvků a počtu vláken v bloku vynutí alokaci bloku navíc (nebude plně využit) int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Volání provádění CUDA kernelu Za voláním funkce kernelu následují parametry direktivy volání n_blocks počet bloků a block_size jejich velikost (počet vláken v bloku) Vektor a_d je předán jako ukazatel do paměti GPU, následuje konstantní N int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Kopírování hodnoty vypočtem modifikovaného vektoru zpět do paměťového prostoru CPU Při kopírování pamětí je opět specifikován a_h cíl a a_d zdroj přenosu, size velikost kopírované oblasti a cudamemcpydevicetohost směr přenosu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Zobrazení výsledku int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Úklid dynamicky alokovaných pamětí před ukončením provádění programu Uvolnění alokované paměti v prostoru GPU pomocí cudafree(a_d) Uvolnění paměti v prostoru CPU pomocí free(a_h) int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); for (int i=0; i<n; i++) { a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); for (int i=0; i<n; i++) { printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Vykonávání vláken Struktura GPU Obsahuje N multiprocesorů Každý multiprocesor obsahuje M skalárních procesorů Každý multiprocesor zpracovává skupiny bloků vláken Blok vláken může běžet jen na jednom multiprocesoru Bloky jsou rozděleny do skupin vláken tzv. warp Warp je prováděn paralelně V současné době obsahuje 32 vláken Vlákna jsou ve warp řazena pokud možno se sekvenčně vzrůstajícím threadid Plánovač přepíná provádění mezi warp instrukcemi
Důsledky výpočetního modelu Optimalizace rozměrů gridů a bloků Počet bloku / Počet multiprocesorů > 1 Všechny multiprocesory mohou být využity Počet vláken v bloku < 32 Warp instrukce nemá potenciálně dost vláken Počet bloku / Počet multiprocesorů > 2 Více bloků bude prováděno na multiprocesoru současně Zdroje per blok celkově dostupné zdroje Sdílená paměť a registry Bloky mohou být na multiprocesorech prováděny souběžně Předcházení divergence cest při zpracování skoků v rámci warp Pokles výkonu serializací divergujících cest Blok se skupinami N*32 vláken s nedivergujícími cestami
Přístup do paměti Přístup k rychlým pamětem registry, sdílená paměť, přístup ke stejné adrese paměti konstant (používá cache) Přístup do paměti textur Používá cache pro akceleraci přístupu Optimalizována pro 2D přístup Princip 2D lokality a prostorová cache Možnost efektivního čtení sousedních adres Není potřeba řešit slučování přístupů do paměti Globální paměť 4 cykly potřebné pro zpracování instrukce přístupu do paměti 400-800 cyklů následná latence paměti (čas pro vyřízení požadavku) Snižování počtu samostatných požadavků pomocí techniky jejich slučování
Akcelerace přístupu do globální paměti Akcelerace založená na technice slučování samostatných požadavků jednotlivých vláken warp Realizuje se pouze v rámci warp Vyžaduje přístup k souvislé a zarovnané oblasti v paměti 128 bytes float nebo int 256 bytes float2 neho int2 512 bytes float4 nebo int4 Warp base address (WBA) musí být násobkem 16*sizeof(type) MMU obsluhuje tzv. Half warp Vektory o N prvcích k-té vlákno může přistupovat pouze k prvku na adrese WBA + k Všechny vlákna se nemusí účastnit přístupu do paměti Omezení platí jak pro čtení tak pro zápis
Slučitelné přístupy do paměti všechna vlákna přistupují k prvkům podle vzoru k-té vlákno k-tý prvek všechny vlákna warp se přístupu do paměti neúčastní vzorec k-té vlákno k tý prvek je zachován
Neslučitelné přístupy do paměti Při přístupu není dodržen vzor k-té vlákno k-tý prvek Sloučení není možné kvůli nezarovnané bázové adrese Nezarovnaná velikost prvků float3 nebo int3
Slučování přístupu do paměti revize architektury 1.2 a vyšší Pokročilejší MMU umožňuje zmírnit požadavky na sloučení přístupu do paměti Sloučení je nyní možné pro jakýkoliv vzor přístupu do paměti, který lze realizovat v rámci bloku velikosti: 32 B 8 bit slovo 64 B 16 bit slovo 128 B 32 bit nebo 64 bit slovo Použití menších transakcí pro předcházení plýtvání pásma vlivem přenosu nepoužívaných slov
HW mírnění dopadu neslučitelných přístupů do globální paměti V zařízeních CUDE revize 2.0 a vyšších je nahrazena grafická cache paměť tzv. pravou hierarchií cache pamětí Hierarchie cache pamětí srovnatelná s hierarchií CPU Adresované bloku jsou ukládány v L1 a L2 cache Latence globální paměti mírněná latenci L1 resp. L2 cache Stále užitečné slučování požadavků na přístup do globální paměti Mnoho zařízení starší revize (< 2.0) Průměrná latence při náhodném přístupu je stále horší než při optimalizaci a slučování požadavků
Kam s načtenými daty Data z sloučeného požadavku na přístup do globální paměti mohou být uložena v malém alokovaném prostoru ve sdílené paměti ve sdílené paměti dále transformována a přeskládána do potřebné podoby pro další zpracování Sloučené načtení bloku a jeho následná úprava ve sdílené paměti je rychlejší než separované čtení prvků z globální paměti (např. transponování matic) Omezená velikost rychlých pamětí vyžaduje promyšlený přístup k práci s pamětí Nutnost rozdělovat počáteční velkou kolekci dat do malých oblastí, s následným výpočtem nad ní (submatice).
Proč CUDA a ne OpenGL/DirectX Výhody Není potřeba zvládnout API grafické karty Snazší práce s pamětí Dostupná dokumentace Nevýhody Specializace na zařízení (grafické čipy) jednoho výrobce CUDA není schopna využít absolutně všechnu výpočetní sílu čipu
Závěr SMID není SMIT HW architektura SMIT modelu CUDA a programování CUDA API První paralelní aplikace Základní optimalizace Velikosti gridu a bloku Práce s pamětí
Literatura D. Kanter: NVIDIA's GT200: Inside a Papallel Processor NVIDIA CUDA C Programming Guide Johan Seland: CUDA Programming Paul H. J. Kelly, Advanced Computer Architecture Lecture notes 332 P. N. Glaskowsky: NVIDIA s Fermi: The First Complete GPU Computing Architecture Internetové zdroje: http://www.nvidia.com/ http://gpgpu.org/