Hoofdstuk 16 · naschrift
De proef: Qwen3.8
Op 15 augustus 2026 — twee dagen voor dit naschrift — bracht Alibaba Qwen3.8-27B uit, het eerste open model van een nieuwe generatie. De vraag lag voor de hand: wat doet onze eigen toolchain, gebouwd rond één bescheiden qwen2-model, met een gloednieuw model van een totaal ander kaliber? Dit hoofdstuk is het eerlijke antwoord: twee derde van de pijplijn werkte binnen een dag, en het laatste derde weigert — luid, en om precies de goede reden.
Deel 01 Wat er op 15 augustus verscheen
De Qwen3.8-familie telt twee open modellen. Het vlaggenschip Qwen3.8-2.4T-A95B is een mixture-of-experts van 2,4 biljoen parameters (512 experts, waarvan er per token 10 plus één gedeelde meerekenen — 95 miljard "actieve" parameters). Daarnaast is er Qwen3.8-27B: een dicht model van zo'n 28 miljard parameters, onder Apache 2.0, dat verrassend genoeg ook beelden kan lezen. Alibaba rapporteert er indrukwekkende cijfers bij (SWE-bench Pro 61,7, LiveCodeBench v6 90,3, GPQA Diamond 89,2) — leverancierscijfers, nog zonder onafhankelijke replicatie, maar duidelijk is dat dit geen speelgoedmodel is.
Voor dit boek is één eigenschap belangrijker dan alle benchmarks: de 27B is geen gewone transformer meer. Van de 64 lagen gebruiken er 48 Gated DeltaNet — "lineaire aandacht" — en maar 16 de klassieke aandacht uit hoofdstuk 14, in een vast patroon van drie-om-één. Daarover meer in deel 07; eerst een praktischer obstakel.
Deel 02 Het gewicht
Het draagbare werkpaard van dit boek is 0,5 miljard parameters groot: 379 MiB op schijf. Qwen3.8-27B is er ruwweg vijftig keer zwaarder, en dat is meteen de hele analyse:
| quantisatie | bestand | bruikbaar op deze machine? |
|---|---|---|
| UD-IQ2_XXS (2-bit) | 9,0 GB | nee — groter dan het volledige RAM |
| Q3_K_M | 13,8 GB | nee |
| Q4_K_M (aanbevolen) | 17,1 GB | nee |
| Q8_0 | 29 GB | nee |
| BF16 | 54,7 GB | nee |
De testmachine van dit boek heeft 8 GB unified memory. Omdat het model dicht is, raakt elke gegenereerde token álle gewichten aan — er valt niets te ontwijken. Zelfs de zwaarst gecomprimeerde variant past dus niet: het besturingssysteem zou per token gigabytes van schijf moeten wisselen. De gemeenschap noemt 24 GB als praktisch minimum, en dat geldt niet alleen voor onze Java-code: ook ollama en llama.cpp kunnen er op deze machine niets mee. Draaien is hier geen kwestie van optimaliseren maar van natuurkunde.
Deel 03 De omweg: een modelbestand met vier parameters
Wat valt er dan wél te toetsen? Twee van de drie lagen van de pijplijn hebben helemaal geen gewichten nodig: de GGUF-lader (hoofdstuk 12) en de tokenizer (hoofdstuk 13). Alibaba publiceert de volledige tokenizer als los bestand van 12,8 MB — tokenizer.json — en het gesprekssjabloon als tekstbestand. Alleen: onze toolchain leest geen tokenizer.json, die leest GGUF.
Hoofdstuk 12 beschreef het GGUF-formaat byte voor byte — nauwkeurig genoeg om het niet alleen te lezen maar ook te schrijven. Een hulpscript van tachtig regels verpakt de woordenschat, de samenvoegregels, de tokentypes en het chatsjabloon in een vers GGUF v3-bestand, met één dummytensor van vier getallen zodat het datablok reglementair is. De rollen zijn daarmee omgedraaid: de Python-schrijver is de leerling, en onze Java-lader — met al zijn uitlijnings- en overlapcontroles uit hoofdstuk 12 — is de examinator die het bestand moet goedkeuren:
$ java src/GgufDump.java qwen38/qwen38-tokenizer.gguf --brief
architectuur qwen35
naam Qwen3.8-27B tokenizer-pakket (zonder gewichten)
woordenschat 248077
tokenizer gpt2
tensoren 1
parameters 4
gewichten samen 16 B
…
== CONTROLES =====================================================
[ok] register, uitlijning en bestandsgrootte kloppen onderling
[FOUT] ontbrekende tensor: token_embd.weight
[FOUT] ontbrekende tensor: output_norm.weight
De structuurkeuring slaagt, en de twee [FOUT]-regels zijn geen smet maar de lader die eerlijk benoemt wat hier bewust ontbreekt: de gewichtstensoren van een echt model. Een taalmodel van zestien bytes dus — genereren kan het niets, maar voor de tokenizer- en sjabloontoetsen is het exact gelijkwaardig aan het bestand van 17 GB, want die metadata is identiek.
Deel 04 Wat de tokenizer moest leren
De architectuurstring in het bestand is qwen35, en dat geldt ook voor de voorsplitser: tokenizer.ggml.pre = "qwen35". Onze tokenizer weigert onbekende voorsplitsers (de les van hoofdstuk 13: raden geeft geen foutmelding, alleen stil verkeerde nummers), dus er moest een nieuwe variant bij. Het verschil met qwen2 is opnieuw subtiel: in de reguliere uitdrukking is \p{L}+ veranderd in [\p{L}\p{M}]+, en spiegelbeeldig is \p{M} aan de uitsluitingsklasse van de leestekensbrok toegevoegd. Die \p{M} zijn combinerende tekens — losse accenten, Vietnamese toontekens, de klinkertekens van het Devanagari. Bij qwen2 werden die van hun letter afgeknipt alsof het leestekens waren; bij qwen3.8 blijven ze erbij. Voor teksten als "tiếng Việt" of "नमस्ते" scheelt dat wezenlijk in de tokenverdeling.
Eén regel regex dus — en toen de externe poort (deel 06) voor het eerst draaide, bleven er 17 teksten afwijkend. Allemaal met hetzelfde patroon:
[!=] "café café naïef naïef" ← twee spellingen: voorgevormd én los accent
wij : [895, 56868, 39579, 52033, ...]
ref : [895, 56868, 50203, 91603, ...]
De referentie maakte van "e + los accent" één voorgevormde "é" vóór het tokeniseren; wij niet. Dat is NFC-normalisatie, en qwen3.8's tokenizer schrijft haar expliciet voor. Java heeft haar ingebouwd (java.text.Normalizer), dus de reparatie was één regel — maar alleen voor de qwen35-variant. De qwen2-tokenizer blijft bewust ongenormaliseerd: die is byte-voor-byte bewezen tegen llama.cpp, dat evenmin normaliseert, en een bewezen gedrag verander je niet omdat een ander model iets anders wil.
Deel 05 De 201 spooktokens
De zelftest van hoofdstuk 13 gebruikt de woordenschat tegen zichzelf: codeer de tekst van elk token, en eis dat er precies dat ene token uitkomt. Op qwen2.5 verklaarde die toets alle 151 936 tokens restloos. Op qwen3.8 bleven er 201 onverklaard — allemaal Chinees, en stuk voor stuk viel de tekst uiteen in losse tekens:
"俱乐部" → [俱, 乐, 部] (club — staat wél als één geheel in de woordenschat)
"毛泽东" → [毛, 泽, 东] (Mao Zedong)
"加拿大" → [加, 拿, 大] (Canada)
Fout in onze samenvoeglus? De onafhankelijke referentie gaf uitsluitsel: HuggingFace's eigen bibliotheek splitst ze exact hetzelfde. De verklaring zit in de rekensom van het bestand zelf: 248 044 woordenschattokens = 256 losse bytes + 247 587 samenvoegresultaten + 201 tokens waar geen enkele samenvoegregel naartoe leidt. Ze staan in de woordenschat, maar BPE kan ze per constructie nooit bouwen — in geen enkele implementatie. Het model kan zulke tokens dus wel uitspreken (als het ze zelf kiest), maar nooit te lezen krijgen. Waarom precies deze 201 paden ontbreken — de lijst bevat opvallend veel namen en landen — vertelt het bestand niet.
De zelftest kent sindsdien een aparte categorie "zonder samenvoegpad", en eist zoals altijd nul in de categorie onverklaard: 20 068 controles, allemaal groen — ook nog steeds op qwen2.5, waar de nieuwe categorie leeg blijkt.
Deel 06 De externe poort, zonder model
Voor qwen2.5 was ollama de onafhankelijke scheidsrechter — maar die moet een model dan wel kunnen drááien. Voor een tokenizer alleen is er een betere: HuggingFace's tokenizers-bibliotheek leest hetzelfde tokenizer.json met een volledig onafhankelijke implementatie. Een hulpscript codeert er 296 toetsteksten mee — de klassiekers uit de eerdere hoofdstukken, gemene combinerende tekens, CJK, emoji, speciale tokens, en 240 gezaaid-willekeurige samenraapsels — en schrijft tekst plus tokenreeks naar een referentiebestand. TokenizerVsReferentie.java codeert dezelfde teksten met ónze tokenizer en eist volledige gelijkheid:
$ java src/TokenizerVsReferentie.java qwen38/qwen38-tokenizer.gguf qwen38/qwen38-referentie.tsv
tokenizer : qwen35, 248.077 tokens
referentie : qwen38/qwen38-referentie.tsv (296 regels)
== RESULTAAT ==
volledig identieke tokenreeksen 296 van 296
GESLAAGD - elke tokenreeks is gelijk aan de referentie-implementatie
Deze toets is strenger dan de ollama-poort van hoofdstuk 13: daar konden alleen tokenaantallen en grenzen vergeleken worden, hier de volledige nummerreeksen. Elke afwijking in voorsplitser, normalisatie, bytetabel of samenvoeglus zou hier zichtbaar zijn.
Deel 07 De grens: een andere motor, geen grotere
Blijft over: de forward pass. Het gesprekssjabloon wordt nog herkend — qwen3.8 spreekt gewoon ChatML, met als nieuwigheid <think>-blokken waarin het model eerst hardop redeneert. Maar wie het tokenizerpakket aan de chat voert, krijgt het bewuste antwoord uit hoofdstuk 14:
$ java --add-modules jdk.incubator.vector src/Chat.java qwen38/qwen38-tokenizer.gguf
IllegalArgumentException: dit is een 'qwen35'-model; deze forward pass kent
alleen qwen2. De graf (rope-stijl, biassen) verschilt per architectuur,
dus raden zou stil verkeerde antwoorden geven.
En het verschil is dit keer geen detail als een RoPE-stijl. Van de 64 lagen zijn er 48 geen aandachtslaag meer:
Gated DeltaNet komt uit de familie van de toestandsmodellen: in plaats van bij elke token terug te kijken naar alle vorige (de aandacht van hoofdstuk 14, met haar almaar groeiende KV-cache) onderhoudt zo'n laag een geheugenmatrix van vaste grootte, die per token met een delta-regel wordt bijgewerkt en uitgelezen. Daar bovenop: poorten op de uitgang van de aandachtslagen, QK-norm, en RoPE op maar een kwárt van de kopdimensies. Dit model ondersteunen betekent hier niet "de matvec breder maken" maar een tweede motor schrijven. Dat vervolgproject is er inmiddels: hoofdstuk 18 bouwt hem, en bewijst hem op het kleine qwen3.5-zustermodel — de geheugengrens van deze machine blijft, maar de architectuurgrens is gevallen.
Slotles Wat de proef bewijst
Een model dat twee dagen oud is, vijftig keer zo groot als het studiemodel, met een woordenschat van 248 duizend tokens en een half nieuwe architectuur — en de score van de toolchain uit dit boek:
- De lader (hoofdstuk 12): volledig mee. Sterker, de GGUF-kennis was nauwkeurig genoeg om zelf een geldig bestand te schrijven waarvan de lader de structuur goedkeurde — en eerlijk meldde dat er geen gewichten in zitten.
- De tokenizer (hoofdstuk 13): mee na twee gerichte uitbreidingen — een nieuwe voorsplitser-regex en NFC-normalisatie — en extern bewezen: 296 van 296 tokenreeksen identiek aan een onafhankelijke implementatie. Onderweg een echte ontdekking: 201 tokens die geen enkele BPE-implementatie ooit kan bouwen.
- De motor (hoofdstuk 14–15): weigert, en dat is het juiste antwoord. Gated DeltaNet is geen grotere transformer maar een andere machine; op 8 GB is bovendien elk 27B-bestand onbegonnen werk. (Sinds hoofdstuk 18 ís die andere machine er.)
Dat is precies waarvoor de mijlpalen destijds zo geknipt waren: M1 en M2 zijn generiek gebouwd en reisden moeiteloos mee naar een nieuwe generatie; M3 is bewust smal gebouwd en zegt dat ook hardop. Wie zelf wil narekenen: het tokenizerpakket zit bij dit boek, model van zestien bytes inbegrepen.
Alle opdrachten hieronder werken vanuit de map code/ bij dit boek:
# het pakket bekijken, de zelftest, en de externe poort
java src/GgufDump.java qwen38/qwen38-tokenizer.gguf --brief
java src/TokenizerTest.java qwen38/qwen38-tokenizer.gguf
java src/TokenizerVsReferentie.java qwen38/qwen38-tokenizer.gguf qwen38/qwen38-referentie.tsv