De Kaart: Een overzicht van PyTorch

Dit is Deel 0: de kaart. We gaan eerst snel door alle lagen heen, tekenen vervolgens het terrein en bespreken twaalf kernideeën die de codebase voorspelbaar maken. Tot slot leggen we uit hoe deze serie werkt en hoe je deze het beste kunt lezen.

Opmerking: Alle getallen in deze serie zijn gemeten met een Apple M3 Max laptop met torch 2.11.0. Jouw eigen resultaten kunnen verschillen, maar de patronen zullen hetzelfde zijn.

De afdaling

PyTorch is diep. Tussen je toetsenbord en de chip zitten acht niveaus (of "verdiepingen").

Verdieping 1: Python

Een functie als torch.randn lijkt een Python-functie, maar als je het type controleert (type(torch.randn)), zie je dat het een <class 'builtinfunctionor_method'> is. Dit betekent dat de functie is geschreven in gecompileerde code; er is geen Python-body om te lezen en geen regel waar een debugger kan stoppen.

Deze machine-instructies bevinden zich in shared libraries (gedeelde bibliotheken) binnen het torch-pakket op je schijf:

  • _C.cpython-312-darwin.so: De loader (ca. 49 KB).
  • libtorch_cpu.dylib: Tensors en kernels (ca. 206,5 MB).
  • libtorch_python.dylib: De Python-zijde van de grens (ca. 28,5 MB).

Het deel van PyTorch dat zichtbaar is vanuit Python is slechts een klein bestandje; het echte lichaam bestaat uit ongeveer 235 MB aan gecompileerde code.

De grens

Zodra je een functie aanroept, verlaat de aanroep direct Python en landt deze in een C++-functie (bijv. THPVariable_randn). Opvallend is dat veel van deze functies niet fysiek in de PyTorch-repository staan; ze worden tijdens het build-proces automatisch gegenereerd.

Het oversteken van deze grens kost tijd. Een optelling van één enkel element kost ongeveer 0,54 microseconden. Dit lijkt weinig, maar het betekent dat Python maximaal zo'n 1,9 miljoen operaties per seconde kan versturen. Aangezien een trainingsstap duizenden operaties bevat, is dit een belangrijke factor.

De dispatcher

Onder de grens komt de aanroep terecht bij de dispatcher. Dit is de router die bepaalt welke stukken code in welke volgorde moeten draaien. De dispatcher beslist bijvoorbeeld of een matrixvermenigvuldiging moet worden onthouden voor backward().

Elke operatie gaat door een vaste stapel lagen (zoals Autograd voor gradiënten of Mixed Precision). Alleen de actieve lagen beïnvloeden de aanroep; de rest laat de aanroep ongewijzigd passeren.

De kernel

Onderaan de stapel wordt een concrete functie gekozen: de kernel. Een naam als addmm (matrixvermenigvuldiging) heeft verschillende implementaties (kernels) afhankelijk van het apparaat (CPU, GPU), het datatype en of tensors dense of sparse zijn. De dispatcher kiest precies één kernel per aanroep.

Geheugen

Een laag onder de kernels bevindt zich het geheugen. Terwijl een CPU-allocatie gewoon is, gebruikt PyTorch op een GPU een eigen allocator. Deze vraagt grote blokken geheugen aan bij de GPU-driver en hergebruikt deze, omdat het constant communiceren met de driver te traag zou zijn.

De twee klokken

Op een GPU voert je Python-regel niet direct het werk uit. Python verzoekt om het werk, en dat verzoek keert onmiddellijk terug terwijl de GPU het werk op zijn eigen klok uitvoert.

Dit betekent dat Python vaak voorloopt op de GPU. Dit is waarom "eager" PyTorch snel genoeg is: de GPU hoeft nooit te wachten op Python. Echter, zodra je een waarde nodig hebt in Python (bijvoorbeeld via .item()), moet Python stoppen en wachten tot de volledige wachtrij van operaties op de GPU is afgewerkt.

De ommekeer: Autograd

Wanneer loss.backward() wordt aangeroepen, moet PyTorch weten wat er gedifferentieerd moet worden. Dit is mogelijk omdat de forward pass een tweede taak had: elke keer dat een operatie door de autograd-laag van de dispatcher ging, werd er een record geschreven.

Deze records vormen een graaf. backward() doet niets anders dan deze graaf teruglopen van de loss naar de inputs, waarbij elke geregistreerde afgeleide wordt uitgevoerd en resultaten in .grad worden opgeslagen. Omdat een afgeleide vaak de oorspronkelijke waarden nodig heeft, bewaart PyTorch deze tijdens de forward pass, wat de reden is waarom training zoveel geheugen kost.

---

Het terrein

PyTorch is opgebouwd in lagen waarbij elke laag alleen communiceert met zijn buren.

De repository-structuur

Als je in de codebase kijkt, kun je de volgende kaart aanhouden:

  • torch/: Pure Python; leesbaar voor iedereen.
  • aten/ en c10/: C++; hier leven de tensors, kernels en de dispatcher.
  • torch/csrc/: De brug die Python en C++ verbindt.
  • torchgen/: Het programma dat tijdens het build-proces code genereert (zoals de functies bij de grens).

Het ecosysteem

Bibliotheken koppelen zich op specifieke plekken aan PyTorch:

