Laboratorul 1 - Introducere în clusterul HPC al UPB

Scopul laboratorului

Scopul acestui laborator este de a vă familiariza cu infrastructura de calcul de înaltă performanță (HPC – High Performance Computing) a facultății.

Concret, veți învăța:

  • ce este un cluster HPC și de ce a apărut paralelismul,
  • cum să vă conectați la cluster prin nodul de acces (FEP - Front-End Processor),
  • cum să investigați resursele disponibile (partiții, CPU-uri, GPU-uri, memorie),
  • cum să alocați resurse și să rulați joburi prin SLURM,
  • cum să compilați și să rulați programe multi-threaded și CUDA pe cluster.

La finalul laboratorului ar trebui să puteți lansa un job pe cluster fără ajutor.

De ce a apărut paralelismul?

Modelul de calcul secvențial (arhitectura von Neumann) a fost suficient pentru multă vreme. Legea lui Moore (o predicție economică din 1965) s-a dovedit adevărată, iar numărul de tranzistori de pe un cip s-a dublat (aproximativ) la fiecare 2 ani. Pe măsură ce tranzistorii se micșorau, energia dinamică necesară comutării lor scădea, la fel și tensiunea de alimentare a cipului. Astfel, procesoarele puteau funcționa, de la o generație la alta, la frecvențe tot mai mari, fără ca puterea consumată să crească. Această observație este cunoscută drept legea de scalare a lui Dennard.

În jurul anului 2004, scalarea Dennard s-a oprit, iar frecvența procesoarelor s-a plafonat. Energia dinamică de comutare a continuat să scadă, dar energia statică (leakage) nu, până când aceasta din urmă a ajuns să domine consumul. Numărul de tranzistori a continuat să crească conform legii lui Moore, dar, odată ce scalarea Dennard a încetat, singura cale de a valorifica această creștere a rămas inovația arhitecturală: mai multe nuclee (core-uri), unități vectoriale mai late, acceleratoare specializate, etc.

În consecință, hardware-ul a devenit tot mai paralel și eterogen. Accentul în designul calculatoarelor s-a mutat de la creșterea continuă a frecvenței procesorului, la ahitecturi de prelucrări paralele: clustere cu memorie distribuită, GPU-uri programabile, unități vectoriale SIMD (Single Instruction Multiple Data), procesoare multi-core. Spre deosebire de paralelismul la nivel de instrucțiune (de exemplu, execuția superscalară), de care programatorul este, în marea majoritate a cazurilor, agnostic, lăsând procesorul să se ocupe de paralelizare, aceste forme de paralelism trebuie exprimate explicit în cod (direct, sau prin compilator). Pentru inginerii software interesați de performanță, programarea paralelă NU mai este opțională.

Domeniul care a dezvoltat și folosește la scară largă aceste arhitecturi se numește calcul de înaltă performanță (high-performance computing, sau, abreviat, HPC).

Ce este un cluster HPC?

Un cluster HPC este format din mai multe sisteme interconectate, numite noduri. Înainte de a vedea cum colaborează, trebuie să înțelegem ce se află în interiorul unui singur nod, pentru că aici se întâlnește prima formă de paralelism pe care o vom folosi.

Un nod este, în esență, un sistem multiprocesor: conține mai multe procesoare care partajează același spațiu de adrese. Memoria este văzută de toate aceste procesoare ca un singur bloc, iar orice procesor poate citi sau scrie la orice adresă. Din acest motiv, astfel de sisteme se mai numesc și calculatoare cu memorie partajată (shared memory).

Cel mai simplu sistem de acest fel este modelul SMP (Symmetric Multiprocessor) (Vezi Figura 1). Un SMP are N procesoare care partajează o singură memorie. Sistemul de operare le tratează pe toate la fel, iar costul accesării oricărei variabile din memorie este același, indiferent de procesorul care solicită accesul. Procesoarele sunt, deci, „simetrice”, din punctul de vedere al sistemului de operare și al memoriei.

 SMP cu N procesoare care partajează o memorie

Figura 1: SMP cu N procesoare care partajează o memorie

Modelul SMP este, însă, o simplificare drastică și nerealistă. Un CPU modern (Vezi Figura 2) este, la bază, tot un sistem multiprocesor (în care fiecare procesor este denumit core). Mai multe nuclee (cores) sunt plasate pe aceeași plăcuță de siliciu și montate într-un singur socket pe placa de bază. Un CPU este optimizat pentru latență mică, adică pentru a finaliza cât mai repede fiecare operație în parte.

 CPU multicore cu 8 nuclee

