Parallellism och varför GPU:er
Kunna förklara varför matrismultiplikation parallelliseras väl och vad en GPU gör annorlunda än en CPU.
Förkunskaper
Intuition
En matrismultiplikation består av miljoner oberoende skalärprodukter. Inget resultat behöver något annat resultat — de kan alla räknas samtidigt. Det kallas pinsamt parallellt, och det är precis vad en GPU är byggd för.
| CPU | GPU | |
|---|---|---|
| Kärnor | 8–64 komplexa | 5 000–20 000 enkla |
| Optimerad för | låg latens per uppgift | hög genomströmning |
| Bra på | förgreningar, sekventiell logik | samma operation på mycket data |
| Minnesbandbredd | ~100 GB/s | 1–3 TB/s |
Bandbredden är ofta den verkliga skillnaden. Vid LLM-dekodning läses alla vikter per token — då är det minnesbandbredden, inte beräkningen, som sätter taket.
Formellt
Amdahls lag sätter gränsen för vad parallellisering kan ge. Om andelen av arbetet kan parallelliseras över enheter:
Med och oändligt många enheter är taket 20× — den seriella femprocenten dominerar. Det är därför dataladdning, förbehandling och Python-loopar i träningsloopen kan äta upp hela GPU-vinsten.
Aritmetisk intensitet (FLOPs per läst byte) avgör om en operation är beräknings- eller minnesbunden:
| Operation | Intensitet | Bunden av |
|---|---|---|
| Elementvis addition | ~0,1 | minne |
| Matris × vektor (dekodning) | ~2 | minne |
| Matris × matris (träning, stor batch) | ~100+ | beräkning |
Därför är träning med stora batchar beräkningsbunden (GPU:n utnyttjas väl) medan dekodning en token i taget är minnesbunden (GPU:n står och väntar på minnet). Det förklarar varför batching hjälper så mycket vid servering — den flyttar arbetet från minnesbundet till beräkningsbundet.
MFU (model FLOPs utilization) mäter hur stor andel av GPU:ns teoretiska kapacitet som faktiskt används. 40–50 % är bra vid träning; under 20 % betyder att något annat är flaskhalsen.
Kod
import torch, time
def mat(n=4096, upprepningar=20, device="cuda"):
a = torch.randn(n, n, device=device, dtype=torch.bfloat16)
b = torch.randn(n, n, device=device, dtype=torch.bfloat16)
for _ in range(3): # uppvärmning
a @ b
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(upprepningar):
a @ b
torch.cuda.synchronize()
sek = (time.perf_counter() - t0) / upprepningar
return {"ms": round(sek * 1000, 2), "TFLOPs": round(2 * n**3 / sek / 1e12, 1)}
print(mat(device="cpu")) # {'ms': 2100.0, 'TFLOPs': 0.07}
print(mat(device="cuda")) # {'ms': 4.6, 'TFLOPs': 29.9}
def amdahl(p, N):
return 1 / ((1 - p) + p / N)
for p in (0.5, 0.9, 0.99):
print(p, [round(amdahl(p, n), 1) for n in (2, 8, 1000)])
# 0.5 [1.3, 1.8, 2.0] ← taket är 2× oavsett antal enheter
# 0.9 [1.8, 4.7, 9.9]
# 0.99 [2.0, 7.5, 91.0]
När GPU inte hjälper: små modeller där overheaden dominerar, kraftigt förgrenad logik, dataladdning som inte hinner med (fixa med fler workers och prefetch), och all Python-kod i loopen.
Behärskning innebär
- Förklarar varför matrisoperationer parallelliseras väl
- Beskriver skillnaden mellan CPU och GPU
- Identifierar när GPU inte hjälper
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- Wikipedia — Amdahls lag (CC BY-SA 4.0) — CC BY-SA 4.0
- NVIDIA — GPU Performance Background — fri läsning