Hoofdstuk 12 · van speelgoednetwerk naar taalmodel
Wat staat er in een GGUF-bestand?
Je downloadt een bestand van 379 megabyte, iemand noemt het "een taalmodel", en als je het in een teksteditor opent zie je alleen brol. Dit hoofdstuk pelt zo'n bestand laag voor laag af — met de echte bytes uit een echt model.
In één alinea
Een GGUF-bestand is een enorme stapel getallen, met vooraan een etiket dat vertelt wat die getallen betekenen. Meer is het niet.
In Deel I bouwden we netwerken die we volledig konden overzien: 329 parameters voor Optelnet, ruim tweeduizend voor RekenNet. Het bestand dat we nu openmaken bevat er 494 miljoen — maar het is nog steeds precies hetzelfde soort ding: gewichten in roosters.
Het etiket is klein: 1,5 % van het bestand. De overige 98,5 % zijn 494 miljoen getallen, samengeperst tot gemiddeld 6,35 bit per stuk. Die getallen zijn de gewichten: de sterkte van elke verbinding in het netwerk. Wat er verder in dit hoofdstuk staat, is vooral uitleg bij die twee zinnen.
Deel 01Eerst: wat is een "gewicht"?
Deze vraag moet eerst beantwoord worden, want zonder dat blijft de rest van het bestand betekenisloos. En het antwoord is eenvoudiger dan de meeste uitleg doet vermoeden.
Een neuraal netwerk doet in wezen één ding: het neemt een rijtje getallen en maakt daar een ander rijtje getallen van. Dat herhaalt het een aantal keer. Alles wat "kunstmatige intelligentie" heet, is opgebouwd uit die ene bewerking.
Neem het allerkleinste voorbeeld: drie getallen in, twee getallen uit. Elk ingangsgetal heeft invloed op elk uitgangsgetal, dus er zijn zes verbindingen. En bij elke verbinding hoort één getal dat zegt hoe sterk die invloed is. Dat getal is een gewicht.
Rechts staat de sleutel tot dit hele hoofdstuk. Zodra je de zes gewichten in een rooster zet, heb je de tekening niet meer nodig. De rijen zeggen waar een verbinding vandaan komt, de kolommen waar ze naartoe gaat, en het getal in het vakje is de sterkte. Zo'n rooster heet een tensor.
Het belangrijkste inzicht van dit hoofdstuk
De verbindingen staan niet in het bestand. Nergens staat een lijst van "ingang 1 is verbonden met uitgang 2". Dat hoeft ook niet, want alles is met alles verbonden. Als je weet dat er drie ingangen en twee uitgangen zijn, weet je dat er precies zes verbindingen zijn, in een vaste volgorde.
Daarom kan een modelbestand niets anders zijn dan een lange rij getallen. De structuur zit in de vorm van de roosters, niet in het bestand als aparte informatie.
En dan schaal je het op
Het voorbeeld hierboven heeft zes gewichten. Het model dat we in dit hoofdstuk opendoen heeft er 494 miljoen, verdeeld over 290 roosters. Eén enkel rooster daarin, dat de aandachtsberekening van laag 7 doet, is 896 bij 896 — dat zijn 802 816 getallen, voor dat ene rooster alleen.
Er verandert conceptueel niets. Het is exact dezelfde bewerking: neem elk ingangsgetal, vermenigvuldig het met het gewicht van zijn verbinding, tel alles op. Alleen 134 000 keer zo veel.
Waarom er nergens een "netwerktekening" in zit
Een GGUF-bestand vertelt niet in welke volgorde de roosters gebruikt moeten worden. Er staat alleen general.architecture = "qwen2", en het programma dat het bestand leest weet wat een qwen2 is: eerst normaliseren, dan de drie aandachtsroosters, dan roteren, enzovoort. Die volgorde zit in het programma, niet in het bestand.
Het bestand is dus geen bouwtekening. Het is een doos onderdelen met een etiket erop dat zegt welk model het is.
Deel 02Waarom een eigen bestandsformaat nodig is
Als een model alleen maar getallen is, waarom zet je die dan niet gewoon achter elkaar in een bestand?
Omdat je er dan niets mee kan. Wie de getallen wil gebruiken moet ook weten:
- Hoe ze verdeeld zijn. 494 miljoen getallen op een rij is nutteloos. Je moet weten waar rooster 1 eindigt en rooster 2 begint, en welke vorm elk rooster heeft.
- Welk rooster welk is. Het rooster voor de aandacht van laag 7 doet iets heel anders dan dat voor de feed-forward van laag 3.
- De instellingen. Hoeveel lagen? Hoeveel aandachtskoppen? Hoe lang mag de invoer zijn?
- De woordenschat. Een taalmodel rekent niet met letters maar met tokennummers. De vertaaltabel tussen tekst en nummers moet mee in het bestand, anders is het model onbruikbaar.
GGUF is het formaat dat llama.cpp daarvoor bedacht heeft, en het is inmiddels de standaard voor modellen die je lokaal draait. De naam staat voor "GGML Universal File" — GGML is de rekenbibliotheek eronder.
Eén ontwerpkeuze is de moeite van het vermelden waard: het formaat is zo gemaakt dat een programma het bestand niet hoeft in te lezen. Het kan het bestand rechtstreeks aan het geheugen koppelen en de getallen laten staan waar ze staan. Daarom staan de gewichten achteraan, in één aaneengesloten blok, netjes uitgelijnd.
Deel 03De vier delen van het bestand
Elk GGUF-bestand heeft dezelfde opbouw. Vier stukken, netjes na elkaar, geen inhoudsopgave nodig.
| Deel | Bytes | Grootte | Wat erin staat |
|---|---|---|---|
| 1 Header | 0 – 23 | 24 | Handtekening, versie, en de twee aantallen die de rest ontsluiten. |
| 2 Metadata | 24 – 5.931.724 | 5.931.701 | 34 sleutel-waardeparen: de instellingen en de volledige tokenizer. |
| 3 Register | 5.931.725 – 5.948.223 | 16.499 | 290 ingangen van gemiddeld 57 bytes: naam, vorm, formaat, positie. |
| 4 Gewichten | 5.948.224 – einde | 391.859.712 | De 494 miljoen getallen zelf, samengeperst. |
Er is geen inhoudsopgave nodig omdat elk deel precies zegt hoe groot het volgende is. De header geeft het aantal metadata-sleutels en het aantal tensoren; elke sleutel begint met zijn eigen lengte; elke registeringang met de lengte van zijn naam. Je kan het bestand dus van voor naar achter doorlopen zonder ooit te moeten raden of terug te springen.
Deel 04De header, byte voor byte
Hier is het begin van een echt bestand. Links de positie, in het midden de bytes in hexadecimale notatie, rechts diezelfde bytes als tekst gelezen — punten waar het geen leesbaar teken is.
00000000 47 47 55 46 03 00 00 00 22 01 00 00 00 00 00 00 |GGUF....".......|
00000010 22 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 |"...............|
00000020 67 65 6E 65 72 61 6C 2E 61 72 63 68 69 74 65 63 |general.architec|
00000030 74 75 72 65 08 00 00 00 05 00 00 00 00 00 00 00 |ture............|
00000040 71 77 65 6E 32 0C 00 00 00 00 00 00 00 67 65 6E |qwen2........gen|
Daar zit het "aha" al in: als je zo'n bestand in een teksteditor opent zie je grotendeels brol, maar er staan wel degelijk leesbare stukjes tussen. Dat zijn de namen van de instellingen en de tokens van de woordenschat.
De eerste 24 bytes zijn de header. Meer dan vier gegevens staan er niet in:
| Bytes | Ruw | Betekenis |
|---|---|---|
| 0–3 | 47 47 55 46 | De letters GGUF. Een handtekening: leest een programma iets anders, dan stopt het meteen. |
| 4–7 | 03 00 00 00 | Versie 3 van het formaat. |
| 8–15 | 22 01 00 00 00 00 00 00 | 290 tensoren. (0x122 = 290) |
| 16–23 | 22 00 00 00 00 00 00 00 | 34 metadata-sleutels. (0x22 = 34) |
Waarom de getallen achterstevoren staan
Het getal 3 staat er als 03 00 00 00 en niet als 00 00 00 03. Dat is geen fout. Er zijn twee manieren om een getal van meerdere bytes op te schrijven, en GGUF kiest voor klein-endisch: het minst belangrijke stuk eerst, zoals wanneer je een datum als 14-08-2026 schrijft in plaats van 2026-08-14.
Zo lees je 290 dus als 22 01 00 00 00 00 00 00: de 22 vooraan is hexadecimaal voor 34, de 01 daarachter telt er 256 bij op, samen 290.
Deel 05De metadata: het etiket
Na de header volgen de sleutel-waardeparen. Elk paar is opgebouwd volgens hetzelfde recept: eerst de lengte van de naam, dan de naam, dan een typenummer, dan de waarde.
Hier is het allereerste paar van ons bestand, uit elkaar gehaald:
positie ruwe bytes betekenis
------- -------------------------------------- ---------------------------
24 14 00 00 00 00 00 00 00 de naam is 20 tekens lang
32 67 65 6E 65 72 61 6C 2E 61 72 ... "general.architecture"
52 08 00 00 00 waardetype 8 = tekst
56 05 00 00 00 00 00 00 00 de waarde is 5 tekens lang
64 71 77 65 6E 32 "qwen2"
samen 45 bytes
Dat is alles. Vijfenveertig bytes voor de mededeling "dit is een qwen2". Zo gaat het 34 keer door, en pas daarna begint het tensorregister.
Er zijn dertien waardetypes. Naast tekst zijn dat gehele getallen in verschillende maten, kommagetallen, ja-nee-waarden, en het type lijst — dat laatste is hoe de hele woordenschat in één sleutel past.
Wat er zoal in staat
| Sleutel | Waarde | Wat het betekent |
|---|---|---|
| general.architecture | qwen2 | Welk soort model. Bepaalt hoe het programma de roosters gebruikt. |
| general.name | Qwen2.5 0.5B Instruct | Alleen ter informatie. |
| qwen2.block_count | 24 | Het model bestaat uit 24 identiek opgebouwde lagen. |
| qwen2.embedding_length | 896 | Elk woord wordt onderweg voorgesteld als 896 getallen. |
| qwen2.attention.head_count | 14 | De aandacht wordt in 14 parallelle stukken gesplitst. |
| qwen2.attention.head_count_kv | 2 | Maar er zijn er maar 2 die eigen geheugen bijhouden; dat bespaart veel. |
| qwen2.feed_forward_length | 4864 | Binnen elke laag wordt tijdelijk uitgezet naar 4864 getallen. |
| qwen2.context_length | 32768 | Zoveel tokens kan het model maximaal tegelijk overzien. |
| tokenizer.ggml.tokens | 151 936 stuks | De volledige woordenschat, als lijst van tekstjes. |
| tokenizer.ggml.merges | 151 387 stuks | De regels waarmee tekst in tokens geknipt wordt. |
| tokenizer.chat_template | tekst | Hoe een gesprek opgemaakt moet worden voor dit model. |
Waarom de metadata toch 5,9 MB is
Alle instellingen uit de tabel hierboven samen — architectuur, lagen, koppen, contextlengte, zelfs het volledige gespreksjabloon — nemen 4 131 bytes in. Vier kilobyte.
De overige 5 927 570 bytes zijn de tokenizer: 151 936 tokentekstjes, 151 387 samenvoegregels, en een lijst van 151 936 tokensoorten. Dat is 99,93 % van de metadata. Wie een GGUF-bestand opendoet en zich afvraagt waarom het "etiket" zo zwaar weegt: het is geen etiket, het is een woordenboek.
Deel 06Het register: de inhoudsopgave van de roosters
Nu weten we hoe het model is ingesteld, maar nog niet waar de getallen staan. Daarvoor is het derde deel: 290 ingangen, één per rooster.
Elke ingang bevat vier dingen: een naam, de vorm, het opslagformaat en de positie. Meer niet.
De namen zijn geen willekeurige etiketten — ze zijn opgebouwd volgens een vast stramien, en als je dat kent kan je uit de naam alleen al afleiden wat het rooster doet.
Zo ziet laag 7 er in werkelijkheid uit — en laag 0 tot 23 zijn identiek, op het nummer na:
| Naam | Vorm | Getallen | Formaat | Rol |
|---|---|---|---|---|
| attn_norm.weight | [896] | 896 | F32 | schaalt de invoer bij |
| attn_q.weight | [896, 896] | 802.816 | Q5_0 | de vraag |
| attn_k.weight | [896, 128] | 114.688 | Q5_0 | de sleutel |
| attn_v.weight | [896, 128] | 114.688 | Q8_0 | de waarde |
| attn_output.weight | [896, 896] | 802.816 | Q5_0 | mengt de koppen terug samen |
| attn_q/k/v.bias | [896] [128] [128] | 1.152 | F32 | vaste bijtelling |
| ffn_norm.weight | [896] | 896 | F32 | schaalt opnieuw bij |
| ffn_gate.weight | [896, 4864] | 4.358.144 | Q5_0 | poort: wat mag door? |
| ffn_up.weight | [896, 4864] | 4.358.144 | Q5_0 | zet uit naar 4864 |
| ffn_down.weight | [4864, 896] | 4.358.144 | Q6_K | perst terug naar 896 |
Nu kan je de vormen zelf natrekken. attn_k.weight is 896 bij 128, want er zijn 2 sleutelkoppen van elk 64 getallen breed: 2 × 64 = 128. En attn_q.weight is 896 bij 896, want daar zijn het 14 koppen van 64: 14 × 64 = 896. Precies de getallen die in de metadata stonden.
Deel 07De gewichten, en hoe ze samengeperst zijn
We komen bij het laatste en veruit grootste deel. En bij een raadsel.
Er zijn 494 032 768 getallen. Een gewoon kommagetal in een computer neemt 4 bytes in. Dat zou dus bijna 2 gigabyte moeten zijn. Het bestand is 379 megabyte. Waar zijn die getallen gebleven?
Ze zijn gekwantiseerd: met opzet minder nauwkeurig opgeslagen. En het mooie is dat dat nauwelijks uitmaakt, om een reden die je misschien niet verwacht.
Het idee: één schaalfactor voor een groepje
Neem 32 gewichten die bij elkaar horen. Ze liggen meestal dicht bij elkaar in grootte — allemaal kleine getallen rond nul. In plaats van elk getal volledig op te schrijven, doe je dit: zoek het grootste getal in het groepje, en druk alle andere uit als een breukdeel daarvan.
Dit formaat heet Q8_0: 8 bits per gewicht, variant 0. De 43 0D vooraan is de schaalfactor in halve precisie, de 32 bytes daarna zijn de gehele getallen. Wie de gewichten wil gebruiken vermenigvuldigt gewoon terug.
Waarom dit werkt en niet tot onzin leidt
Je verliest wel degelijk nauwkeurigheid. Een gewicht van −0,0102771 wordt −0,0102768. Maar een neuraal netwerk telt honderden van die vermenigvuldigingen bij elkaar op, en die kleine afrondingen heffen elkaar grotendeels op — de ene te hoog, de andere te laag. Wat overblijft is ruis die verdwijnt in het geheel.
Dat is de eigenlijke reden dat je een model van 2 GB tot 379 MB kan indikken zonder dat het merkbaar dommer wordt.
Nog verder: 4 en 5 bits
Hetzelfde trucje werkt met kleinere getallen. Bij Q4_0 krijgt elk gewicht maar 4 bits, dus 16 mogelijke waarden, en passen er twee in één byte: 32 gewichten in 18 bytes. Bij Q5_0 zijn het er 32, in 22 bytes.
De K-varianten (Q4_K, Q6_K) zijn slimmer: ze werken met groepen van 256 en gebruiken twee niveaus van schaalfactoren, een grove voor het hele blok en fijnere per stukje van 32. Dat levert bij dezelfde grootte een merkbaar betere benadering op.
| Formaat | Groep | Bytes | Bit/gewicht | Waar het hier voor gebruikt wordt |
|---|---|---|---|---|
| F32 | 1 | 4 | 32,00 | de 121 kleine roosters: normalisaties en biassen |
| Q8_0 | 32 | 34 | 8,50 | de woordentabel en enkele gevoelige roosters |
| Q6_K | 256 | 210 | 6,56 | 12 van de 24 ffn_down-roosters |
| Q5_0 | 32 | 22 | 5,50 | 132 roosters — het merendeel |
| Q4_K | 256 | 144 | 4,50 | de andere 12 ffn_down-roosters |
Een raadsel dat zichzelf verklaart
Dit model wordt aangeboden als "Q4_K_M". Toch bestaat het grotendeels uit Q5_0, en niet uit Q4_K. Dat is geen fout.
De K-varianten werken in groepen van 256, en dus moet de rijlengte van een rooster deelbaar zijn door 256. Qwen2.5-0.5B werkt met een breedte van 896, en 896 is niet deelbaar door 256. Elk rooster dat uit die breedte leest kán dus geen Q4_K zijn en valt terug op Q5_0. Alleen ffn_down leest uit de feed-forward-breedte 4864, en die is wél deelbaar door 256 — vandaar precies 12 + 12 K-roosters voor 24 lagen.
Zulke details merk je pas als je zelf in het bestand kijkt. Ze staan in geen enkele modelbeschrijving.
Deel 08Waar zitten die 494 miljoen getallen eigenlijk?
Als je alle roosters bij elkaar optelt en groepeert, komt er iets uit dat de meeste mensen verrast.
De aandacht — het mechanisme waar de meeste uitleg over transformers over gaat, en waar de titel "Attention Is All You Need" naar verwijst — beslaat nog geen 9 % van de getallen. Bijna twee derde zit in de feed-forward-lagen, het onderdeel waar zelden over gesproken wordt.
En de woordentabel is bij dit kleine model bijna 28 %. Dat is ook waarom deze modellen vaak gebonden inbeddingen gebruiken: dezelfde tabel wordt hergebruikt aan het begin (woord naar getallen) en aan het eind (getallen naar woord). Scheelt in één klap 136 miljoen getallen. In dit bestand is dat te zien doordat er wel een token_embd.weight is maar geen output.weight.
Deel 09Zelf kijken
Alle cijfers in dit hoofdstuk komen uit één bestand dat waarschijnlijk al op je schijf staat.
De volledige broncode zit bij dit boek in de map code/ — hieronder ook als zip — en draait met één JDK (22 of nieuwer). Voer de opdrachten uit vanuit die map. De code is vrij te gebruiken onder de MIT-licentie.
Als je ooit een model met ollama hebt gedraaid, heb je GGUF-bestanden. Ollama bewaart ze zonder omhaal als gewone GGUF-blobs. Het pad vind je via het manifest:
# welke modellen staan er
ls ~/.ollama/models/manifests/registry.ollama.ai/library/
# het manifest wijst naar de blob; de laag met mediaType
# application/vnd.ollama.image.model is het GGUF-bestand
cat ~/.ollama/models/manifests/registry.ollama.ai/library/qwen2.5/0.5b
En dan kan je er het leesgereedschap uit dit project op loslaten:
java src/GgufDump.java ~/.ollama/models/blobs/sha256-c5396e06af... --tokens 10
Dat drukt precies af wat hierboven beschreven staat: de header, de 34 instellingen, alle 290 roosters met hun vorm en positie, de verdeling per formaat, en een reeks controles die nagaan of het bestand inwendig klopt.
Wil je de bytes zelf zien
Een hexdump van de eerste regels werkt op elk GGUF-bestand en is het snelste bewijs dat er niets magisch aan is:
xxd -l 96 model.gguf
Je zou nu elk teken van die uitvoer moeten kunnen verklaren.
SamenvattingHet hele bestand in zeven zinnen
- Een neuraal netwerk is een stapel getallenroosters; elk getal is de sterkte van één verbinding.
- De verbindingen zelf worden niet opgeslagen, want alles is met alles verbonden — de vorm van het rooster zegt genoeg.
- Een GGUF-bestand bestaat uit vier delen: een header van 24 bytes, de instellingen, een register van de roosters, en dan de getallen.
- De eerste drie delen samen zijn hier 1,5 % van het bestand, en daarvan is het grootste deel de woordenschat.
- De getallen zijn gekwantiseerd: per groepje van 32 of 256 wordt één schaalfactor bewaard plus een klein geheel getal per gewicht.
- Welk formaat een rooster krijgt, hangt af van de vorm: de betere K-formaten eisen een rijlengte die deelbaar is door 256.
- De volgorde waarin de roosters gebruikt moeten worden staat níét in het bestand — alleen de naam van de architectuur, waarna het programma de rest weet.