Figura 2: Un CPU multi-core tipic, cu 8 nuclee. Fiecare nucleu are cache-uri de nivel 1 pentru date (L1D) și pentru instrucțiuni (L1I), un cache unificat de nivel 2 (L2) și un cache de nivel 3 (L3) partajat. CPU-ul are două controlere de memorie, fiecare cu trei canale de acces la memoria din afara CPU-ului (DRAM).

Memoria unui sistem multi-core este mult mai complicată decât cea din modelul SMP. Toate nucleele văd memoria DRAM ca un singur spațiu de adrese, dar aceasta este mult mai lentă decât nucleele. De aceea, CPU-urile moderne includ regiuni mici de memorie foarte rapidă, integrate în CPU, numite cache-uri.

Cache-urile nu reprezintă spații de adrese separate, ci conțin copii ale unor zone de memorie din DRAM, pe care programatorul nu le accesează direct. Copierea între cache și DRAM se face în blocuri mici, numite linii de cache. O linie de cache tipică are 64 de octeți (suficient pentru 8 numere în virgulă mobilă cu precizie dublă).

Cache-urile sunt organizate în linii pentru că, în multe programe, dacă se accesează valoarea de la adresa N, este probabil ca următoarea valoare accesată să fie cea de la adresa N + 1. Această proprietate se numește localitate spațială: când aducem o linie întreagă din DRAM, aducem și vecinii valorii cerute, iar accesele următoare sunt rapide.

Memoria cache este ierarhică și cuprinde:

  • Cache de nivel 1 (L1): cel mai apropiat de nucleu, separat pentru date (L1d) și pentru instrucțiuni (L1i). Fiecare nucleu are propriul L1.
  • Cache de nivel 2 (L2): fiecare nucleu are propriul L2, care conține atât date, cât și instrucțiuni (de aceea se numește unificat).
  • Cache de nivel 3 (L3): partajat de toate nucleele unui CPU.

Deoarece liniile de cache se mișcă constant prin ierarhie în timpul execuției programului, iar mai multe nuclee pot accesa oricând aceeași linie, există posibilitatea să apară conflicte de acces la memorie. De aceea, CPU-ul are integrat un protocol de coerență a cache-urilor (cache coherency protocol), care se asigură că, în final, toate nucleele vor vedea aceleași valori în memorie.

Cache-urile au dimenisuni reduse. De exemplu, un nod al clusterului haswell al UPB are 2 CPU-uri a câte 8 nuclee fiecare, iar cache-urile au următoarele dimensiuni:

  • 32 KiB L1d per nucleu
  • 32 KiB L1i per nucleu
  • 256 KiB L2 per nucleu
  • 20 MiB L3 per CPU (partajat între cele 8 nuclee)

Cu cât ne depărtăm de nucleu, cu atât memoria este mai mare, dar și lentă. Pentru un CPU generic, de performanță ridicată, timpul necesar accesării unei singure valoari din memorie (latența accesului) este:

  • L1: 4 cicluri
  • L2: 12 cicluri
  • L3: 42 de cicluri
  • DRAM: 250 de cicluri

Aceste valori sunt orientative. Diferența de peste 60 de ori dintre L1 și DRAM justifică existența întregii ierarhii. Un program care reutilizează datele din cache poate rula de zeci de ori mai repede decât unul care accesează mai des DRAM-ul.

Prin urmare, sistemele multiprocesor moderne nu au timp de acces uniform la memorie (ca în modelul SMP), ci o arhitectură de memorie neuniformă, numită NUMA (Nonuniform Memory Architecture). Situația este și mai complicată decât în cazul unui singur CPU multi-core. Serverele performante, în special cele folosite în HPC, conectează adesea mai multe CPU-uri în același nod, prin interconexiuni P2P (point to point) de mare viteză. Un exemplu este prezentat în Figura 3, cu 4 CPU-uri.

 Sistem NUMA cu patru CPU-uri conectate prin interconexiuni P2P

Figura 3: Un sistem NUMA cu 4 CPU-uri conectate P2P. DRAM-ul este partajat, iar costul accesării diferitelor regiuni de memorie variază semnificativ.

