38.2 CPU, GPU és elosztott rendszer közötti döntés

Egy háromfős adattudományi csapat három párhuzamos feladaton dolgozik. Az A projekt a Northstar-kezdőadaton (300 ügyfél, 2400 rendelés, 8 ismert feature) logisztikus regressziót tanít, B projekt egymillió soros táblás adaton random forestet, C projekt pedig ötvenmillió sorból tanuló, tízmillió paraméteres neurális hálót futtat. A kérdés mindháromnál ugyanaz: helyi CPU, távoli CPU/GPU vagy elosztott rendszer kell? A hibás válasz drága: GPU-t adni egy 0,3 másodperces modellhez vagy klasztert indítani 10 MB adathoz a munkakör neve miatt, nem a mért korlát miatt. Az induló becslés: A helyi CPU, B többmagos CPU, C egy GPU; ezt a mérés pontosíthatja. A megfigyelési egység egy teljes kísérleti futás, a kimenet a futásidő, csúcsmemória, költség és reprodukálhatósági jegyzőkönyv.

38.2.1 Munkaterhelés, BLAS, kötegelés és a skálázási veszteség

A munkaterhelés (workload) típusát az uralkodó erőforrás-korlát adja. Egy futtatás idejét durva felbontással így becsülhetjük:

T≈WP+DB+L T \approx \frac{W}{P} + \frac{D}{B} + L

ahol

Ha W/PW/P dominál, a feladat számításkötött; ha D/BD/B dominál, memória- vagy I/O-kötött; ha LL a meghatározó, a párhuzamosítás overheadje a fő ellenfél. A pontos számításhoz mérés kell, de a képlet arra jó, hogy a szűk keresztmetszetet megkeressük.

A BLAS (Basic Linear Algebra Subprograms, alap lineáris algebrai szubrutinok) a mátrixműveletek szabványos építőeleme. A NumPy @ művelete jellemzően egy helyi BLAS-könyvtárhoz fordul, amely jellemzően CPU-n fut, és implementációtól függően többszálú lehet; a hagyományos NumPy-BLAS művelet nem használ automatikusan GPU-t. Az A∈ℝm×kA \in \mathbb{R}^{m \times k} és B∈ℝk×nB \in \mathbb{R}^{k \times n} szorzás közelítő műveletigénye:

F=2mnk[FLOP] F = 2mnk \quad [\text{FLOP}]

Kézi kontroll: m=k=n=500m=k=n=500 esetén F=2⋅500⋅500⋅500=2,5⋅108F = 2 \cdot 500 \cdot 500 \cdot 500 = 2{,}5 \cdot 10^8 FLOP. Ha a gép 10 GFLOP/s-ot ér el, a számítási komponens 0,0250{,}025 s; a valódi idő ennél nagyobb a memóriából olvasás és az ütemezés miatt. A BLAS tehát nem automatikusan GPU-t jelent: CPU-n is párhuzamos szálakat futtat, és kis mátrixoknál a GPU-ra másolás több időt emészthet fel, mint a számítás.

A kötegelés (batching) a gradienses tanítás alapegysége: a modell egyszerre BB mintát dolgoz fel, és egy paraméterfrissítéshez a batch gradiensét használja. nn mintánál a lépések száma:

nsteps=⌈nB⌉ n_{\text{steps}} = \left\lceil \frac{n}{B} \right\rceil

Példa: n=128n=128, B=32B=32 esetén 4 lépés, B=16B=16 esetén 8 lépés van. A nagyobb batch kevesebb lépést és több GPU-kihasználtságot adhat, de a batch aktivációit egyszerre kell a memóriában tartani, ezért a batch méretét a VRAM korlátozza.

