Am petrecut câteva ore parcurgând depozitul /karpathy/autoresearch linie cu linie. Unghiul "agenți AI care fac cercetare" este ceea ce atrage toată atenția, dar cred că lucrul mai interesant este ce se află de fapt în scriptul de antrenament și deciziile inginerești care fac cercul de căutare mai strâns. Este unul dintre cele mai dense sisteme de antrenament pe un singur șir pe care le-am citit. Permiteți-mi să încep cu lucrul care face posibil întregul proiect: bugetul de timp este fixat la ceasul de perete de 300 de secunde. nu pași fixi, nu jetoane fixe, nu flop-uri fixe. secunde de ceasul de perete. Pare un detaliu minor, dar acesta este motivul principal pentru care funcționează bucla autonomă. Agentul poate face modelul de 3 ori mai mare, poate înjumătăți dimensiunea lotului, poate schimba o arhitectură complet diferită, iar rezultatul este totuși direct comparabil cu orice alt experiment pentru că toți au primit exact 5 minute de antrenament pe aceeași placă video. Dacă ai corecta pașii în schimb, un model mai mare ar primi mai puține actualizări de gradient pe secundă și l-ai penaliza nedrept. Dacă ai repara jetoane, ai avea aceeași problemă. Rezolvarea timpului de perete înseamnă că pui întrebarea corectă: având acest hardware și atât de mult timp, care este cel mai bun model pe care îl poți produce? Tot restul este o variabilă liberă. Agentul poate explora întreaga suprafață Pareto a dimensiunii modelului vs throughput vs viteza de convergență fără ca niciunul dintre aceste compromisuri să fie derutat de protocolul de evaluare. Metrica este, de asemenea, aleasă cu grijă. Este vorba despre biți pe octet, nu pierdere de entropie încrucișată. Entropia încrucișată depinde de mărimea vocabularului tău. Un model cu 32k tokenuri și un model cu 8k tokens vor avea valori de pierdere foarte diferite chiar dacă comprimă datele la fel de bine. BPB normalizează acest lucru prin însumarea entropiei încrucișate per token în NATs, sumarea lungimii UTF-8 bytes ale tokenurilor țintă și conversia NATs-pe-byte în biți-per-octet. Așadar, chiar dacă agentul modifică ceva care afectează distribuția efectivă a tokenului, comparația rămâne corectă. Aceste două alegeri, timp fix de perete și o metrică invariantă la vocabular, transformă ceea ce ar fi o căutare dezordonată și incomparabilă într-o problemă de optimizare curată. Acum modelul în sine. este un GPT, dar cu o mulțime de trucuri moderne care merită înțelese. în primul rând, RMSnorm peste tot. Pe intrările pe blocuri (înainte de norm), dar și pe interogări și taste chiar înainte de produsul punct de atenție. această problemă cu QK-norma este importantă pentru că fără ea normele Q și K pot crește nelimitat în timpul antrenamentului, determinând ca logiturile de atenție să se ascuțească și softmax-ul să se satureze. Normalizarea Q și K menține produsele scalare într-un interval stabil, indiferent cât de adâncă este rețeaua sau cum evoluează dinamica antrenamentului. atenția în sine este FA 3, încărcată prin biblioteca kernels. Folosește implementarea lui Varunneal pe Hopper (sm_90) și revine la o construcție comunitară pe GPU-uri mai vechi. modelul de atenție este "SSSL", ceea ce înseamnă trei straturi de atenție glisantă a ferestrei (fereastra = jumătate din lungimea secvenței), urmate de un strat de atenție cauzală completă, repetând. Acesta este modelul de la rar la dens pe care îl vezi la Mistral și Gemma2. straturile locale de atenție sunt ieftine din punct de vedere computațional deoarece matricea de atenție este bandată, iar stratul global periodic permite informației să curgă pe întregul context. Cu 8 straturi și un model de 4 caractere obții straturile 0,1,2 local, stratul 3 global, straturile 4,5,6 local, stratul 7 global. Ultimul strat este forțat global indiferent de tipar. Chestia cu încorporarea valorii este subtilă și cred că este subapreciată. Fiecare alt strat primește propriul tabel de încorporare, complet separat de embedding-ul principal al tokenului, care mapează ID-urile tokenurilor direct în vectori valoare-dimensiune. Acestea sunt amestecate în valorile atenției printr-o poartă învățată: v = v + 2 * sigmoid(W_gate @ x:32) * ve. Greutatea porții este zero-inițializată, deci sigmoid(0) = 0,5, înmulțit cu 2 dă 1,0, care este un punct de pornire neutru. Supra-antrenament, modelul poate învăța să amplifice sau să suprime valoarea încorporată pe cap pe baza primelor 32 de dimensiuni ale stării ascunse. aceasta provine din linia de lucru ResFormer și intuiția este că oferă atenției o scurtătură directă către identitatea tokenului. Vectorii de valoare pot transporta informații despre "ce token se află în această poziție" fără ca acea informație să supraviețuiască transformărilor reziduale ale fluxului de straturi anterioare. Practic, este o conexiune skip de la intrare direct la valorile de atenție, blocată astfel încât modelul să poată decide când este util. Există, de asemenea, scalari învățabili pe strat pe fluxul rezidual: x = lambda_residi * x + lambda_x0i * x0, unde x0 este încorporarea normalizată din stratul 0. Fiecare strat poate controla independent cât de mult ascultă rezidualul de rulare față de intrarea originală. Lambda-urile reziduale încep de la 1.0, lambda-urile x0 încep de la 0.1. Aceasta este o versiune blândă a ideii de "reziduu descurcat". Într-un transformator standard, fluxul rezidual este suma tuturor ieșirilor din straturile anterioare și devine tot mai poluat pe măsură ce pătrunzi mai adânc. Oferind fiecărui strat acces la încorporarea originală curată, nu trebuie să învețe să "anuleze" straturile anterioare pentru a recupera informații de nivel scăzut. Logiturile sunt softlimitate la 15 prin tanh(logits/15)*15, ceea ce împiedică modelul să fie prea încrezător la începutul antrenamentului, când reprezentările sunt încă zgomotoase. Dar sincer, partea cea mai interesantă a întregului fișier este optimizatorul. MuonAdamW este un optimizator combinat care distribuie diferite reguli de actualizare în funcție de grupul de parametri. Încorporațiile (încorporare de token, încorporații de valori, cap de unembedding) și scalarii pe strat primesc AdamW standard cu rate de învățare diferite pentru fiecare grup. Răspândirea este sălbatică. Încorporarea LR este 0.6, deblocarea LR este 0.004, adică o diferență de 150x și este intenționat. Matricea de încorporare vede fiecare token și trebuie actualizată agresiv. Matricea de unembedding este o sondă liniară pe reprezentarea finală și beneficiază de stabilitate. încorporarea, încorporarea valorii și rata de dezintegrare sunt toate scalate cu (d_model / 768)^(-0,5), ceea ce este o corecție inspirată de muP. Pe măsură ce lățimea modelului se schimbă, aceste rate de învățare se ajustează pentru a menține dinamica învățării caracteristicilor invariabilă la scară. Ratele scalare de învățare pentru lambda-urile pe strat sunt gestionate separat și nu primesc această scalare. matricile de greutate 2D din transformer, proiecțiile de atenție și greutățile MLP, obțin Muon, și aici devine cu adevărat interesant. Muonul ia gradientul, aplică impulsul Nesterov, apoi rulează o iterație Newton-Schulz pentru a aproxima descompunerea polară a matricei gradientului. factorii de descompunere polară o matrice G în G = U * S, unde U este ortogonal, iar S este simetric, pozitiv semi-definit. muonul calculează U, cea mai apropiată matrice ortogonală de gradient, și o folosește ca direcție de actualizare. Iterația Newton-Schulz are 5 pași. pentru matrici înalte (mai multe rânduri decât coloane), A = X^T @ X apoi X -> aX + X @ (bA + cA^2). pentru matrici late, A = X @ X^T apoi X -> aX + (bA + cA^2) @ X. Coeficienții sunt codificați fix dintr-un precalcul. Ei îl numesc "Polar Express." Întregul proces se compilează într-un singur nucleu fuzionat prin Torch.compile. De ce contează asta? Pentru că pentru matricile de greutăți gradientul normei Frobenius (pe care îl folosesc Adam și SGD) este greșit din punct de vedere geometric. Direcția "corectă" de coborâre cea mai abruptă pentru o matrice de greutate este cea care minimizează pierderea, supusă constrângerii ca actualizarea să aibă o normă spectrală unitare, nu norma Frobenius unitare. Factorul polar ortogonal îți oferă exact acest lucru. În practică, înseamnă că Muon face actualizări efective mult mai mari pentru că nu irosește dimensiunea pasului la scalarea valorilor singulare. doar le rotește. De aceea muonul converge semnificativ mai rapid decât Adam pe matricile de greutate ale transformatorului. Muonul menține buffere de impuls pe element (aceeași formă ca parametrii, suprapuse pe fiecare grup de forme), dar spre deosebire de Adam, nu urmărește momentele secunde pe element. estimările momentului al doilea sunt pe rând sau pe coloană după ortogonalizare, nu pe element. aici intervine NorMuon. deasupra muonului de bază există NorMuon, o schemă de reducere a varianței. După ortogonalizare, calculează estimările de moment secundar pe rând (sau pe coloană, în funcție de raportul de aspect), menține o medie mobilă exponențială a acestora și rescalează actualizarea astfel încât fiecare dimensiune de ieșire să aibă propria dimensiune adaptivă a pasului. Este practic ideea adaptivității Adam, dar aplicată în sistemul de coordonate ortogonalizat, nu în spațiul de parametri brut. Scăderea greutății este, de asemenea, neobișnuită. Este "precaut", ceea ce înseamnă că dezintegrează doar parametrii în care direcția de actualizare a muonului este de acord cu semnul parametrului: mask = (g * parametri) >= 0. Aceasta evită modul cunoscut de defectare, în care scăderea greutății împinge parametrii spre zero împotriva dorinței actualizării, ceea ce poate destabiliza antrenamentul. Un mic detaliu pe care l-am apreciat: după primul pas de instruire, codul apelează gc.collect(), gc.freeze(), gc.disable() pentru a opri complet garbage-collector-ul din Python. GC-ul Python rulează periodic și cauzează blocaje de ~500ms. Când bugetul tău total este de 300 de secunde și fiecare pas are poate 300ms, o pauză aleatorie la GC te costă aproape 2 pași de antrenament. Ei declanșează manual gc.collect() la fiecare 5000 de pași ca un compromis. Acesta este genul de lucru pe care îl înveți doar prin profilarea antrenamentelor reale și observarea unor scăderi misterioase de debit. Primii 11 pași (0 până la 10) nu sunt nici ei luați în calcul pentru bugetul de timp. Asta e încălzirea în care torch.compile își face treaba și kernel-urile CUDA sunt JIT-uri. Fără această excludere, diferite experimente ar primi cantități diferite de antrenament "real" în funcție de cât durează compilarea pentru acea configurație de model. Din nou, o alegere de design care pare mică, dar este esențială pentru a face experimentele comparabile. Acum dă zoom înapoi. Bucla reală de autocercetare este: agentul citește program.md (un fișier markdown care descrie sarcina sa), modifică .py de tren, face un commit, rulează 5 minute, verifică dacă val_bpb îmbunătățit, păstrează sau revine, repetă. program.md spune explicit "NU TE OPRI NICIODATĂ." Agentul fuge la nesfârșit până când omul îl ucide. ~12 experimente pe oră, ~100 peste noapte cât dormi. ...