Fiecare CPU are propriul controler de memorie și propriul bloc de DRAM. Toată memoria nodului este organizată într-un singur spațiu de adrese (partajat) și poate fi accesată de orice nucleu din sistem. Costul accesului variază însă mult: un nucleu accesează rapid liniile de cache mapate la DRAM-ul propriului CPU și mai lent pe cele mapate la DRAM-ul altui CPU, aflat la celălalt capăt al unei interconexiuni.

Datorită protocoalelor de cache coherence și a compilatoarelor moderne, când scriem un program, ne putem gândi inițial că îl scriem pe un model SMP. Ulterior, pentru optimizare, ne folosim de faptul că lucrăm pe un sistem NUMA (de exemplu reordonarea buclelor la înmulțirea matricelor, pentru a refolosi date din cache, Blocked Matrix Multiplication, inițializarea datelor chiar pe nucleele care le vor procesa ulterior, etc.).

Dar ce facem atunci când nevoia de calcul depășește posibilitatea rulării optime pe un sistem NUMA? Aplicațiile moderne din știință și inginerie, precum simulări numerice, antrenare AI, procesare Big Data, necesită resurse de calcul care depășesc cu mult capacitatea unui astfel de sistem, care deja este foarte costisitor de realizat. Soluția este conectarea acestor servere (noduri) în rețea, realizând un cluster HPC. Combinând puterea a zeci sau sute de noduri de calcul, interconectate printr-o rețea internă de mare viteză (de obicei InfiniBand sau Ethernet RDMA, de ordinul sutelor de Gbps), un cluster HPC satisface noile cerințe de calcul intensiv.

Atunci când nodurile unui cluster sunt specializate pentru calcul intensiv (de exemplu, fiecare nod reprezintă un sistem NUMA de CPU-uri multi-core, conectate la GPU-uri performante și RAM de ordinul TB), ele se numesc noduri de calcul (compute nodes), pentru a le deosebi de nodurile utilizate în afara calcului (server nodes). Nodurile nu partajează memorie fizică. Memoria este distribuită în sistem, fiecare nod având propriul spațiu de adrese. Pentru a comunica, nodurile își transmit mesaje. API-ul standard pentru programarea distribuită este MPI (Message Passing Interface).

Modelul cel mai răspândit în HPC este MPI între noduri și OpenMP în interiorul unui nod, numit și modelul hibrid MPI + OpenMP. Cele două niveluri de paralelism din model corespund direct celor două tipuri de memorie de mai sus: memoria partajată din interiorul nodului și memoria distribuită dintre noduri.

Alte tipuri de acceleratoare întâlnite în infrastructurile HPC moderne (click pentru detalii)

Alte tipuri de acceleratoare întâlnite în infrastructurile HPC moderne (click pentru detalii)

Pe lângă GPU-uri, în infrastructurile HPC și în centrele de date pot apărea acceleratoare specializate:

  • TPU (Tensor Processing Unit) – ASIC (Application-Specific Integrated Circuit) dezvoltat de Google, optimizat pentru operații cu tensori (înmulțiri de matrice) folosite în antrenarea și inferența rețelelor neuronale. Se folosește mai ales prin Google Cloud.
  • Alte acceleratoare AI – de exemplu Intel Gaudi sau Huawei Ascend (numit și NPU - Neural Processing Unit), realizate pentru antrenarea și inferența rețelelor neuronale în centre de date. Termenul NPU desemnează, în general, procesoare pentru inferență eficientă energetic din dispozitive mobile și edge (Intel, AMD, Qualcomm, Samsung, Apple), mai puțin întâlnite în nodurile HPC.
  • DPU (Data Processing Unit) – procesor dedicat operațiilor de rețea, securitate și stocare în centrele de date, scutind CPU-ul de aceste sarcini (ex: NVIDIA BlueField, AMD Pensando).
  • FPGA (Field-Programmable Gate Array) – circuit integrat reconfigurabil după fabricație, care poate fi programat la nivel hardware pentru sarcini specifice. Oferă mai multă flexibilitate decât un ASIC și, pentru anumite sarcini, o eficiență energetică mai bună decât un procesor general. Se folosește în procesare de semnal și inferență cu latență redusă (ex: AMD/Xilinx, Altera)
  • QPU (Quantum Processing Unit) – unitatea de procesare a unui calculator cuantic, bazată pe qubiți aflați în superpoziție. Utilizat în optimizare, simulări moleculare și criptografie. Tehnologie încă în stadiu experimental.


Clusterul HPC al UPB

Universitățile tehnice dispun adesea de clustere HPC proprii, folosite pentru activități de cercetare și educație. Universitatea Politehnica din București deține un astfel de cluster modern, echipat cu GPU-uri performante (precum NVIDIA H100, A100).

