ik heb een paar uur door de /karpathy/autoresearch repo regel voor regel gegaan. de "AI-agenten die onderzoek doen" invalshoek krijgt alle aandacht, maar ik denk dat het interessantere is wat er daadwerkelijk in het trainingsscript staat en de engineeringbeslissingen die de zoeklus strak maken. het is een van de meest compacte single-file trainingsopstellingen die ik heb gelezen. laat me beginnen met het ding dat het hele project mogelijk maakt: het tijdsbudget is vastgelegd op 300 seconden kloktijd. niet vaste stappen, niet vaste tokens, niet vaste flops. klokseconden. dit klinkt als een klein detail, maar het is de hele reden dat de autonome lus werkt. de agent kan het model 3x groter maken, de batchgrootte halveren, een compleet andere architectuur inruilen, en het resultaat is nog steeds direct vergelijkbaar met elk ander experiment omdat ze allemaal precies 5 minuten training op dezelfde gpu hebben gekregen. als je in plaats daarvan vaste stappen zou hebben, zou een groter model minder gradientupdates per seconde krijgen en zou je het oneerlijk straffen. als je vaste tokens zou hebben, zou je hetzelfde probleem hebben. het vastleggen van de tijd betekent dat je de juiste vraag stelt: gegeven deze hardware en deze tijd, wat is het beste model dat je kunt produceren? alles daarbuiten is een vrije variabele. de agent kan het volledige pareto-oppervlak van modelgrootte versus doorvoer versus convergentiesnelheid verkennen zonder dat een van die afwegingen verstoord wordt door het evaluatieprotocol. de metriek is ook zorgvuldig gekozen. het is bits per byte, niet cross-entropy verlies. cross-entropy hangt af van je vocabulairegrootte. een model met 32k tokens en een model met 8k tokens zullen heel verschillende verlieswaarden hebben, zelfs als ze de data even goed comprimeren. bpb normaliseert dit door de per-token cross-entropy in nats op te tellen, de utf-8 byte lengtes van de doel-tokens op te tellen, en nats-per-byte om te zetten naar bits-per-byte. dus zelfs als de agent iets verandert dat de effectieve tokenverdeling beïnvloedt, blijft de vergelijking eerlijk. deze twee keuzes, vaste kloktijd en een vocab-invariant metriek, maken van wat een rommelige onvergelijkbare zoektocht zou zijn, een schoon optimalisatieprobleem. nu het model zelf. het is een GPT maar met een aantal moderne trucs die het waard zijn om te begrijpen. ten eerste, RMSnorm overal. op de blokinvoeren (pre-norm), en ook op vragen en sleutels net voor het aandachtspuntproduct. deze QK-norm is belangrijk omdat zonder het de normen van q en k onbeperkt kunnen groeien tijdens de training, waardoor de aandachtlogits scherp worden en softmax verzadigt. het normaliseren van q en k houdt de dotproducten in een stabiel bereik, ongeacht hoe diep het netwerk is of hoe de trainingsdynamiek evolueert. de aandacht zelf is FA 3, geladen via de kernels-bibliotheek. het gebruikt varunneal's implementatie op hopper (sm_90) en valt terug op een community-build op oudere gpu's. het aandachtspatroon is "SSSL" wat betekent drie lagen van schuivende vensteraandacht (venster = de helft van de sequentielengte) gevolgd door een laag van volledige causale aandacht, die zich herhaalt. dit is het sparse-to-dense patroon dat je ziet in mistral en gemma2. de lokale aandachtlagen zijn computationeel goedkoop omdat de aandachtmatrix geband is, en de periodieke globale laag laat informatie door de volledige context stromen. met 8 lagen en een 4-tekenpatroon krijg je lagen 0,1,2 lokaal, laag 3 globaal, lagen 4,5,6 lokaal, laag 7 globaal. de laatste laag is gedwongen globaal ongeacht het patroon. de waarde-embedding is subtiel en ik denk dat het ondergewaardeerd is. elke andere laag krijgt zijn eigen embeddingtabel, volledig gescheiden van de hoofdtokenembedding, die token-id's rechtstreeks naar waarde-dimensievectors mappt. deze worden gemengd in de aandachtwaarden via een geleerde poort: v = v + 2 * sigmoid(W_gate @ x:32) * ve. het poortgewicht is nul-geïnitialiseerd, dus sigmoid(0) = 0.5, maal 2 geeft 1.0, wat een neutraal startpunt is. tijdens de training kan het model leren om de waarde-embedding per hoofd te versterken of te onderdrukken op basis van de eerste 32 dimensies van de verborgen toestand. dit komt uit de ResFormer-lijn van werk en de intuïtie is dat het aandacht een directe snelkoppeling naar tokenidentiteit geeft. de waardevectoren kunnen informatie dragen over "welk token zich op deze positie bevindt" zonder dat die informatie de residuele stroomtransformaties van eerdere lagen hoeft te overleven. het is in wezen een overslaan verbinding van de invoer rechtstreeks naar de aandachtwaarden, gegateerd zodat het model kan beslissen wanneer het nuttig is. er zijn ook per-laag leerbare scalars op de residuele stroom: x = lambda_residi * x + lambda_x0i * x0, waar x0 de genormaliseerde embedding van laag 0 is. elke laag kan onafhankelijk controleren hoeveel het luistert naar de lopende residu versus de oorspronkelijke invoer. de residuele lambdas beginnen bij 1.0, de x0 lambdas beginnen bij 0.1. dit is een zachte versie van het "ontkoppelde residu" idee. in een standaard transformer is de residuele stroom een som van alle eerdere laaguitgangen en wordt deze steeds meer vervuild naarmate je dieper gaat. elke laag toegang geven tot de schone oorspronkelijke embedding betekent dat het niet hoeft te leren om eerdere lagen "ongedaan" te maken om laag-niveau informatie te herstellen. de logits zijn zacht begrensd op 15 via tanh(logits/15)*15 wat voorkomt dat het model te zelfverzekerd is in de vroege training wanneer de representaties nog ruisachtig zijn. maar eerlijk gezegd is het meest interessante deel van het hele bestand de optimizer. MuonAdamW is een gecombineerde optimizer die verschillende update regels dispatches op basis van parametergroep. embeddings (token embedding, waarde embeddings, unembedding head) en per-laag scalars krijgen standaard AdamW met verschillende leersnelheden voor elke groep. de spreiding is wild. embedding lr is 0.6, unembedding lr is 0.004, dat is een verschil van 150x, en het is opzettelijk. de embeddingmatrix ziet elke enkele token en moet agressief updaten. de unembeddingmatrix is een lineaire probe op de uiteindelijke representatie en profiteert van stabiliteit. de leersnelheden voor embedding, waarde embedding en unembedding worden allemaal geschaald door (d_model / 768)^(-0.5) wat een muP-geïnspireerde correctie is. naarmate de modelbreedte verandert, passen die leersnelheden zich aan om de dynamiek van feature learning schaal-invariant te houden. de scalare leersnelheden voor de per-laag lambdas worden apart behandeld en krijgen deze schaling niet. de 2D-gewichtsmatrices in de transformer, aandachtprojecties en mlp-gewichten, krijgen Muon, en hier wordt het echt interessant. muon neemt de gradient, past nesterov-momentum toe, en voert vervolgens een newton-schulz-iteratie uit om de polaire decompositie van de gradientmatrix te benaderen. de polaire decompositie factoriseert een matrix G in G = U * S waar U orthogonaal is en S symmetrisch positief semi-definiet is. muon berekent U, de dichtstbijzijnde orthogonale matrix bij de gradient, en gebruikt dat als de update richting. de newton-schulz-iteratie is 5 stappen. voor lange matrices (meer rijen dan kolommen), A = X^T @ X dan X -> aX + X @ (bA + cA^2). voor brede matrices, A = X @ X^T dan X -> aX + (bA + cA^2) @ X. de coëfficiënten zijn hardcoded vanuit een voorafgaande berekening. ze noemen het "polar express." het hele ding compileert naar een enkele gefuseerde kernel via torch.compile. waarom is dit belangrijk? omdat voor gewichtsmatrices de frobenius-norm gradient (wat adam en sgd gebruiken) geometrisch verkeerd is. de "juiste" steilste dalingsrichting voor een gewichtsmatrix is degene die het verlies minimaliseert onder de voorwaarde dat de update eenheidspectrale norm heeft, niet eenheidsfrobeniusnorm. de orthogonale polaire factor geeft je precies dit. in de praktijk betekent het dat muon veel grotere effectieve updates maakt omdat het geen stapgrootte verspilt aan het schalen van de singuliere waarden. het roteert ze alleen. dit is waarom muon aanzienlijk sneller convergeert dan adam op transformer-gewichtsmatrices. muon onderhoudt wel per-element momentumbuffers (zelfde vorm als de parameters, gestapeld over elke vormgroep), maar in tegenstelling tot adam houdt het geen per-element tweede momenten bij. de tweede moment schattingen zijn per-rij of per-kolom na orthogonalizatie, niet per-element. daar komt NorMuon om de hoek kijken. bovenop de basis muon is er NorMuon, een variantiereductieschema. na orthogonalizatie berekent het per-rij (of per-kolom afhankelijk van de aspectverhouding) tweede moment schattingen, onderhoudt een exponentieel voortschrijdend gemiddelde daarvan, en schaalt de update zodat elke uitvoerdimensie zijn eigen adaptieve stapgrootte krijgt. het is in wezen het idee van adam adaptiviteit maar toegepast in het orthogonaliseerde coördinatensysteem in plaats van de ruwe parameterspace. de gewichtsafname is ook niet-standaard. het is "voorzichtig," wat betekent dat het alleen parameters afneemt waar de muon-update richting overeenkomt met het parametersignaal: mask = (g * params) >= 0. dit voorkomt de bekende faalmodus waarbij gewichtsafname parameters naar nul duwt tegen de wensen van de update in, wat de training kan destabiliseren. één klein detail dat ik waardeerde: na de allereerste trainingsstap, roept de code gc.collect(), gc.freeze(), gc.disable() aan om de garbage collector van python volledig uit te schakelen. de GC van python draait periodiek en veroorzaakt ~500ms stiltes. wanneer je totale budget 300 seconden is en elke stap misschien 300ms duurt, kost een willekeurige GC-pauze je bijna 2 trainingsstappen. ze triggeren handmatig gc.collect() elke 5000 stappen als een compromis. dit is het soort dingen dat je alleen leert door echte trainingsruns te profileren en mysterieuze doorvoersdalingen op te merken. de eerste 11 stappen (0 tot 10) worden ook niet meegerekend in het tijdsbudget. dat is de opwarming waar torch.compile zijn ding doet en CUDA-kernels JIT worden. zonder deze uitsluiting zouden verschillende experimenten verschillende hoeveelheden "echte" training krijgen, afhankelijk van hoe lang compilatie duurt voor die specifieke modelconfiguratie. opnieuw, een ontwerpkeuze die klein lijkt maar cruciaal is voor het vergelijkbaar maken van experimenten. nu inzoomen. de daadwerkelijke autoresearch-lus is: de agent leest program.md (een markdown-bestand dat zijn taak beschrijft), wijzigt train.py, commit, draait 5 minuten, controleert of val_bpb verbeterd is, houdt of herstelt, herhaalt. program.md zegt expliciet "STOP NOOIT." de agent draait eindeloos totdat de mens het stopt. ~12 experimenten per uur, ~100 's nachts terwijl je slaapt. ...