A VRAM a grafikus kártya saját memóriája. A szükséges méret első közelítésben a paraméterektől, a batch aktivációktól és a visszaterjesztés köztes értékeitől függ. Egy 10710^7 paraméterű modell 32 bites lebegőpontos (fp32) súllyal 107⋅4B=40MB10^7 \cdot 4~\text{B} = 40~\text{MB} paramétert foglal, a gradiens további 40 MB-ot, az optimizer nyomatékai és a batch aktivációi pedig ennek többszörösét tehetik ki. Ezért a memóriaszámítást nem elég a paraméterekből megtenni: a valós csúcs a tanítási lépés közben mérhető.

Ha a rendszer több gépre oszlik, a hálózati átvitel (network) költsége lép be. Az átviteli idő:

Tnet=DBnet T_{\text{net}} = \frac{D}{B_{\text{net}}}

10 GiB adat 1 Gbit/s-os hálózaton 10⋅8Gbit/1Gbit/s=80s10 \cdot 8~\text{Gbit} / 1~\text{Gbit/s} = 80~\text{s}, 10 Gbit/s-on 8 s; ezen felül jön a protokollszintű overhead. A következtetés: akkor érdemes elosztani az adatot, ha a távoli gépek közötti átvitel és ütemezés kisebb, mint a megtakarított számítási idő.

A skálázási veszteség (scaling overhead) az a jelenség, hogy a párhuzamosításból származó gyorsulás nem nő arányosan a gépek számával, mert a feladat egy része soros marad, és az összerendezés is időt fogyaszt. Az Amdahl-törvény szerint, ha a feladat ss hányada nem párhuzamosítható:

S(N)=1s+1−sN S(N) = \frac{1}{s + \frac{1-s}{N}}

ahol NN az azonos sebességű egységek száma, SS pedig a gyorsulás dimenziótlan aránya. Példa: s=0,05s=0{,}05, N=4N=4 esetén S=1/(0,05+0,95/4)=1/0,2875≈3,48S = 1/(0{,}05 + 0{,}95/4) = 1/0{,}2875 \approx 3{,}48. A tökéletes lineáris gyorsulás 4 lenne; itt a hatékonyság 0,870{,}87. A kódban és a mérésekben ez konkrét összehasonlítási alap.

Amdahl-gyorsulás s=0,05 mellett - A pontok az S(N)=1/(0,05+0,95/N) képletből számítottak, nem mérések.

Az ábra mutatja, hogy 4 egységnél még 0,87 a hatékonyság, 8-nál már csak 0,74; a hozzáadott gépek egyre kevesebbet hoznak, miközben a hálózat és az ütemezés költsége nő. Ez a mérési varianciával együtt értelmezendő: ha a futásidő mérésének szórása 20%, akkor egy 10% gyorsulás nem igazolható megbízhatóan. Ezért minden erőforrás-döntést mért korlátból kell indítani: gépspecifikáció, adatméret, ismétlésszám, seed és pontos futási jegyzőkönyv nélkül a teljesítmény szám csak anekdota. A reprodukálhatóság különbségét is jegyezzük fel: a statisztikai bizonytalanság a mintából, nem a hardverből származik; a gyorsabb gép nem csökkenti a bootstrap-intervallum szélességét, csak több ismétlést enged.

38.2.2 Döntési lap és kis helyi mérési kontroll

A döntési lap eszközválasztás előtt kitöltendő. A három bevezető esetre:

Eset Mért input és becslés Elsődleges választás Elosztási feltétel
A: Northstar 2400 sor, 8 feature, logreg, 5×55 \times 5 CV; RAM <1< 1 GB Helyi CPU uv/Jupyter Nem osztjuk el; a futás 30 s alatt marad
B: közepes táblás 1 M sor, 60 feature, random forest 200 fa, 12 kombináció, 5-fold Többmagos CPU, 64 GB VM Csak akkor Dask, ha egy folyamat csúcs-RAM >80%> 80\% fizikai RAM
C: nagy ML 50 M sor, 10 M paraméter, batch 64, 5 epoch 1 GPU, 16 GB VRAM Több GPU akkor, ha az egykártyás batch nem fér, és a mért gyorsulás hatékonysága >0,7> 0{,}7