Componentele principale ale clusterului UPB sunt:

  • Noduri de acces (FEP) - servere prin care utilizatorii se conectează la cluster. Aici doar se pregătește codul, urmând să fie apoi lansat ca job pe cluster prin intermediul scheduler-ului de joburi (vezi mai jos).
  • Noduri de calcul - serverele pe care rulează efectiv joburile utilizatorilor (conțin CPU-uri, GPU-uri, RAM extins).
  • Sistem de fișiere partajat - accesibil de pe toate nodurile (de exemplu Lustre, BeeGFS, Ceph).
  • Scheduler de joburi - software-ul care primește cererile utilizatorilor și alocă resursele. Clusterul UPB folosește SLURM (Simple Linux Utility for Resource Management).

Nodurile de calcul sunt organizate în partiții - grupuri de noduri cu configurație hardware omogenă. Partițiile sunt, în schimb, heterogene între ele (de exemplu: o partiție doar cu CPU-uri, una cu GPU-uri A100, alta cu H100, etc.).

Exemplu de afișare a partițiilor:

[nume.student@fep10 ~]$ sinfo -o "%10P %30N %8c %10m %20G %10a"
PARTITION  NODELIST                       CPUS     MEMORY     GRES                 AVAIL
dgxa100    dgxa100-ncit-wn[01-04]         256      2063510    gpu:tesla_a100:8     up
dgxh100    dgxh100-precis-wn[01-03]       224      1998908+   gpu:tesla_h100:8     up
h200       ucsc-precis-h200-wn[11-17]     256      2321395+   gpu:nvidia_h200:8    up
haswell*   haswell-wn[29-42]              32       127309     (null)               up
hd         xl675dg10-wn175                96       773225     gpu:tesla_a100:10    up
ml         sprmcrogpu-wn[140-141]         112      128224     gpu:tesla_a100:2     up
sprmcrogpu sprmcrogpu-wn13                64       515093     gpu:rtx_2080ti:8     up
ucsx       ucsx-ncit-gpu-wn100            64       257158     gpu:tesla_a100:3     up
xl         xl270-wn[161-162]              56       257138     gpu:tesla_p100:2     up

Explicații coloane:

  • CPUS = număr total de thread-uri hardware per nod,
  • MEMORY = RAM per nod (în MB),
  • GRES = resurse generice (GPU-uri: tip și număr per nod),
  • AVAIL = starea partiției

Asteriscul din dreptul partiției (haswell* în cazul nostru) indică partiția implicită. Dacă nu specificați o partiție, SLURM va aloca jobul pe partiția implicită.

Nu toate partițiile sunt accesibile tuturor utilizatorilor! Puteți verifica accesul vostru folosind următoarele comenzi:

# Vedeți account-ul vostru per partiție, inclusiv timpul maxim de rulare a unui job.
# Rândurile fără partiție specificată arată limitele implicite ale account-ului vostru, care se aplică pe orice partiție unde nu există o regulă specifică.
[nume.student@fep10 ~]$ sacctmgr show user $USER withassoc format=user%20,account,partition,MaxWallDurationPerJob
                User    Account  Partition     MaxWall
-------------------- ---------- ---------- -----------
        nume.student        asc               00:10:00
 
# Vedeți restricțiile unei partiții (de exemplu, ucsx)
[nume.student@fep10 ~]$ scontrol show partition ucsx | grep -Ei 'allow|deny'
   AllowGroups=ALL DenyAccounts=ml,nlp,nlp_highprio,ml_test,dataset-proc,hria AllowQos=ALL

Contul din care faceți parte la această materie (asc) are acces pe partițiile: dgxa100, dgxh100, haswell, ucsx și xl. Celelalte partiții (h200, hd, ml, sprmcrogpu) sunt rezervate unor echipe de cercetare specifice.

În SLURM, un account este o grupare administrativă de utilizatori (ex: asc, student, prof, ml). Fiecare utilizator este asociat la unul sau mai multe account-uri, iar partițiile pot restricționa accesul pe baza acestora prin AllowAccounts / DenyAccounts. Separat, AllowGroups controlează accesul pe baza grupurilor Linux (cele vizibile prin comanda id). Pe clusterul UPB, toate partițiile au AllowGroups=ALL, deci accesul este controlat exclusiv prin account-uri SLURM.

Arhitectura nodurilor de calcul ale clusterului UPB