KoppelingBibliothekenWat ze doen / brengen
nn.Moduletransformers, diffusers, timmModellen zijn Modules; torch voert ze uit
Training looplightning, accelerateTorch blijft de engine; zij sturen deze aan
Distributed enginedeepspeedVervangt de engine, brengt ZeRO
Eager runtimevllm, sglang, TensorRT-LLMBehouden gewichten, vervangen de runtime met eigen kernels
Operatie-lijstflash-attention, torchvision opsVoegen nieuwe namen toe aan de lijst
Meerdere lagenunslotTraint via transformers, brengt eigen Triton kernels
Alleen gewichtenTEI, llama.cpp, MLXHebben PyTorch verlaten, behouden alleen de gewichten

---

De twaalf ideeën

Deze twaalf principes maken de codebase voorspelbaar:

  1. Een tensor is een venster over storage: Een tensor bevat geen getallen, maar een beschrijving (pointer, grootte en strides) van waar in een plat blok geheugen gekeken moet worden.
  2. Autograd registreert een programma dat je nooit schreef: Elke wijziging aan een tensor wordt als een record in een graaf opgeslagen, zodat de geschiedenis bewaard blijft voor backward().
  3. Eén lijst met operaties is de gehele interface: Er is een centrale lijst met geregistreerde namen (ca. 3.677). Alles (compilers, quantisatie) draait om wat er met deze lijst gebeurt.
  4. PyTorch schrijft het meeste van zijn eigen code: Via native_functions.yaml en derivatives.yaml genereert PyTorch tijdens het build-proces automatisch grote delen van de C++ en Python bindingen.
  5. Features zijn lagen met een schakelaar: Functionaliteiten zoals torch.no_grad() zijn simpelweg lagen in de dispatcher die aan- of uitgeschakeld kunnen worden via context managers.
  6. Elke operatie betaalt eerst een vaste kostprijs: De overhead van het oversteken van de grens en routing verklaart waarom torch.compile en fused optimizers nodig zijn.
  7. Geheugen, niet snelheid, is wat training-runs beëindigt: Het opslaan van waarden voor de backward pass is de primaire oorzaak van "Out of Memory" errors.
  8. Python is waarom het won, en wat het kost: De flexibiliteit van Python zorgde voor succes, maar introduceert de grens-kosten per operatie.
  9. Forward bepaalt wat backward moet doen: In distributed training bepaalt de manier waarop tensors in de forward pass worden gesplitst, wie er in de backward pass met elkaar moet communiceren.
  10. Gedeelde bytes plus in-place writes veroorzaken de moeilijkste problemen: Omdat meerdere tensors naar dezelfde data kunnen kijken, kan één in-place wijziging onverwachte effecten hebben op de hele graaf.
  11. De code bewaart zijn geschiedenis: De repository bevat resten van alle tijdperken (Caffe2, TorchScript), wat sommige bestanden vreemd maakt.
  12. Floating point is een contract; lees het: Precisiebeperkingen in float32 (en lager) betekenen dat kleine getallen simpelweg verdwijnen bij optelling met grote getallen.

---

Hoe deze serie te lezen

De serie bestaat uit twaalf delen:

  • 0. De Kaart: Overzicht (hier ben je).
  • 1. Tensor: Storage, strides, views, datatypes, broadcasting.
  • 2. Autograd: De graaf, in-place writes, checkpointing.
  • 3. Daily PyTorch: nn, optim, data loading, mixed precision.
  • 4. Seeing PyTorch: Profiler, geheugen, floating point.
  • 5. The Machinery: Dispatcher, aten, torchgen, historie.
  • 6. Extending PyTorch: Subclasses, custom operations, backends.
  • 7. The Compiler: Dynamo, AOT Autograd, Inductor.
  • 8. Kernels & Hardware: GPU model, Triton, Cutlass.
  • 9. Distributed: Collectives, DDP, FSDP, parallel training.
  • 10. Ship It: Export, quantisatie, Executorch.
  • 11. Working on PyTorch: Gids voor bijdragers.

---

Zelf testen en oefenen

Controleer je kennis

Na het lezen van deze pagina zou je in je eigen woorden moeten kunnen uitleggen:

  • Wat type(torch.randn) teruggeeft en waar de gecompileerde code op je schijf staat.
  • Wat de dispatcher is en hoe no_grad autograd stopt zonder functies aan te passen.
  • Wat een kernel is en wat bepaalt welke er wordt uitgevoerd.
  • Waarom CPU en GPU op twee verschillende klokken draaien en waarom dit timing-metingen bemoeilijkt.
  • Wat de graaf is, wie deze schrijft en waarom backward alleen kan doen wat forward heeft vastgelegd.

Oefeningen

Probeer de volgende scripts uit om de theorie in de praktijk te zien:

  • p0thelibrary.py: Controleer de grootte van de gecompileerde libraries op jouw systeem.
  • p1graphchain.py: Bekijk welke graaf overblijft na het draaien van een model.
  • p2dispatchcost.py: Meet de vaste kosten per operatie op jouw machine.
  • p3twotimelines.py: Ontdek hoe lang de GPU doorwerkt nadat Python klaar is met aanvragen.
  • p4microproofs.py: Een verzameling experimenten die de twaalf ideeën illustreren.

Volgende stap: De Tensor

De serie begint bij de basis: de tensor. Waarom weigert v.t().view(-1) terwijl v.t().reshape(-1) wel werkt, ook al gaat het om dezelfde data? Het antwoord op die vraag vormt de kern van Deel 1.