A táblázat az „elosztási feltétel” oszlopban explicit küszöböt ad, tehát nem elég azt mondani, hogy „ha lassú”. A >0,7> 0{,}7 hatékonyság azt jelenti: 2 GPU-val legalább 1,41{,}4-szeres mért gyorsulás az 1 GPU-hoz képest ugyanazon batch, seed és adat mellett. A 80%-os memória-küszöb azt kerüli el, hogy a swap már a mérés alatt eltorzítsa az eredményt.

A helyi mérési kontrollhoz készítsünk egy minimális BLAS-benchmarkot. A következő kódot a tanuló a saját gépen futtathatja; az alábbi kimeneti alak szemléltető, nem mért, mert a könyvírás nem hajtja végre a scriptet. A méréshez rögzíteni kell a processzormodellt, magszámot, NumPy-verziót és a futás időpontját.

# döntési_mérés.py
# Platform: bármely, ahol a projekt virtuális környezete aktív; cwd = projektgyökér.
# A mérést a tanuló futtatja; az alábbi kimenet csak alakminta.
import platform, statistics, time
import numpy as np

def measure_dot(m, k, n, reps=7, seed=20261020):
    rng = np.random.default_rng(seed)
    a = rng.normal(size=(m, k))
    b = rng.normal(size=(k, n))
    samples = []
    for _ in range(reps):
        t0 = time.perf_counter()
        c = a @ b
        _ = np.sum(c)              # a késleltetett BLAS-kimenetet felolvassuk
        samples.append(time.perf_counter() - t0)
    return statistics.median(samples), min(samples), max(samples)

for m in (100, 500, 1000):
    med, mn, mx = measure_dot(m, m, m)
    flop = 2 * m ** 3
    print(f"m={m:>4d}  median={med:.4f}s  min={mn:.4f}s  max={mx:.4f}s  "
          f"flop={flop:.3e}  gflops={flop/med/1e9:.1f}")

# Szemléltető várt kimeneti alak (nem mért):
# m= 100  median=0.0012s  min=0.0009s  max=0.0028s  flop=2.000e+06  gflops=1.7
# m= 500  median=0.0210s  min=0.0190s  max=0.0400s  flop=2.500e+08  gflops=11.9
# m=1000  median=0.1500s  min=0.1400s  max=0.2900s  flop=2.000e+09  gflops=13.3

A mérés értelmezése nem a pontos futásidő, hanem a mérési variancia és a skálázódás. Ha a medián idő m=100m=100-nál 0,001 s, m=1000m=1000-nél 0,15 s, akkor a munka közelítőleg a várt m3m^3-ös növekedést követi; a min–max tartomány 2–3-szoros szórása viszont arra figyelmeztet, hogy 10%-os különbségekből hardverdöntést levonni nem szabad. A GFLOP/s érték az elméleti műveletszám és a mért idő hányadosa; ellenőrző invariant, hogy a műveletszám pontosan 2m32m^3, a mért idő pedig pozitív valós szám.

A döntési lapot ezután így töltjük ki. A projekt A mérési futása a starter_experiment.py scripttel ellenőrizhető; ha 10 ismétlés mediánja 5 s alatt van, a CPU-döntés stabil. A B projektnél a random forest tanítása a faillesztés miatt sok elágazást és memória-keresést végez, ezért a szűk keresztmetszet a CPU, nem a BLAS; a GPU itt nem segít. A C projektnél a batch B=64B=64, a modell 40 MB paraméter + 40 MB gradiens + optimizer, így 16 GB VRAM-nál a korlát az aktivációk; ha az egy batch csúcsmemóriája eléri a 12 GB-ot, akkor kell batch-csökkentés vagy nagyobb kártya; ha a futási idő 1 GPU-n 8 óra, akkor a párhuzamosítás előtti mért overhead kell.

