Differences

This shows you the differences between two versions of the page.

Link to this comparison view

asc:laboratoare:05 [2020/02/04 00:05]
127.0.0.1 external edit
asc:laboratoare:05 [2024/04/08 13:48] (current)
emil.slusanschi [Referinte]
Line 1: Line 1:
-=Tehnici de Optimizare de Cod – Inmultirea Matricelor=+====== Laboratorul 05 - Arhitectura GPU NVIDIA CUDA ======
  
-==Obiective==+Pentru o înțelegere profundă a arhitecturii CUDA vă recomandăm să citiți din resursele bilbiografie "​Graphics and Computing GPUs". În continuare o să discutăm mai aplicat despre implementări ale acestei arhitecturi.
  
-In acest laborator vom exemplifica o serie de optimizari de cod pe una dintre cele mai simplesi in acelasi timp utilizate problemesi anume, inmultirea matricelor+Arhitectura NVIDIA FERMI [[https://​www.nvidia.com/​content/​PDF/​fermi_white_papers/​NVIDIA_Fermi_Compute_Architecture_Whitepaper.pdf|aici]]Tesla 2070coada executie fep8.grid.pub.ro -> dp
  
-===De ce inmultirea matricelor?​===+Arhitectura NVIDIA KEPLER [[https://​www.nvidia.com/​content/​PDF/​kepler/​NVIDIA-Kepler-GK110-Architecture-Whitepaper.pdf|aici]],​ Tesla K40M, coada executie fep8.grid.pub.ro -> hpsl
  
-Este o operatie fundamentala si elementara in algebra liniara ce serveste la rezolvarea unui numar extrem de mare de problemecum ar fi: rezolvarea sistemelor liniare de ecuatii ​in majoritatea domeniilor stiintifice si economice (operatiile cu matrice sunt practic prezente pretudindeni); calcule si operatii cu grafuri; inversari de matriceProblema inmultirii matricelor este in mod cert cea mai bine studiata problema in HPC (High Performance Computing), ea beneficiind ​de o multitudine ​de algoritmi inteligenti ​si implementari performante pe toate arhitecturile existente astaziPentru a simplifica lucrurile, in acest laborator ne vom ocupa doar de inmultirea matricelor patratice.+Prima arhitectura NVIDIA complet programabila a fost G80 (ex. [[http://​www.nvidia.com/​page/​8800_tech_briefs.html|Geforce 8800]]lansat ​in anul 2006). Cu aceasta arhitectura s-a trecut ​de la unitati hardware fixe vertex/​pixel la cele de unified shader care puteau procesa atat vertex/​pixel cat si geometryEvolutia arhitecturilor GPU de la NVIDIA este detaliata [[http://​s08.idav.ucdavis.edu/​luebke-nvidia-gpu-architecture.pdf|aici]].
  
-==Cel mai simplu algoritm==+Implementarea NVIDIA pentru GPGPU se numeste CUDA (Compute Unified Device Architecture) si permite utilizarea limbajului C pentru programarea pe GPU-urile proprii. Lista de GPU-uri ce suporta API-ul CUDA sau OpenCL se regaseste pe site-ul oficial [[https://​www.geforce.com/​hardware/​technology/​cuda/​supported-gpus|aici]] sau pe wiki [[https://​en.wikipedia.org/​wiki/​List_of_Nvidia_graphics_processing_units|aici]]. Fiecare noua arhitectura are un codename (ex Fermi, Pascal) si este reprezentata de un "​compute capability"​ (list [[https://​developer.nvidia.com/​cuda-gpus|aici]]). Cu cat arhitectura este mai noua, cu atat sunt suportate mai multe facilitati din API-urile CUDA si OpenCL.
  
-Intuitivcel mai simplu algoritm, urmeaza formularea matematica:​ +Unitatea GPU este potrivita pentru paralelismul de date SIMD (Single Instruction Multiple Data)astfel aceleasi instructiuni sunt executate in paralel pe mai multe unitati de procesareDatorita faptului ​ca acelasi program este executat pentru fiecare element ​de datesunt necesare mai putine elemente pentru controlul fluxuluiSi deoarece calculele sunt intensive computationallatenta accesului la memorie poate fi ascunsa prin calcule in locul unor cache-uri mari pentru date.
-{{:​asc:​lab5:​cij.jpg|}} +
-  +
-Matricele A = [aij], i,​j=1,​...,​N ​ si B = [bij], i,j=1,...,N sunt salvate ​ca vectori bidimensionali ​de marime N x N. Matricea rezultat C = A x B = [cij], i,j=1,...,Navand fireste aceeasi dimensiune.+
  
-{{:​asc:​lab5:​axb_c.jpg|}}+Motivul discrepantei intre performanta paralela dintre CPU si GPU este faptul ca GPU sunt specializate pentru procesare masiv paralela si intensiva computational (descrierea perfecta a taskurilor de randare grafica) si construite in asa fel incat majoritatea tranzistorilor de pe chip se ocupa de procesarea datelor in loc de cachingul datelor si controlul fluxului executiei
  
-Cum este si de asteptatsimilar cu majoritatea operatiilor din algebra liniaraformula ​de mai sus se transforma in urmatorul program extrem de simplu: ​+La GPU-urile NVIDIA, un Streaming Processor (SP) este un microprocesor cu executie secventiala,​ ce contine un pipeline, unitati aritmetico-logice (ALU) si de calcul in virgula mobila (FPU). Nu are un cachefiind bun doar la executia multor operatii matematice. Un singur SP nu are performante remarcabileinsa prin cresterea numarului ​de unitati, ​se pot rula algoritmi ce se preteaza paralelizarii masive.
  
-<code cpp> +SP impreuna cu Special Function Units (SFUsunt incapsulate intr-un Streaming Multiprocessor ​(SM/SMX). Fiecare SFU contine unitati pentru inmultire in virgula mobila, utilizate pentru operatii transcendente ​(sin, cossi interpolare. MT se ocupa cu trimiterea instructiunilor pentru executie la SP si SFU.
-int i,j,k; +
-double a[N][N], b[N][N], c[N][N]; +
-// initializarea matricelor a si b +
-for (i=0;​i<​N;​i++)+
-   ​for ​(j=0;​j<​N;​j++)+
-      c[i][j] = 0.0; +
-      for (k=0;​k<​N;​k++)+
- c[i][j] += a[i][k] * b[k][j]; +
-      } +
-   } +
-+
-</​code>​+
  
-<note important> ​+Pe langa acestea, exista si un cache (de dimensiuni reduse) pentru instructiuni,​ unul pentru date precum si memorie shared, partajata de SP-uri. Urmatorul nivel de incapsulare este Texture / Processor Cluster (TPC). Acesta contine SM-uri, logica de control si un bloc de handling pentru texturi. Acest bloc se ocupa de modul de adresare al texturilor, logica de filtrare a acestora precum si un cache pentru texturi.
  
-**Cat de bun este acest algoritm?**+{{:​asc:​lab11:​cuda-arch.png?direct&​720|}}
  
-Algoritmul ​este bun pentru ca:  +Filosofia din spatele arhitecturii ​este permiterea rularii unui numar foarte mare de threaduri. Acest lucru este facut posibil prin paralelismul existent la nivel hardware.
-* Se poate specifica in doar cateva linii;  +
-* Este o mapare directa a formulei ​de calcul pentru Cij (din algebra liniara); ​este usor de inteles si de urmarit de catre oricine poseda cunostinte minime de matematica;​ +
-* In sfarsit, in mod sigur nu contine bug-uri datorita simplitatii extreme pe care o manifesta algoritmul!+
  
-Algoritmul este prost pentru ca+Documentatia NVIDIA recomanda rularea unui numar cat mai mare threaduri ​pentru ​a executa un task. Arhitectura CUDA de exemplu suporta zeci de mii de threaduri, numarul acestora fiind mult mai mare decat unitatile fizice existente pe chip. Acest lucru se datoreaza faptului ​ca un numar mare de threaduri poate masca latenta accesului la memorie.
-* Are performante extrem ​de reduse!+
  
-De aceea ne vom ocupa in acest laborator de optimizarea acestei operatii ​din punctul ​de vedere al performantei.+Urmarind acelasi model modular ​din punct de vedere al arhitecturii,​ threadurile sunt incapsulate in blocuri (thread blocks / warps), iar blocurile in grile (thread grid). Fiecare thread este identificat prin indexul threadului in bloc, indexul blocului in grila si indexul grilei. Indexurile threadurilor si ale blocurilor pot fi uni/​bi/​tri-dimensionale,​ iar  indexul grilei poate fi uni sau bi-dimensional. Acest tip de impartire are rolul de a usura programarea pentru probleme ce utilizeaza structuri de date cu mai multe dimensiuni. Se poate observa ca thread-urile dintr-un thread block trebuie sa execute cat mai multe instructiuni identice spre a nu irosi resurse.
  
-</​note>​  +{{:​asc:​lab11:​thread.blocks.jpg?​direct&​360|{{thread.blocks.jpg|''​Structura threadurilor in blocuri''​}}
-==Optimizarea algoritmului de inmultire a doua matrice==+
  
-===Detectarea constantelor din bucle===+Threadurile dintr-un bloc pot coopera prin partajarea de date prin intermediul memoriei shared si prin sincronizarea executiei. Functia de bariera functioneaza doar pentru threadurile dintr-un bloc. Sincronizarea nu este posibila la alt nivel (intre blocuri/​grila etc.). Mai multe explicatii se regasesc in [[https://​ocw.cs.pub.ro/​courses/​asc/​laboratoare/​06|Laboratorul 6]].
  
-Prima optimizare, consta in a observa ca c[i][j] este o constanta in cadrul ciclului interior k. Totusi, pentru un compilator acest fapt nu este neaparat evident deoarece c[i][j] este o referinta in cadrul unui vector. Astfel, o prima optimizare va arata asa:+===== Ierarhia de memorie =====
  
-<code cpp> +Intelegerea ierharhiei de memorie este esentiala in programarea eficienta a unitatii GPU. Capacitatea mare de executie in paralel a unui GPU necesita ascunderea latentei de acces catre memoria principala ​(fie VRAM pentru dGPU sau RAM pentru iGPU).
-for (i=0;​i<​N;​i++)+
-   for (j=0;​j<​N;​j++){ +
-      register double suma = 0.0; +
-      for (k=0;​k<​N;​k++) { +
-         suma += a[i][k] * b[k][j]; +
-      } +
-      c[i][j] = suma; +
-   } +
-+
-</​code>​+
  
-In acest mod, compilatorul va putea avea grija ca variabila suma sa fie tinut intr-un registru, permitand astfel o utilizare optima a acestei resurseAstfel utilizarea keyword-ului "​register"​ este util de folosit ca hint pentru compilator, atunci cand socotiti ca acest lucru este util.+{{:​asc:​lab11:​mem.hierarchy.png?​direct|Ierarhia memoriei in CUDA}}
  
-===Accesul la vectori=== +**Register File** 
-Un alt aspect care necesita resurse din plin, este utilizarea ​si accesul variabilelor ​de tip vectorial. De fiecare data cand programul face o referinta la un obiect de tipul X[i][j][k] compilatorul trebuie sa genereze expresii aritmetice complexe, pentru a calcula aceasta adresa, in cadrul vectorului muldimensional X. De exemplu, iata cum arata un vector bidimensional in limbajul C (salvat row-major):+<code sh> 
 +/* marcam pentru compilator regValPi in register file */ 
 +__private float regValPi ​3.14f; 
 +/* compilatorul cel mai probabil oricum incadreaza regVal2Pi ca registru */ 
 +float regVal2Pi ​2 * 3.14f; 
 +</​code>​ 
 +  *Cea mai rapida forma de memorie de pe GPU 
 +  *Accesibila doar de catre threadiar durata de viata este aceeasi ca si a threadului 
 +  *Un kernel complex poate determina folosirea unui numar mare de registrii si astfel: 
 +    * limitarea executiei multor thread-uri simultan 
 +    * register spillatunci cand valorile registrilor sunt salvate ​in memoria globala
  
-{{:​asc:​lab5:​aij.jpg|}}+**Local Memory** 
 +<code sh> 
 +/* fiecare work item salveaza un element */ 
 +__local float lArray[lid] = data[gid];​ 
 +</​code>​ 
 +  *In functie de implementarea hardware, 100GB/sec -> 2TB/sec 
 +  *Pentru GPU o memorie rapida, actioneaza ca un cache L1/alt register file, la CPU de regula este doar o portiune din RAM 
 +  *Ca si in cazul registrilor,​ este accesibila doar de catre thread, iar durata de viata este aceeasi ca si a threadului
  
-Astfel, pentru N = 6, M = 4: a[2][3] = a[0][0] + 2*6 + 3 = a[0][0] + 15+**Shared Memory** 
 +<code sh> 
 +/* elementele sunt salvate la nivel de bloc */ 
 +__shared__ int s[64]
 +</​code>​ 
 +  *Accesibila tuturor threadurilor dintr-un bloc (warp/​wavefront),​ iar durata de viata este aceeasi ca si blocului 
 +  ​*Trebuie evitate conflictele de access (bank conflicts)
  
-In limbaje ​de programare ca FORTRAN-ulformula este inversatadeoarece aceste limbaje salvează vectorii în format column-major:​+**Constant Memory** 
 +<code sh> 
 +__const float pi = 3.14f 
 +</​code>​ 
 +  * In functie ​de implementarea hardware, 100GB/​sec ​-> 1TB/sec 
 +  * In general performanta foarte buna(cache L1/L2zona dedicata),​ 
 +  * Are durata de viata a aplicatiei kernel
  
-a[i][j] = a[0][0] + j*M + i+**Global Memory** 
 +<code sh> 
 +__kernel void process(__global float* data){ ... } 
 +</​code>​ 
 +  * In functie de implementarea hardware, 30GB/sec -> 500GB/sec 
 +  * Video RAM (VRAM), de regula cu o capacitate intre 1GB si 12GB in functie de placa video 
 +  * Memorie dedicata specializata doar pentru placile grafice discrete (GPU-urile integrate in CPU folosesc RAM) 
 +  * In general latime mare de banda (256-512 biti) si chipuri de memorii de mare viteza (GDDR5)
  
-Oricare ar fi asezarea vectorilor in memorieaccesele la vectori sunt scumpe ​din punctul ​de vedere al performantelor. Noi vom considera ​de aici inainte o asezare row-major, ca in limbajul C. Conform acestei formule, pentru vectori bidimensionali ​(matrice), fiecare acces presupune doua adunari ​si o inmultire (de numere intregi). Evident, pentru vectori cu mai multe dimensiuni, aceste costuri cresc considerabil. Astfel, in momentul in care compilatorul intalneste instructiunea:​+**Host Memory (RAM)** 
 +  * In general4GB/sec -> 30GB/sec 
 +  * Pentru acces din kernel trebuie transfer/​mapare explicita RAM->​VRAM pe partea ​de host/CPU 
 +  * Memoria RAM accesibila direct ​de CPU si indirect de GPU via DMA si magistrala PCIe 
 +  * Viteza de transfer ​(throughput/​latentaeste limitata de magistrala PCIe cat si de memoria RAM
  
-suma += a[i][k] * b[k][j]+Caracteristici GPU K40m (coada hpsl), via query device properties CUDA
  
-se vor efectua implicitsuplimentar inmultirii si adunarii in virgula mobila implicata de codul de mai suspatru adunari si doua inmultiri in numere intregi pentru ​calcula adresele necesare din vectorii ​si b. Se intampla astfel destul de frecvent ca procesorul sa nu aiba date disponibile pentru a lucra in continuudin cauza faptului ca overhead-ul pentru calculul adreselor este semnificativ.+<code sh> 
 +Device 0: "Tesla K40m"​ 
 +  CUDA Driver Version / Runtime Version ​         9.1 / 9.1 
 +  CUDA Capability Major/Minor version number: ​   3.5 
 +  Total amount of global memory: ​                11441 MBytes (11996954624 bytes) 
 +  (15) Multiprocessors(192) CUDA Cores/​MP: ​    2880 CUDA Cores 
 +  GPU Max Clock rate:                            745 MHz (0.75 GHz) 
 +  Memory Clock rate:                             3004 Mhz 
 +  Memory Bus Width: ​                             384-bit 
 +  L2 Cache Size:                                 ​1572864 bytes 
 +  Maximum Texture Dimension Size (x,y,z)         ​1D=(65536),​ 2D=(65536, 65536), 3D=(4096, 4096, 4096) 
 +  Maximum Layered 1D Texture Size, (num) layers ​ 1D=(16384), 2048 layers 
 +  Maximum Layered 2D Texture Size, (num) layers ​ 2D=(16384, 16384), 2048 layers 
 +  Total amount of constant memory: ​              65536 bytes 
 +  Total amount of shared memory per block: ​      49152 bytes 
 +  Total number of registers available per block: 65536 
 +  Warp size:                                     32 
 +  Maximum number of threads per multiprocessor: ​ 2048 
 +  Maximum number of threads per block: ​          ​1024 
 +  Max dimension size of thread block (x,y,z): (1024, 1024, 64) 
 +  Max dimension size of grid size    (x,y,z): (2147483647,​ 65535, 65535) 
 +  Maximum memory pitch: ​                         2147483647 bytes 
 +  Texture alignment: ​                            512 bytes 
 +  Concurrent copy and kernel execution: ​         Yes with 2 copy engine(s) 
 +  Run time limit on kernels: ​                    No 
 +  Integrated GPU sharing Host Memory: ​           No 
 +  Support host page-locked memory mapping: ​      Yes 
 +  Alignment requirement for Surfaces: ​           Yes 
 +  Device has ECC support: ​                       Enabled 
 +  Device supports Unified Addressing (UVA): ​     Yes 
 +  Device PCI Domain ID / Bus ID / location ID:   0 / 8 / 0 
 +</​code>​
  
-Astfelun mod de a spori viteza programului este renuntarea la accesele vectoriale prin derefentiere utilizand in acest scop pointeri. De exemplu: ​+Caracteristici GPU M2070 (coada dp)via query device properties CUDA
  
-<​code ​cpp+<​code ​sh
-for (j=0;​j<​N;​j+++Device 0: "Tesla M2070"​ 
-   a[i][j] ​= 2;         // 2*N adunari si N inmultiri+  CUDA Driver Version / Runtime Version ​         9.1 / 9.1 
 +  CUDA Capability Major/Minor version number: ​   2.0 
 +  Total amount of global memory: ​                5302 MBytes ​(5559156736 bytes) 
 +  (14) Multiprocessors,​ ( 32) CUDA Cores/​MP: ​    448 CUDA Cores 
 +  GPU Max Clock rate:                            1147 MHz (1.15 GHz) 
 +  Memory Clock rate:                             1566 Mhz 
 +  Memory Bus Width: ​                             384-bit 
 +  L2 Cache Size:                                 ​786432 bytes 
 +  Maximum Texture Dimension Size (x,​y,​z) ​        1D=(65536), 2D=(65536, 65535), 3D=(2048, 2048, 2048
 +  ​Maximum Layered 1D Texture Size, (num) layers ​ 1D=(16384), 2048 layers 
 +  Maximum Layered 2D Texture Size, (num) layers ​ 2D=(16384, 16384), 2048 layers 
 +  Total amount of constant memory: ​              65536 bytes 
 +  Total amount of shared memory per block: ​      49152 bytes 
 +  Total number of registers available per block: 32768 
 +  Warp size:                                     32 
 +  Maximum number of threads per multiprocessor: ​ 1536 
 +  Maximum number of threads per block: ​          ​1024 
 +  Max dimension size of a thread block (x,y,z): (1024, 1024, 64) 
 +  Max dimension size of a grid size    (x,y,z): (65535, 65535, 65535) 
 +  Maximum memory pitch: ​                         2147483647 bytes 
 +  Texture alignment: ​                            512 bytes 
 +  Concurrent copy and kernel execution: ​         Yes with copy engine(s) 
 +  Run time limit on kernels: ​                    No 
 +  Integrated GPU sharing Host Memory: ​           No 
 +  Support host page-locked memory mapping: ​      Yes 
 +  Alignment requirement for Surfaces: ​           Yes 
 +  Device has ECC support: ​                       Enabled 
 +  Device supports Unified Addressing (UVA): ​     Yes 
 +  Device PCI Domain ID Bus ID location ID:   0 / 20 / 0
 </​code>​ </​code>​
  
-se va inlocui cu:+Caracteristici GPU P100 (coada xl), via query device properties CUDA
  
-<​code ​cpp+<​code ​sh
-double *ptr=&​(a[i][0]); /2 adunari si o inmultire +Device 1: "Tesla P100-PCIE-16GB"​ 
-for (j=0;​j<​N;​j++{ +  CUDA Driver Version / Runtime Version ​         12.2 / 11.4 
-   *ptr = 2                +  CUDA Capability Major/Minor version number: ​   6.0 
-   ptr++; ​              // N adunari in numere intregi +  Total amount of global memory: ​                16276 MBytes (17066885120 bytes) 
-}+  (056) Multiprocessors,​ (064) CUDA Cores/MP:    3584 CUDA Cores 
 +  GPU Max Clock rate:                            1329 MHz (1.33 GHz) 
 +  Memory Clock rate:                             715 Mhz 
 +  Memory Bus Width: ​                             4096-bit 
 +  L2 Cache Size:                                 ​4194304 bytes 
 +  Maximum Texture Dimension Size (x,​y,​z) ​        1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384
 +  ​Maximum Layered 1D Texture Size, (num) layers ​ 1D=(32768), 2048 layers 
 +  Maximum Layered 2D Texture Size, (num) layers ​ 2D=(32768, 32768), 2048 layers 
 +  Total amount of constant memory: ​              65536 bytes 
 +  Total amount of shared memory per block: ​      49152 bytes 
 +  Total shared memory per multiprocessor: ​       65536 bytes 
 +  Total number of registers available per block: 65536 
 +  Warp size:                                     32 
 +  Maximum number of threads per multiprocessor: ​ 2048 
 +  Maximum number of threads per block: ​          ​1024 
 +  Max dimension size of a thread block (x,y,z): (1024, 1024, 64) 
 +  Max dimension size of a grid size    (x,y,z): (2147483647,​ 65535, 65535) 
 +  Maximum memory pitch: ​                         2147483647 bytes 
 +  Texture alignment: ​                            512 bytes 
 +  Concurrent copy and kernel execution: ​         Yes with copy engine(s) 
 +  Run time limit on kernels: ​                    No 
 +  Integrated GPU sharing Host Memory: ​           No 
 +  Support host page-locked memory mapping: ​      Yes 
 +  Alignment requirement for Surfaces: ​           Yes 
 +  Device has ECC support: ​                       Enabled 
 +  Device supports Unified Addressing (UVA): ​     Yes 
 +  Device supports Managed Memory: ​               Yes 
 +  ​Device supports Compute Preemption: ​           Yes 
 +  Supports Cooperative Kernel Launch: ​           Yes 
 +  Supports MultiDevice Co-op Kernel Launch: ​     Yes 
 +  Device PCI Domain ID / Bus ID / location ID:   ​142 0
 </​code>​ </​code>​
  
-In mod similar se procedeaza si pentru cazul in care indexul incrementat este cel al liniilor si nu cel al coloanelor. In ambele cazuri, practic se va calcula "de mana" adresa in cadrul vectorului, exact in modul in care ar face-o compilatorul limbajului folosit. Totusi, rezolvarea noastra este mai rapida, deoarece ea tine cont de pozitia in care ne aflam in cadrul vectorului, lucru destul de complicat de facut automat. De exemplu, pentru a trece la urmatoarea coloana, e suficient sa adunam N pointer-ului,​ fata de recalcularea pornind de la @(a[0][0]ce necesita doua inmultiri si o adunare in intregi. Evident, facilitatile oferite de limbaje ca C-ul, ne vin in ajutor: astfel incrementarile de pointeri de tip char * vor face incrementarea cu un byte, in vreme ce pentru int * se va face cu patru bytes. Ca urmare a aspectelor prezentate mai susiata forma optimizata in care ajunge algoritmul nostru:+Caracteristici GPU A100 (coada ucsx), via query device properties CUDA
  
-<​code ​cpp+<​code ​sh
-for(i = 0; i < N; i++){ +Device ​0: "​NVIDIA A100-PCIE-40GB"​ 
-  ​double *orig_pa = &a[i][0]; +  ​CUDA Driver Version / Runtime Version ​         12.4 / 11.4 
-  ​for(j = 0; j < N; j++){ +  CUDA Capability Major/Minor version number: ​   8.
-    ​double *pa orig_pa; +  ​Total amount of global memory: ​                40326 MBytes ​(42285268992 bytes
-    ​double *pb &​b[0][j];​ +  (108) Multiprocessors,​ (064) CUDA Cores/​MP: ​   6912 CUDA Cores 
-    ​register double suma 0; +  GPU Max Clock rate:                            1410 MHz (1.41 GHz) 
-    ​for(k = 0; k < N; k++){ +  Memory Clock rate:                             1215 Mhz 
-      suma += *pa * *pb; +  Memory Bus Width: ​                             5120-bit 
-      pa++; +  L2 Cache Size:                                 ​41943040 bytes 
-      ​pb += N; +  Maximum Texture Dimension Size (x,​y,​z) ​        1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384) 
-    } +  ​Maximum Layered 1D Texture Size, (num) layers ​ 1D=(32768), 2048 layers 
-    ​c[i][j] = suma; +  ​Maximum Layered 2D Texture Size, (num) layers ​ 2D=(32768, 32768), 2048 layers 
-  ​} +  Total amount of constant memory: ​              65536 bytes 
-}+  Total amount of shared memory per block: ​      49152 bytes 
 +  Total shared memory per multiprocessor: ​       167936 bytes 
 +  Total number of registers available per block: 65536 
 +  Warp size:                                     32 
 +  Maximum number of threads per multiprocessor: ​ 2048 
 +  Maximum number of threads per block: ​          ​1024 
 +  Max dimension size of a thread block (x,y,z): (1024, 1024, 64) 
 +  Max dimension size of a grid size    (x,y,z): (2147483647,​ 65535, 65535
 +  ​Maximum memory pitch: ​                         2147483647 bytes 
 +  ​Texture alignment: ​                            512 bytes 
 +  ​Concurrent copy and kernel execution: ​         Yes with 3 copy engine(s) 
 +  Run time limit on kernels: ​                    No 
 +  Integrated GPU sharing Host Memory: ​           No 
 +  Support host page-locked memory mapping: ​      Yes 
 +  Alignment requirement for Surfaces: ​           Yes 
 +  Device has ECC support: ​                       Enabled 
 +  Device supports Unified Addressing (UVA): ​     Yes 
 +  ​Device supports Managed Memory: ​               Yes 
 +  ​Device supports Compute Preemption: ​           Yes 
 +  ​Supports Cooperative Kernel Launch: ​           Yes 
 +  ​Supports MultiDevice Co-op Kernel Launch: ​     Yes 
 +  Device PCI Domain ID / Bus ID / location ID:   0 / 49 / 0
 </​code>​ </​code>​
  
-<note tip>​Atentie! Codul de mai sus va da rezultate corecte doar daca matricile sunt declarate global sau pe stivă pentru că în felul acesta sunt stocate continuu în memorie (și are sens pb +N). Dacă alocați dinamic, atunci folosiți matrici liniarizate și adaptați acest cod pentru cazul lor.</​note>​ +===== Optimizarea ​accesului ​la memorie =====
- +
-<note important>​ +
-Din primele doua optimizari se pot desprinde cateva concluzii. Prima ar fi ca optimizarea unui cod (din punct de vedere al performantelor),​ presupune utilizarea a cat mai putine constructii complexe (high-level),​ puse la dispozitie de limbajul folosit. Aceasta concluzie poate suna extrem de ciudat pentru cineva care porneste de la ideea ca facilitatile limbajelor de programare sunt acolo pentru a fi folosite. Da, este adevarat acest lucru, insa atunci cand vrei performanta,​ trebuie sa stii ce constructii sa eviti! Astfel, apare concluzia a doua: vectorii sunt concepte mai abstracte decat pointerii (ca implementare),​ asadar, utilizati pointeri cand vreti viteza. Viteza crescuta insa, va fi obtinuta cu pretul unui cod mult mai dificil de urmarit si de inteles, mai rau, mult mai greu de debug-at. Un cod complex si performant, de multe ori poate contine bug-uri extrem de subtile si greu de depistat. Asadar, e util sa stii exact ceea ce faci cand incepi sa faci astfel de optimizari! +
- +
-</​note>​ +
- +
----- +
- +
-===Activitate practica - Optimizare constantelor si al accesului la vectori === +
- +
-Intrebarea este acum: aduc ceva imbunatatiri optimizarile 1 si 2? Pentru a afla raspunsul la aceasta intrebare, va invitam sa implementati problema, cu optimizarile sugerate, si sa observati singuri ce se intampla. +
- +
----- +
- +
-===Optimizarea ​pentru accesul ​la memorie=== +
- +
-Dupa cum ar trebui sa va fie destul de evident pana acum, din experienta voastra de programatori,​ memoria este in general cel mai problematic bottleneck. Optimizarile prezentate mai sus reduc timpul de executie intr-o oarecare masura, insa ele nu schimba in nici un fel modul in care memoria este accesata in cadrul algoritmului. Cu alte cuvinte, aceleasi locatii de memorie sunt accesate in aceeasi ordine, indiferent daca am operat sau nu optimizarile prezentate. O intrebare interesanta ar fi acum: ce se intampla, daca am schimba ordinea in care se executa buclele? S-ar obtine performante diferite? +
-Pentru problema noastra, care contine trei bucle, exista asadar sase secvente posibile, si anume: i-j-k, i-k-j, j-i-k, j-k-i, k-i-j, si k-j-i. Fiecare dintre aceste secvente corespunde unui tip diferit de acces la memorie pentru matricele considerate. Deoarece bucla interioara este cea mai des executata, ne vom concentra acum atentia un pic asupra ei. Operatia executata acolo ramane: +
- +
-c[i][j] +a[i][k] * b[k][j] +
- +
-Pentru fiecare dintre cele trei matrice, a, b si c, fiecare element poate fi accesat in trei moduri diferite, si anume: +
-* Constant: accesul nu depinde de indexul buclei interioare +
-* Secvential: aceesul la memorie este contiguu (adica in celule succesive de memorie) +
-* Nesecvential:​ accesul la memorie nu este contiguu (celulele de memorie logic succesive, sunt de fapt adresate cu pauze de dimensiune N) +
- +
-Astfel, pentru cele sase configuratii,​ se obtine:+
  
-^ Loop order ^ c[i][j] += ^ a[i][k] ^ * b[k][j] ^ +Modul cum accesam memoria influenteaza foarte mult performanta sistemului. Cum putem avea arhitecturi foarte diferite din punctul de vedere al ierarhiei de memorie este important de inteles ca nu putem dezvolta un program care sa ruleze optim in toate cazurile. Un program CUDA este portabil caci poate fi usor rulat pe diferite arhitecturi NVIDIA CUDA, insa de cele mai multe ori trebuie ajustat in functie de arhitectura pentru o performanta optima.
-| i-j-k: | Constant ​  | Secvential | Nesecvential | +
-| i-k-j: | Secvential | Constant | Secvential | +
-| j-i-k: | Constant | Secvential | Nesecvential | +
-| j-k-i: | Nesecvential | Nesecvential | Constant | +
-| k-i-j: | Secvential | Constant | Secvential | +
-| k-j-i: | Nesecvential | Nesecvential | Constant |+
  
-Care sunt totusi, comparativ, performantele celor trei moduri ​de acces? In mod claraccesul constant ​este mai bun decat cel secvential – aceste constante ​in cadrul unor buclesunt in general puse in registriducand la imbunatatirea performantelor ​ algoritmuluidupa cum s-a aratat in optimizarea 1Accesul secvential la randul sau, este mai bun decat cel nesecvential, ​in principal pentru ca utilizeaza considerabil mai bine cache-ul.+In general pentru arhitecturile ​de tip GPUmemoria shared ​este impartita ​in module de SRAM identicedenumite bancuri de memorie (memory banks). Fiecare banc contine o valoare succesiva de 32 biti (de exempluun int sau un float)astfel incat accesele consecutive intr-un array provenite de la threaduri consecutive sa fie foarte rapidBank conflicts au loc atunci cand se fac cereri multiple asupra datelor aflate ​in acelasi banc de memorie.
  
 <note important>​ <note important>​
-Luand in considerare aceste observatii, putem concluziona ca: +Conflictele de access la bancuri de memorie (cache) pot reduce semnificativ performanta.
-* Configuratiile k-i-j si i-k-j ar trebui sa aiba cele mai bune performante +
-* Configuratiile i-j-k si j-i-k ar trebui sa fie mai proaste decat primele, si +
-* Configuratiile j-k-i si k-j-i ar trebui sa fie cele mai proaste!+
 </​note>​ </​note>​
  
-===Activitate practica ​Ordinea buclelor ===+Cand are loc un bank conflict, hardware-ul serializeaza operatiile cu memoria (warp/​wavefront serialization),​ si face astfel toate threadurile sa astepte pana cand operatiile de memorie sunt efectuate. In unele cazuri, daca toate threadurile citesc aceeasi adresa de memorie shared, este invocat automat un mecanism de broadcast iar serializarea este evitata. Mecanismul de broadcast este foarte eficient si se recomanda folosirea sa de oricate ori este posibil.  
 +Spre exemplu daca linia de cache este alcatuita din 16 bancuri de memorie, avem urmatoarele situatii care impacteaza performanta accesului la cache (in comentarii apare echivalentul OpenCL, intrucat memoria shared din CUDA are ca echivalent memoria locala in OpenCL):
  
----- +<​code ​sh
- +__global__ void func(...) {  // __kernel void func(...) 
-Efectiv, care este adevarul? Construiti singuri aceste scenarii si analizati aceasta problema! +...  
- +   __shared__ int *array// __local int *array
----- +   x array[threadIdx.x]// x array[get_local_id(0)]; => performanta 100%, 0 bank conflicts 
- +   x array[threadIdx.x + 1]; // x = array[get_local_id(0) + 1]; => performanta 100%, 0 bank conflicts 
-Pentru a studia mai in detaliu problema, sa analizam un pic configuratia i-j-k (desi nu este cea mai buna configuratie,​ cum vedem de mai sus):  +   x = array[threadIdx.x * 4]; // x = array[get_local_id(0) * 4]; => performanta 25%, 4 bank conflicts 
- +   x = array[threadIdx.x * 16]; // x = array[get_local_id(0) * 16]; => performanta 6%,  16 bank conflicts 
-<​code ​cpp+...
-for (i=0;​i<​N;​i++){ +
-   for (j=0;j<N;j++){ +
-      sum=0; +
-      for (k=0;k<N;k+++
-         sum+=a[i][k]*b[k][j]; +
-      c[i][j= sum;  +
-   }+
 } }
 </​code>​ </​code>​
  
-Cate cache-miss-uri sunt generate in acest algoritm, cu aceasta secventa ​de acces la memorie? In mod evidentaceasta nu este o intrebare usoaraDe exemplu: daca fiecare matrice ​ar fi de doua ori mai mare decat cache-ul, ar avea loc multe incarcari si eliberari de linii, ducand astfel la o formula complicataAstfel, cel mai simplu aproximam, si consideram ca dimensiunea matricei este mult mai mare decat cea a Cache-ului. Astfel, fie C, numarul de elemente din matrice ce intra in Cache. +In cazul arhitecturilor ​de tip CPUmemoria shared ​este doar regiune din RAMOptimizarile pentru a tine datele critice in memoria shared pentru GPU nu ar prezenta deci aceleasi imbunatatiri ​de performanta.
-Astfel, considerand algoritmul de mai sus (fara optimizarea pentru constante):+
  
-<code cpp> +====== Aplicații ======
-for (i=0;​i<​N;​i++){ +
-   // Citeste linia i pt a in Cache (Ra) +
-   // Scrie linia i a lui c in Memorie (Wc) +
-   for (j=0;​j<​N;​j++){ +
-      // Citeste coloana j a lui b in Cache (Rb)  +
-      for (k=0;​k<​N;​k++){ +
-         ​c[i][j] +a[i][k] * b[k][j]; +
-      } +
-   } +
-+
-</​code>​+
  
-Astfel, daca L este dimensiunea unei linii de Cachepentru (Ra) obtinem aproximativ N*(N/L) cache-miss-uri,​ pentru (Wc) la fel, iar pentru (Rb) un dezastruos N*N*N! Acest lucru se intampla deoarece, desi accesul la b este secvential intr-o coloana, matricea este salvata in memorie utilizand row-major! Concluzia este descurajatoare:​ 2N<​sup>​2<​/sup>/L + N<​sup>​3<​/sup> ​-> N<​sup>​3<​/sup> cache-miss-uri! Se adauga la acest aspect si cele 2N<​sup>​3<​/sup> operatii aritmetice, si se ajunge la raportul: operatii aritmetice ​operatii cu memoria -> 2. Acest lucru este extrem de rau, deoarece noi stim de la (curs) si de la alte materii, ca arhitecturile calculatoarelor NU sunt echilibrate,​ si ca operatiile aritmetice sunt de ordine de marime mai rapide decat operatiile cu memoria. De aceea, memoria ramane in continuare bottleneck-ul pentru aceasta implementare a inmultirii de matrice. Pentru a obtine performante mai bune, este necesara obtinerea unui raport considerabil mai mare. +Urmăriți instrucțiunile ​de pe [[https://gitlab.cs.pub.ro/asc/asc-public/-/tree/master/​labs/​cuda/​arch|GitLab]]
  
-Cum se face insa, ca pentru N<​sup>​2</​sup>​ elemente intr-o matrice, ajungem la N<​sup>​3</supcache-miss-uri?​ Pai am stabilit ca acest lucru se datoreaza accesului ineficient al lui bdeoarece se incearca incarcarea coloana cu coloana a matricei!+<note important> 
 +Recomandăm sa va delogati mereu de pe serverele din cluster dupa terminarea sesiuniiutilizand comanda ''​exit''​
  
-Concluzia acestei analize este ca nu putem spunedoar dupa numarul ​de operatii efectuate si dimensiunea datelor folositedaca un algoritm ​va suferi sau nu din cauza unui bottleneck la memorie.+Alternativdaca ati uitat sesiuni deschise, puteti verifica acest lucru de pe fep8.grid.pub.routilizand comanda ''​squeue''​. In cazul in care identificati astfel de sesiuni "​agatate",​ le puteti sterge (si va rugam sa faceti asta), utilizand comanda ''​scancel ​ ID''​ unde ID-ul il identificati ​din comanda anterioara ''​squeue''​. Puteți folosi mai precis ''​squeue -u username''​ (username de pe fep8.grid.pub.ro) pentru a vedea doar sesiunile care vă interesează.
  
-Solutia este: utilizarea mai ingenioasa a cache-ului. +Daca nu veti face aceasta ​delogareveti putea ajunge ​in situatia ​in care sa nu va mai puteti loga pe nodurile ​din cluster
- +</note>
-Acest lucru se poate realiza prin reorganizarea operatiilor din cadrul inmultirii de matrice pentru a obtine mai multe cache-hit-uri. Faptul ca adunarea si inmultirea sunt atat operatii asociative, cat si comutative ​face posibila ​aceasta ​reordonare a operatiilor. Acesta este un subiect de cercetare asupra caruia si-au indreptat atentia numerosi cercetatori de-a lungul timpului, generand o multitudine de algoritmi si de teoreme matematice care sa ii sustina. In orice caz, daca vom considera r = raportul intre operatiile aritmetice si operatiile ​ la memorie (cu cache-miss-uri),​ este evident ca se doreste un r maxim, pentru a elimina bottleneck-ul de la memorie. S-a aratat ca orice reorganizare a acestui algoritm este limitata la r = O(sqrt(C))unde C este dimensiunea Cache-ului (in numar de elemente ce intra in Cache). Acest lucru arata ca r nu scaleaza cu dimensiunea matricei N, indiferent de impartirea intuitiva a lui 2N<​sup>​3</​sup>​ la N<​sup>​2</​sup>​... +
-===Solutia: “Blocked Matrix Multiplication”=== +
- +
-Pentru a rezolva problema accesului in b pentru coloane intregi, se va trece la accesarea unui subset a unei coloane in b, sau a mai multor coloane la un moment dat. Pentru o mai buna intelegere, urmariti desenele de mai jos: +
- +
-{{:​asc:​lab5:​c_axb_1.jpg|}} +
- +
-Ideea de baza este refolosirea cat mai buna a elementelor aflate in cache (pentru matricea b). Astfel odata cu calculul lui c[i][j], de ce nu am calcula si c[i][j+1], daca tot se afla in cache si coloana j+1. Acest lucru presupune insa reordonarea operatiilor astfel: calculeaza primii b termeni pentru c[i][j], calculeaza primii b termeni pentru c[i][j+1], calculeaza urmatorii b termeni pentru c[i][j], calculeaza urmatorii b termeni pentru c[i][j+1], etc. +
- +
-{{:​asc:​lab5:​c_axb_2.jpg|}} +
-  +
-In acest mod, de ce nu am calcula o intreaga sectiune de linie din c, folosind aceste reordonari de operatii? +
- +
-Ce s-ar intampla daca am incerca sa calculam o intreaga linie din c? +
- +
-{{:​asc:​lab5:​c_axb_3.jpg|}} +
- +
-Ar insemna ca trebuie sa incarcam toate coloanele lui b in memorie (cache), lucru pe care am incercat ​sa il evitam aici! Astfel, se vor refolosi doar acele blocuri din b ce au fost deja incarcate. De aici nu ne mai ramane decat sa utilizam intreaga linie de cache din b, si obtinem ideea de baza a algoritmului “Blocked Matrix Multiplication”:​ +
- +
-{{:​asc:​lab5:​c_axb_4.jpg|}} +
-  +
-Operatiile trebuie reordonate astfel: calculeaza primii b termeni pentru c[i][j] din blocul C, calculeaza urmatorii b termeni pentru c[i][j] din blocul C, ..., calculeaza ultimii b termeni pentru c[i][j] din blocul C. Generalizand:​ +
- +
-{{:​asc:​lab5:​bmm.jpg|}} +
- +
-Pentru a calcula blocul C<​sub>​22</​sub>​ folosim formula: +
- +
-C<​sub>​22</​sub>​ %%=%% A<​sub>​21</​sub>​B<​sub>​12</​sub>​ + A<​sub>​22</​sub>​B<​sub>​22</​sub>​ + A<​sub>​23</​sub>​B<​sub>​32</​sub>​ + A<​sub>​24</​sub>​B<​sub>​42</​sub>​  +
- +
-ce presupune patru inmultiri si patru adunari de matrice. Ideea este ca fiecare inmultire opereaza ​pe un block suficient de mic ca dimensiune astfel incat sa intre in Cache! +
- +
-Versiunea inmultirii de matrice utilizand metoda bloc si ordonarea i-j-k devine:  +
- +
-<code cpp> +
-for (i=0;​i<​N/​b;​i++){ +
-   for (j=0;​j<​N/​b;​j++){ +
-      for (k=0;​k<​N/​b;​k++){ +
-         ​C[i][j] += A[i][k]*B[k][j] +
-      } +
-   } +
-+
-</​code>​ +
- +
-unde: +
-* b este dimensiunea blocului (presupunem ca b divide N) +
-* C[i][j] este un bloc al matricei C pe linia i si coloana j +
-* "​+="​ inseamna adunare de matrice +
-* si "​*"​ inseamna inmultire de matrice +
- +
-Ce se intampla cu Cache-miss-urile acum? +
-<code cpp> +
-for (i=0;​i<​N/​b;​i++){ +
-   for (j=0;​j<​N/​b;​j++){ +
-      // Scrie blocul C[i][j] al lui c in Memorie (Wc) +
-      for (k=0;​k<​N/​b;​k++){ +
-         // Citeste blocul A[i][k] pt a in Cache (Ra) +
-         // Citeste blocul B[k][j] pt b in Cache (Rb)  +
-         ​C[i][j] += A[i][k] * B[k][j]; +
-      } +
-   } +
-+
-</​code>​ +
- +
-Pentru (Wc) avem acum (N/​b)*(N/​b)*b*b Cache-miss-uri,​ in vreme ce pentru (Ra) si (Rb) avem (N/​b)*(N/​b)*(N/​b)*b*b,​ astfel ducand la N<​sup>​2</​sup>​ + 2N<​sup>​3</​sup>/​b -> 2N<​sup>​3</​sup>/​b Cache-miss-uri pentru intregul algoritm. Combinand acest calcul cu faptul ca avem 2N<​sup>​3</​sup>​ operatii aritmetice, rezulta un raport r = 2N<​sup>​3</​sup>/​b / 2N<​sup>​3</​sup>​ -> b. Dupa cum am stabilit, r trebuie sa fie maxim (mai mare oricum decat 2-ul obtinut in varianta anterioara). Daca mergem pana la cazul extrem, il vom face pe b = N, dar asta nu este viabil, pentru ca atunci suntem ​din nou la cazul fara blocuri, de la care tocmai venim..+
- +
-Astfel, acest algoritm functioneaza doar daca blocurile intra in Cache. Acest lucru inseamna ca trei blocuri diferite, de dimensiune b*b, trebuie sa intre in Cache, pentru toate cele trei matrice (a, b si c). Daca C este dimensiunea Cache-ului in elemente de matrice, atunci trebuie sa fie 3b<​sup>​2</​sup>​ ≤ C sau b ≤ √(C / 3) . Astfel, in cel mai bun caz, r-ul trebuie sa fie si el  √(C / 3). +
- +
-Putem astfel spune, pentru diverse procesoare, cunoscand rata de operatii aritmetice la cache-miss-uri r, care este dimensiunea necesara a Cache-ului, pentru a rula acest algoritm, astfel incat procesorul sa NU astepte niciodata memoria: +
- +
-/* PLEASE UPDATE THIS! +
-^ Procesor ^ Dimensiune Cache (KB) ^ +
-|Ultra 2i   | 14.8 | +
-|Ultra 3    | 4.7 | +
-|Pentium 3  | 0.9 | +
-|Pentium 3M | 2.4 | +
-|Power 3    | 1.8 | +
-|Power 4    | 5.4 | +
-|Itanium 1  | 31.1 | +
-|Itanium 2  | 0.7 | +
- +
-*/ +
- +
-===Activitate practica - BMM & Optimizare pentru Cache === +
-De aceea incercati sa experimentati cele prezentate in acest laborator, in C. Pentru cei interesati, incercati completarea tabelului de mai sus cu dimensiunea Cache-ului pentru procesoarele voastre personale. Acest lucru presupune evident, si o documentare asupra caracteristicilor sistemului propriu (determinarea r-ului, a dimensiunii Cache-ului etc.). +
- +
-==In loc de concluzie== +
- +
-Intelegerea reala a comportamentului unei aplicatii (algoritm), din punctul de vedere al utilizarii cache-ului (si al performantelor in general), este o chestiune complexa, ce necesita multa rabdare si cunostinte diverse. Deseori, aproximatii utile pot fi folosite pentru a imbunatati unele aspecte ale implementarii curente. Utilizarea blocurilor este intalnita deseori in algoritmi si aplicatii ce necesita performante crescute.  +
- +
- +
-== Exercitii == +
- +
-# [[:​asc:​lab5:​index#​activitate_practica_-_optimizare_constantelor_si_al_accesului_la_vectori | Optimizarea constantelor si al accesului la vectori]] (**3p**) folosind matrici liniarizate. +
-# [[:​asc:​lab5:​index#​activitate_practica_-_ordinea_buclelor | Ordonarea buclelor]] folosind matrici liniarizate. (**3p**) +
-# [[:​asc:​lab5:​index#​activitate_practica_-_bmm_optimizare_pentru_cache | Optimizari pentru Cache]] folosind matrici liniarizate. (**4p**) +
-# In general, nu recomandam alocarea matricelor ca vectori de vectori. Ca bonus va sugeram sa realizati un test unde se face acest tip de alocare si se verifica "​performantele"​ obtinute. ​ Bonus (**2p**). +
- +
-== Resurse ==  +
-* Responsabilul acestui laborator: [[emil.slusanschi@cs.pub.ro|Emil Slușanschi]] +
-* <​html><​a class="​media mediafile mf_pdf"​ href=":​asc:​lab5:​index?​do=export_pdf">​PDF laborator</​a>​</html>+
  
-== Discutii interesante ​== +===== Resurse =====
  
-* [[http://​stackoverflow.com/​questions/​11227809/​why-is-processing-a-sorted-array-faster-than-an-unsorted-array ​De ce este mai rapida procesarea unui vector ordonat?]]+<​hidden>​ 
 +{{:asc:​lab8:​sol:​lab8_sol.tar.gz|Soluție Laborator 8}} 
 +</​hidden>​
  
-{{:asc:​lab5:​what_every_programmer_should_know_about_memory_by_ulrich_drepper_.pdf|What every programmer should know about memory.pdf}}+  ​Responsabili laboratorMatei Barbu, Alexandru Bala
  
-=== Valgrind ​=== +==== Referinte ==== 
- ​* ​http://valgrind.org/docs/manual/cg-manual.html+  Bibliografie 
 +    * [[https://booksite.elsevier.com/​9780124077263/​downloads/​advance_contents_and_appendices/​appendix_C.pdf|Graphics and Computing GPUs]] 
 +  * Documentatie CUDA: 
 +    * [[https://docs.nvidia.com/cuda/cuda-c-programming-guide/​index.html|CUDA C Programming]] 
 +    * [[https://​docs.nvidia.com/​cuda/​pdf/​CUDA_Compiler_Driver_NVCC.pdf| CUDA NVCC compiler]] 
 +    * [[https://​docs.nvidia.com/​cuda/​profiler-users-guide/​index.html| CUDA Visual Profiler]] 
 +    * [[https://​docs.nvidia.com/​cuda/​cuda-toolkit-release-notes/​index.html|CUDA Dev Toolkit]] 
 +    * [[https://​developer.nvidia.com/​cuda-gpus|CUDA GPUs]] 
 +  * Acceleratoare xl (NVidia P100) 
 +    * [[https://​www.nvidia.com/​en-us/​data-center/​tesla-p100/​|NVIDIA Pascal P100]] 
 +  * Advanced CUDA 
 +    * [[https://​developer.download.nvidia.com/​CUDA/​training/​StreamsAndConcurrencyWebinar.pdf|CUDA Streams 1]] 
 +    * [[https://​devblogs.nvidia.com/​gpu-pro-tip-cuda-7-streams-simplify-concurrency/​|CUDA Streams 2]] 
 +    * [[https://​devblogs.nvidia.com/​introduction-cuda-dynamic-parallelism/​|CUDA Dynamic Parallelism]] 
 +    * [[http://​www-personal.umich.edu/​~smeyer/​cuda/​grid.pdf | CUDA Thread Basics]] 
 +    * [[https://​devblogs.nvidia.com/​even-easier-introduction-cuda/​ | An Even Easier Introduction to CUDA]]
  
asc/laboratoare/05.1580767511.txt.gz · Last modified: 2020/02/04 00:19 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0