CPU

Partiție CPU (arhitectură) Topologie CPU¹ Noduri NUMA Frecvență nucleu (bază; boost maxim)
dgxh100 Intel Xeon Platinum 8480C (Sapphire Rapids) 2 × 56 × 2 = 224 2 2.0 GHz; 3.8 GHz
dgxa100 AMD EPYC 7742 (Zen 2) 2 × 64 × 2 = 256 8 2.25 GHz; 3.4 GHz
ucsx Intel Xeon Gold 6326 (Ice Lake) 2 × 16 × 2 = 64 2 2.9 GHz; 3.5 GHz
xl Intel Xeon E5-2680 v4 (Broadwell) 2 × 14 × 2 = 56 2 2.4 GHz; 3.3 GHz
haswell Intel Xeon E5-2640 v3 (Haswell) 2 × 8 × 2 = 32 2 2.6 GHz; 3.4 GHz

¹ Socket-uri per nod × nuclee per socket × thread-uri per nucleu = thread-uri per nod.

Alte informații utile (click pentru detalii)

Alte informații utile (click pentru detalii)

Pe AMD EPYC, numărul de noduri NUMA depinde de o setare din BIOS (NPS - NUMA nodes per socket). Pe Intel, cu configurația implicită, 1 socket = 1 nod NUMA.

Toate nodurile sunt x86_64 și au instrucțiuni vectoriale (SIMD) AVX2. În plus, ucsx și dgxh100 au AVX-512, iar dgxh100 are și AMX.


Ierarhia de memorie

Partiție L1 (date / instrucțiuni)¹ L2¹ L3² DRAM/nod
dgxh100 48 KB / 32 KB 2 MB 105 MB ~2 TB (DDR5)
dgxa100 32 KB / 32 KB 512 KB 256 MB ~2 TB (DDR4)
ucsx 48 KB / 32 KB 1.25 MB 24 MB ~512 GB (DDR4)
xl 32 KB / 32 KB 256 KB 35 MB ~256 GB (DDR4)
haswell 32 KB / 32 KB 256 KB 20 MB ~128 GB (DDR4)

¹ Per nucleu
² Per socket

Pe toate partițiile, linia de cache are 64 B.
Pe AMD EPYC, L3 este împărțit în blocuri de 16 MB, fiecare partajat de câte 4 nuclee; un nucleu nu vede decât 16 MB.
Pe Intel, L3 este partajat de toate nucleele unui socket.

GPU

Partiție GPU Arhitectură GPUs/nod Topologie GPU¹ Tensor Cores Compute Capability Memorie GPU²
dgxh100 NVIDIA H100 80GB HBM3 Hopper 8 132 SM/GPU x 128 C/SM = 16896 C/GPU 528 (Gen 4) sm_90 80 GB HBM3; 5120-bit bus; ~3350 GB/s
dgxa100 NVIDIA A100-SXM4-80GB Ampere 8 108 SM/GPU x 64 C/SM = 6912 C/GPU 432 (Gen 3) sm_80 80 GB HBM2e; 5120-bit bus; ~2039 GB/s
ucsx NVIDIA A100-PCIE-40GB Ampere 3 108 SM/GPU x 64 C/SM = 6912 C/GPU 432 (Gen 3) sm_80 40 GB HBM2; 5120-bit bus; ~1555 GB/s
xl Tesla P100-PCIE-16GB Pascal 2 56 SM/GPU x 64 C/SM = 3584 C/GPU 0 sm_60 16 GB HBM2; 4096-bit bus; ~732 GB/s

¹ Streaming Multiprocessors (SMs)/GPU x CUDA cores/SM = CUDA cores/GPU
² capacitate tip; bus; bandwidth teoretic

Alte informații utile (click pentru detalii)

Alte informații utile (click pentru detalii)

Un Streaming Multiprocessor (SM) este unitatea fundamentală de execuție pe GPU, echivalentul unui core pe CPU. Fiecare SM conține multiple CUDA cores (pentru operații generale FP32/INT32) și Tensor Cores (pentru operații matriceale accelerate, folosite în deep learning). Toate CUDA cores dintr-un SM partajează aceleași resurse: registre, shared memory și cache L1.

Compute Capability (sm_XX) determină ce instrucțiuni și funcționalități hardware sunt disponibile. Este esențial la compilarea CUDA: nvcc -arch=sm_90 pentru H100, sm_80 pentru A100. Un binar compilat pentru un anumit compute capability nu rulează pe GPU-uri cu o versiune mai veche.