38.2.3 Négy feladat

  1. Értelmezés. A B projektnél (n=106n=10^6 sor, p=60p=60 feature, random forest 200 fa, max_depth=15) miért nem elsődleges a GPU? Nevezd meg a munka jellegét, és adj egy elérhető mérési jelet, amely megerősíti a döntést.
  2. Megvalósítás. Írj Python-kódot, amely a fenti measure_dot függvénnyel m=200,400,800m=200, 400, 800-ra kiszámítja a medián időt és a GFLOP/s-ot, majd kiírja, hogy a mátrixszorzás ideje monoton nő-e. A seed legyen 20261020, az ismétlésszám 9.
  3. Hibadiagnózis. Egy távoli VM-en a C projekt futásnaplója szerint az idő 80%-a adatbetöltés, a CPU 12%, a GPU 95% rövid tüskékben. Mi a szűk keresztmetszet, és milyen két konkrét mérést kérsz, mielőtt nagyobb GPU-t vásárolnál?
  4. Transzfer. Az A projekt adatmérete 100-szorosra nő: 240 000 sor, továbbra is 8 feature és logreg, de a 5×55 \times 5 CV együtt 40 percig fut. Számold ki a sorok szerinti Dask-szétosztás potenciális gyorsulását, ha a feladat 90%-a párhuzamosítható, 4 workerrel dolgozol, és az ütemezés plusz 10% overheadet ad. Mikor érdemes mégis elosztani?

38.2.4 Megoldások és indoklás

1. megoldás. A random forest faillesztése során a rendezések, hisztogramok és elágazások sok apró, adatfüggő memória-hozzáférést végeznek, nem pedig hosszú mátrixszorzásokat. A scikit-learn implementáció CPU-n futtat; a GPU-ra portolt változatok sem feltétlenül gyorsabbak ezen a feladaton. Mérési jel: a CPU magok kihasználtsága közel 100% a tanítás alatt, a GPU kihasználtsága viszont nulla vagy oszcilláló, míg a futásidő nem csökken GPU-hozzárendeléssel. Ellenőrző invariant: a várt munka nagyságrendje ntrees⋅n⋅log(n)n_{\text{trees}} \cdot n \cdot \log(n) jellegű, ami CPU-ütemezéssel jobban szabályozható.

2. megoldás.

# mérés_bővített.py, platform: helyi virtuális környezet, cwd = projektgyökér
import statistics, time
import numpy as np

def measure_dot(m, k, n, reps=9, seed=20261020):
    rng = np.random.default_rng(seed)
    a = rng.normal(size=(m, k))
    b = rng.normal(size=(k, n))
    samples = []
    for _ in range(reps):
        t0 = time.perf_counter()
        c = a @ b
        _ = np.sum(c)
        samples.append(time.perf_counter() - t0)
    return statistics.median(samples)

prev = None
for m in (200, 400, 800):
    med = measure_dot(m, m, m)
    flop = 2 * m ** 3
    print(f"m={m:>3d}  median={med:.4f}s  gflops={flop/med/1e9:.1f}  "
          f"monotone={prev is None or med > prev}")
    prev = med

A kimenetet a tanuló gépe határozza meg; a kód az idő monoton növekedését logikai értékként írja ki. Ellenőrzés: a FLOP-szám 2m32m^3, az egyes GFLOP/s-értékek toleranciája a mérés természetes varianciája miatt 10–30%; ha m=800m=800-nál a GFLOP/s lényegesen magasabb, a cache- vagy BLAS-hatás miatt a mérési kontrollt újra kell futtatni.

3. megoldás. A napló I/O-kötöttséget mutat: a GPU a számítást rövid tüskékben elvégzi, majd vár az adatra, a CPU pedig nem számol. A nagyobb GPU nem segít, mert a betöltés nem gyorsul. Két mérés a döntés előtt:

4. megoldás. A sorok szerinti párhuzamosítás Amdahl-gyorsulása s=0,10s=0{,}10, N=4N=4: S=1/(0,10+0,90/4)=1/0,325≈3,08S=1/(0{,}10 + 0{,}90/4) = 1/0{,}325 \approx 3{,}08. Ha ehhez 10% ütemezési overhead adódik, a hasznos gyorsulás 3,08/1,10≈2,83{,}08/1{,}10 \approx 2{,}8-szoros, tehát 40 percből körülbelül 14–17 perc lehet. Ez azonban csak akkor igaz, ha a foldok sorai függetlenek, a partíciók kiegyenlítettek, és a kommunikáció minimális. Elosztani akkor érdemes, ha a 240 000 soros adat egyetlen gépen már nem fér a RAM-ba, vagy ha a futási idő költségkeretet sért; előbb azonban olcsóbb a foldok számának csökkentése vagy a feature-műveletek vektorizálása, mert 240 000 ×\times 8 float64 alig 15 MB.

38.2.5 Gyakori tévedés, visszautalás és önellenőrzés

A leggyakoribb tévedés: „data scientist vagyok, tehát GPU és klaszter kell.” Ellenpélda: a Northstar 2400 soros logisztikus regresszióját GPU-ra ütemezve a CUDA-kontextus indítása és az adat másolása gyakran hosszabb, mint a teljes helyi CPU-s tanítás; a klaszter pedig 10 MB adaton a schedulerindítás miatt lassabb. A munkakör neve nem mért korlát.

Korábbi fogalmak visszahívása: a Dask task gráfnál tanultuk, hogy a sok apró task overheadje nagyobb lehet a hasznos munkánál; ugyanez igaz a GPU-ra küldött apró batch-ekre. A kalibrációs és predikciós bizonytalanságnál láttuk, hogy az intervallum szélessége a minta- és modellbizonytalanságból ered, nem a hardverből; gyorsabb gép csak több szimulációt enged, nem csökkenti a standard hibát. Az R-beli OLS-nél Tábla és formula R-ben is a minta elemszáma határozta meg a standard error-t, nem a futtatógép.

Önellenőrzés. Számold ki: 32 MB adat 1 Gbit/s hálózaton hány másodperc? (32⋅8/109=0,25632 \cdot 8/10^9 = 0{,}256 s plusz késleltetés.) Mennyi az Amdahl-gyorsulás s=0,20s=0{,}20, N=8N=8 esetén? (S=1/(0,20+0,80/8)=1/0,30≈3,33S = 1/(0{,}20+0{,}80/8) = 1/0{,}30 \approx 3{,}33; a nyolc gépből háromnál kevesebb hasznosul.) Ha a mért mátrixszorzási idő m=1000m=1000-re 0,15 s, mekkora a GFLOP/s? (2⋅109/0,15⋅10−9≈13,32 \cdot 10^9/0{,}15 \cdot 10^{-9} \approx 13{,}3 GFLOP/s; ha a gép csúcsértéke 100 GFLOP/s, akkor nem számításkötött a BLAS, hanem memória-korlátos a művelet.)

38.2.6 Rövid összefoglaló

A CPU, GPU és elosztott rendszer közötti választás mért szűk keresztmetszetből indul: műveletigény, memória, I/O és ütemezési overhead. A BLAS a mátrixműveleteket gyorsítja, a batching a gradienses tanítás egysége, a VRAM a batch és aktiváció korlátja, a hálózati átvitel és az Amdahl-veszteség pedig az elosztás valódi árát mutatja. A három eset döntési lapja: kicsi Northstar helyi CPU, egymillió soros random forest többmagos CPU, ötvenmillió soros mély modell egy GPU; a több GPU-ra és klaszterre csak explicit, mérhető feltétellel lépünk. A következő Eszközbevezetés, licenc és verziófrissítés alfejezetben az eszközbevezetés, licenc és verziófrissítés döntési keretét tárgyaljuk.