Ce este FEP-ul?

FEP-ul reprezintă nodul de acces către clusterul HPC al universității. Pe scurt, este serverul prin intermediul căruia utilizatorii se conectează la infrastructură pentru a pregăti și lansa job-urile pe resursele de calcul ale clusterului.

FEP-ul are un rol limitat, oferind:

  • un mediu de lucru minimal (shell Linux),
  • acces la fișierele proprii,
  • pregătirea codului sursă,
  • lansarea joburilor către nodurile de calcul din cluster.

Execuția efectivă a joburilor are loc exclusiv pe nodurile de calcul, care dispun de resursele reale de procesare (CPU, GPU, RAM extins).

Vă rugăm NU rulați aplicațiile direct PE FEP! Trimiteți-le ca joburi către cluster, folosind SLURM. FEP-ul este un nod comun de acces pentru toți utilizatorii, iar rularea de programe intensive pe el afectează toți utilizatorii conectați.

Conectarea la FEP se realizează prin SSH:

 ssh -X -o ServerAliveInterval=100 user.name@fep.grid.pub.ro 

Explicația parametrilor SSH (click pentru detalii)

Explicația parametrilor SSH (click pentru detalii)

  • -X – permite redirecționarea interfeței grafice (dacă folosiți aplicații GUI);
  • ServerAliveInterval=100 – menține conexiunea activă dacă terminalul e inactiv o perioadă.


Configurare SSH pentru conectare simplificată (click pentru detalii)

Configurare SSH pentru conectare simplificată (click pentru detalii)

Pentru o conectare mai simplă, puteți configura SSH să rețină parametrii de conectare.

1. Generați o pereche de chei SSH (dacă nu aveți deja una):

# Linux / macOS / WSL
ssh-keygen -t ed25519 -f ~/.ssh/id_fep
# Windows (PowerShell)
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_fep

2. Copiați cheia publică pe FEP:

# Linux / macOS / WSL
ssh-copy-id -i ~/.ssh/id_fep.pub user.name@fep.grid.pub.ro
# Windows (PowerShell)
type $env:USERPROFILE\.ssh\id_fep.pub | ssh user.name@fep.grid.pub.ro "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

3. Adăugați un alias în fișierul local ~/.ssh/config (Linux/macOS/WSL) sau %USERPROFILE%\.ssh\config (Windows):

Host fep fep.grid.pub.ro
    HostName fep.grid.pub.ro
    User <user.name>
    ForwardX11 yes
    IdentityFile ~/.ssh/id_fep
    ServerAliveInterval 100

De acum vă puteți conecta direct prin ssh fep.


Montarea sistemului de fișiere FEP pe calculatorul local (click pentru detalii)

Montarea sistemului de fișiere FEP pe calculatorul local (click pentru detalii)

Pentru o experiență confortabilă, recomandăm montarea sistemului de fișiere de pe FEP pe calculatorul local folosind sshfs. Astfel, puteți edita codul cu un IDE local (e.g. VS Code) și compila/rula pe cluster prin SSH:

Pregătire (o singură dată):

# Pe local, creați un director pentru mount
mkdir ~/asc_labs

Montare:

# Linux / macOS / WSL
sshfs user.name@fep.grid.pub.ro: ~/asc_labs
 
# Sau dacă ați configurat alias-ul SSH prezentat mai sus:
sshfs fep: ~/asc_labs

Demontare:

fusermount -u ~/asc_labs    # Linux / WSL
umount ~/asc_labs           # macOS

Pe Windows, puteți folosi SSHFS-Win sau Rclone pentru funcționalitate echivalentă.

Configurarea mediului de lucru pe cluster ​

Pe un cluster sunt instalate simultan mai multe versiuni de compilatoare și biblioteci (GCC, CUDA, OpenMPI, etc.). Pentru a evita conflicte și pentru a permite fiecărui utilizator să aleagă versiunile dorite, clusterele HPC folosesc Environment Modules — un sistem care modifică temporar variabilele de mediu (PATH, LD_LIBRARY_PATH) la cerere. Astfel, utilizatorul poate folosi exact versiunea dorită a unui compilator sau a unei biblioteci, fără a afecta sistemul global.

Comenzi uzuale:

module help
module avail                      # afișează modulele disponibile
module load libraries/cuda-13.0   # încarcă modulul de CUDA corespunzător
module list                       # arată modulele active în sesiunea curentă
module unload libraries/cuda-13.0 # dezactivează un modul
module purge                      # dezactivează toate modulele încărcate

Utilizare SLURM ​

SLURM este scheduler-ul de joburi folosit pe clusterul UPB. Rolul său este să primească cererile utilizatorilor, să le pună în coadă și să aloce resursele disponibile (CPU-uri, GPU-uri, memorie) în mod echitabil. Fiecare cont are restricții per partiție: în primul rând dacă are acces sau nu, timp maxim per job (de exemplu maximum 10 minute pentru conturile voastre, după cum ați văzut mai sus), număr maxim de joburi simultane, număr maxim de noduri folosite per job, limită de RAM per job, etc.

În mod implicit, SLURM alocă 1 CPU logic și 0 GPU-uri pentru orice job. Dacă programul vostru are nevoie de mai multe resurse (mai multe CPU-uri, GPU, memorie suplimentară), trebuie să le cereți explicit. Altfel, un program care are nevoie de N thread-uri logice va rula pe 1 singur thread hardware, iar un program care are nevoie de CUDA nu va merge deloc, chiar dacă codul în sine este corect.

Lansarea joburilor: sbatch vs srun

Există două moduri de a lansa joburi pe cluster: sbatch și srun.

sbatch trimite un job în coada SLURM și returnează imediat controlul — jobul rulează asincron, în fundal. Outputul standard se salvează în fișierul specificat prin --output (implicit slurm-<JOB_ID>.out), iar erorile în --error (implicit tot în slurm-<JOB_ID>.out dacă nu e specificat separat).

[nume.student@fep10 ~]$ sbatch hello_pthreads.sh
Submitted batch job 125771
[nume.student@fep10 ~]$ cat slurm-125771.out    # output-ul apare aici după finalizare
Hello from thread 0
Hello from thread 3
Hello from thread 1
...

srun lansează comanda sincron — terminalul se blochează și așteaptă finalizarea, iar outputul apare direct în consolă. Dacă închideți terminalul, job-ul se oprește. Este util pentru testare rapidă sau sesiuni interactive (srun –pty bash).

[nume.student@fep10 ~]$ srun --cpus-per-task=8 ./hello_pthreads
Hello from thread 0
Hello from thread 3
Hello from thread 1
...
sbatch srun
Mod execuție Asincron (fundal) Sincron (blocant)
Output Fișiere .out / .err Direct în terminal
La închiderea terminalului Jobul continuă Jobul se oprește
Utilizare tipică Aplicații complexe Testare, debugging, interactiv

Exemple de joburi

Mai jos sunt două programe pe care le vom folosi pentru a înțelege cum funcționează alocarea resurselor în SLURM.

Program Pthreads hello_pthreads.c (paralelism pe CPU) (click pentru a vedea codul)

Program Pthreads hello_pthreads.c (paralelism pe CPU) (click pentru a vedea codul)

hello_pthreads.c
#include <stdio.h>
#include <pthread.h>
 
void* thread_func(void* arg) {
    int id = *(int*)arg;
    printf("Hello from thread %d\n", id);
    return NULL;
}
 
int main() {
    int N = 8;
    pthread_t threads[N];
    int ids[N];
    for (int i = 0; i < N; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &ids[i]);
    }
    for (int i = 0; i < N; i++)
        pthread_join(threads[i], NULL);
    return 0;
}


Script sbatch corespunzător:

hello_pthreads.sh
#!/bin/bash
# Nu specificăm --partition, deci SLURM va folosi partiția implicită (haswell)
#SBATCH --cpus-per-task=8    # programul creează 8 thread-uri, deci cerem 8 CPU-uri logice
gcc -o hello_pthreads hello_pthreads.c -lpthread
./hello_pthreads

Program CUDA hello_cuda.cu (paralelism pe GPU) (click pentru a vedea codul)

Program CUDA hello_cuda.cu (paralelism pe GPU) (click pentru a vedea codul)

hello_cuda.cu
#include <stdio.h>
 
__global__ void hello() {
    printf("Hello from GPU thread %d\n", threadIdx.x);
}
 
int main() {
    hello<<<1, 4>>>();
    cudaDeviceSynchronize();
    printf("CUDA test done!\n");
    return 0;
}


Script sbatch corespunzător:

hello_cuda.sh
#!/bin/bash
#SBATCH --partition=ucsx
#SBATCH --gres=gpu:1
module load libraries/cuda-13.0
nvcc -o hello_cuda hello_cuda.cu
./hello_cuda

Submitere:

sbatch hello_pthreads.sh    # lansare job CPU
sbatch hello_cuda.sh        # lansare job GPU

Pentru programul Pthreads, partiția implicită (haswell) este suficientă. Pentru programul CUDA, trebuie specificată o partiție cu GPU (de exemplu ucsx).

Opțiuni avansate SLURM (click pentru detalii)

Opțiuni avansate SLURM (click pentru detalii)

SLURM oferă un control mult mai granular asupra resurselor hardware decât simpla specificare a numărului de CPU-uri sau GPU-uri. De exemplu:

  • distribuția proceselor MPI pe socket-uri (--ntasks-per-socket);
  • memory binding NUMA — procesele alocă și accesează memorie doar din nodul NUMA local, reducând latența (--cpu-bind, --mem-bind);
  • activarea sau dezactivarea SMT (--hint=nomultithread).

Aceste opțiuni devin relevante când scrieți aplicații hibride MPI+OpenMP+CUDA și doriți să vă folosiți la maximum de arhitectura hardware a nodului de calcul.


Comenzi uzuale SLURM

# === Submitere și rulare ===
sbatch hello_cuda.sh                             # submitere job; returnează un job_id
sbatch --partition=ucsx hello_cuda.sh            # override partiție din linia de comandă
srun --partition=ucsx --gres=gpu:1 ./hello_cuda  # rulare interactivă simplă
srun --partition=ucsx --gres=gpu:1 --pty bash    # sesiune interactivă pe nodul de calcul
srun --partition=haswell -w haswell-wnxx --pty bash # rulare interactivă simplă pe un anumit nod (xx se inlocuieste cu un numar anume de sistem) de pe partitia haswell
 
# === Informații despre cluster ===
sinfo                                              # afișare simplă a partițiilor
sinfo -o "%20P %6a %8D %8c %10m %20G %10l %8t %N"  # format detaliat
sinfo -o '%9P %4c %8z %8X %8Y %8Z'                 # distribuție sockets/cores/threads per partiție
 
# === Monitorizare joburi ===
squeue                          # afișare toate joburile
squeue --me                     # afișare doar joburile tale
squeue -p ucsx --state=R --format="%.8i %.10P %.15u %.10T %.10M %.8C %.10m %.5b %.20R"  # joburi active pe o partiție
 
# === Oprire joburi ===
scancel <job_id>                # oprește un job specific
scancel --me                    # oprește TOATE joburile tale
 
# === Detalii despre resurse ===
scontrol show partition <nume>  # detalii complete despre o partiție
scontrol show node <nume>       # detalii despre un nod specific
scontrol show job <job_id>      # detalii și stare completă a unui job
 
# === Account și limite ===
sacctmgr show user $USER withassoc format=user%20,account,partition,MaxWallDurationPerJob

Exerciții

  • Creați-vă un document în care să vă treceți toate informațiile pe care le-ați putut obține despre cluster (ex: arhitecturi existente, permisiuni de rulare a joburilor, etc.). Exemple de întrebări din partea asistenților de laborator:
    • Care sunt partițiile existente pe cluster? Cu ce diferă una față de alta? Pe ce partiții aveți acces? (hint: scontrol show partition)
    • Rulați pe un nod CPU comanda lscpu și pe un nod GPU comanda nvidia-smi -q. Ce informații ați putut extrage despre CPU/GPU? Comparați cu datele din tabelele de mai sus.
    • Cât RAM e disponibil per nod vs. maximul permis per job? (hint: scontrol show partition <nume>)
    • Cum vedeți joburile care se execută în prezent pe cluster? Cum opriți un job specific?
    • Cum puteti rula un job (interactiv sau nu) pe un anumit nod din cluster (Hint, explorati optiunea -w de la srun)
  • Rulați cel puțin un program C multi-threaded și un program CUDA, atât cu sbatch, cât și cu srun. Verificați rezultatele. Care este diferența dintre cele două comenzi?
  • Scrieți un program C multi-threaded care face calcule intensive (de exemplu suma unei serii pe fiecare thread). Rulați-l o dată cu --cpus-per-task=N și o dată fără --cpus-per-task=N, unde N este numărul de thread-uri din program. Comparați timpii de execuție (hint: time ./program) și explicați diferența.

Resurse utile

app/laboratoare/00.txt · Last modified: 2026/10/01 00:47 by tudor.calafeteanu
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