Passei algumas horas analisando o repositório /karpathy/autoresearch linha por linha. O ângulo de "agentes de IA fazendo pesquisa" é o que está chamando toda a atenção, mas acho que o mais interessante é o que realmente está dentro do script de treinamento e as decisões de engenharia que tornam o ciclo de busca mais apertado. É um dos sistemas de treinamento mais densos em fila única que já li. Deixe-me começar com o que torna todo o projeto possível: o orçamento de tempo é fixo em 300 segundos de relógio de parede. Não são passos fixos, nem tokens fixos, nem flops fixos. segundos do relógio de parede. Isso pode parecer um detalhe menor, mas é a razão principal pela qual o loop autônomo funciona. O agente pode fazer o modelo 3x maior, reduzir o tamanho do lote pela metade, trocar por uma arquitetura completamente diferente, e o resultado ainda é diretamente comparável a qualquer outro experimento porque todos receberam exatamente 5 minutos de treinamento na mesma GPU. Se você corrigisse os passos, um modelo maior teria menos atualizações de gradiente por segundo e você estaria penalizando injustamente. Se você consertar os tokens, teria o mesmo problema. Corrigir o tempo de parede significa que você está fazendo a pergunta certa: dado esse hardware e tanto tempo, qual é o melhor modelo que você pode produzir? Todo o resto é uma variável livre. O agente pode explorar toda a superfície de Pareto entre tamanho do modelo, throughput e velocidade de convergência, sem que nenhum desses compromissos seja confundido pelo protocolo de avaliação. A métrica também é cuidadosamente escolhida. É bits por byte, não perda de entropia cruzada. Entropia cruzada depende do tamanho do seu vocabulário. Um modelo com tokens de 32k e um modelo com tokens de 8k terão valores de perda muito diferentes, mesmo que comprimam os dados de forma igual. O BPB normaliza isso somando a entropia cruzada por token em NATs, somando os comprimentos UTF-8 bytes dos tokens-alvo e convertendo nats por byte em bits por byte. Então, mesmo que o agente altere algo que afete a distribuição efetiva do token, a comparação continua justa. Essas duas escolhas, tempo fixo de parede e métrica invariante ao vocabulário, transformam o que seria uma busca bagunçada e incomparável em um problema de otimização limpa. Agora o modelo em si. é um GPT, mas com vários truques modernos que valem a pena entender. primeiro, RMSnorm em todo lugar. nas entradas de bloco (pré-norm), e também nas consultas e teclas logo antes do produto de atenção dos pontos de ponto. Essa questão da QK-norma é importante porque, sem ela, as normas de q e k podem crescer ilimitadamente durante o treinamento, fazendo com que os logits de atenção fiquem mais nítidos e o softmax saturem. Normalizar Q e K mantém os produtos escalares em uma faixa estável, independentemente da profundidade da rede ou de como a dinâmica de treinamento evolui. a atenção em si é FA 3, carregada pela biblioteca kernels. Ele usa a implementação da Varunneal no Hopper (sm_90) e volta a uma build comunitária sobre GPUs antigas. o padrão de atenção é "SSSL", que significa três camadas de atenção em janela deslizante (janela = metade do comprimento da sequência) seguidas por uma camada de atenção causal completa, repetindo. Esse é o padrão esparso a denso que você vê em Mistral e Gemma2. As camadas de atenção local são computacionalmente baratas porque a matriz de atenção é faixosa, e a camada global periódica permite que a informação flua através de todo o contexto. Com 8 camadas e um padrão de 4 caracteres, você obtém as camadas 0, 1, 2 local, camada 3 global, camadas 4, 5, 6 local, camada 7 global. A última camada é forçada a ser global independentemente do padrão. A questão da incorporação de valor é sutil e acho que subestimada. Cada outra camada recebe sua própria tabela de incorporação, completamente separada da incorporação principal do token, que mapeia IDs de tokens diretamente para vetores valor-dimensão. esses são misturados aos valores de atenção por meio de uma porta aprendida: v = v + 2 * sigmoide(W_gate @ x:32) * ve. O peso da porta é zero-inicializado, então sigmoide(0) = 0,5, vezes 2 dá 1,0, que é um ponto de partida neutro. Com sobre-treinamento, o modelo pode aprender a amplificar ou suprimir a incorporação de valor por cabeça com base nas primeiras 32 dimensões do estado oculto. isso vem da linha de trabalho ResFormer e a intuição é que isso dá à atenção um atalho direto para a identidade do token. Os vetores de valor podem transportar informações sobre "qual token está nessa posição" sem que essa informação precise sobreviver às transformações residuais do fluxo das camadas anteriores. É basicamente uma conexão de salto da entrada diretamente para os valores de atenção, bloqueada para que o modelo possa decidir quando é útil. Também existem escalares aprendíveis por camada no fluxo residual: x = lambda_residi * x + lambda_x0i * x0, onde x0 é a imersão normalizada da camada 0. Cada camada pode controlar independentemente o quanto ouve o residual em execução versus a entrada original. As lambdas residuais começam em 1,0, as lambdas x0 começam em 0,1. Esta é uma versão suave da ideia do "residual desenrelaçado". Em um transformador padrão, o fluxo residual é a soma de todas as saídas da camada anterior e fica cada vez mais poluído à medida que você vai mais fundo. Dar a cada camada acesso à incorporação original limpa significa que ela não precisa aprender a "desfazer" camadas anteriores para recuperar informações de baixo nível. Os logits são softcaps em 15 via tanh(logits/15)*15, o que impede que o modelo fique excessivamente confiante no início do treinamento, quando as representações ainda são ruidosas. Mas, honestamente, a parte mais interessante de todo o arquivo é o otimizador. MuonAdamW é um otimizador combinado que distribui diferentes regras de atualização baseadas no grupo de parâmetros. Embeddings (embedding de token, embeddings de valor, cabeça de desembedding) e escalares por camada recebem o padrão AdamW com taxas de aprendizado diferentes para cada grupo. A distribuição é selvagem. Embedding LR é 0,6, desembedding LR é 0,004, isso é uma diferença de 150x, e é intencional. A matriz de embedding vê todos os tokens e precisa atualizar agressivamente. A matriz de desembedding é uma sonda linear na representação final e se beneficia da estabilidade. as taxas de aprendizado de embedding, value embedding e desembedding são todas escaladas por (d_model / 768)^(-0,5), o que é uma correção inspirada no muP. À medida que a largura do modelo muda, essas taxas de aprendizado se ajustam para manter a dinâmica de aprendizado de características invariante em escala. As taxas de aprendizado escalar para os lambdas por camada são tratadas separadamente e não obtêm essa escala. as matrizes de pesos 2D no transformer, projeções de atenção e os pesos MLP, pegam Muon, e é aí que fica realmente interessante. O muon pega o gradiente, aplica o momento de Nesterov e então executa uma iteração de Newton-Schulz para aproximar a decomposição polar da matriz do gradiente. a decomposição polar factoriza uma matriz G em G = U * S onde U é ortogonal e S é simétrico positivo semidefinido. múon calcula U, a matriz ortogonal mais próxima ao gradiente, e usa isso como direção de atualização. A iteração Newton-Schulz tem 5 etapas. para matrizes altas (mais linhas do que colunas), A = X^T @ X depois X -> aX + X @ (bA + cA^2). para matrizes largas, A = X @ X^T então X -> aX + (bA + cA^2) @ X. Os coeficientes são codificados fixamente a partir de uma pré-computação. Eles chamam de "Polar Express". Tudo compila em um único kernel fundido via Torch.compile. Por que isso importa? Porque para matrizes de pesos o gradiente da norma de Frobenius (que Adam e SGD usam) é geometricamente errado. A direção de descida "correta" mais íngreme para uma matriz de pesos é aquela que minimiza a perda, sujeita à restrição de que a atualização tenha norma espectral unitária, não norma de Frobenius unitária. O fator polar ortogonal fornece exatamente isso. Na prática, isso significa que o Muon faz atualizações efetivas muito maiores porque não está desperdiçando o tamanho do passo escalando os valores singulares. Ele só os roda. É por isso que o múon converge significativamente mais rápido que o Adam nas matrizes de peso do transformador. O muon mantém buffers de momento por elemento (mesma forma dos parâmetros, empilhados em cada grupo de forma), mas diferente de Adam, ele não acompanha os segundos momentos por elemento. as estimativas do segundo momento são por linha ou por coluna após ortogonalização, não por elemento. É aí que entra o NorMuon. no topo do muon base há o NorMuon, um esquema de redução de variância. Após a ortogonalização, ele calcula as estimativas de segundo momento por linha (ou por coluna, dependendo da razão de aspecto), mantém uma média móvel exponencial dessas e reescala a atualização para que cada dimensão de saída receba seu próprio tamanho de passo adaptativo. É essencialmente a ideia da adaptividade de Adam, mas aplicada no sistema de coordenadas ortogonalizada, e não no espaço bruto dos parâmetros. A perda de peso também não é padrão. É "cauteloso", ou seja, só decabe parâmetros onde a direção de atualização do múon coincide com o sinal de parâmetro: máscara = (g * parâmetros) >= 0. Isso evita o modo de falha conhecido, onde a perda de peso empurra os parâmetros para zero contra a vontade da atualização, o que pode desestabilizar o treinamento. Um pequeno detalhe que apreciei: após a primeira etapa de treinamento, o código chama gc.collect(), gc.freeze(), gc.disable() para desligar completamente o coletor de lixo do python. O GC do Python roda periodicamente e causa paralisações de ~500ms. Quando seu orçamento total é de 300 segundos e cada passo pode ser de 300ms, uma pausa aleatória do GC custa quase 2 etapas de treinamento. Eles acionam manualmente o gc.collect() a cada 5000 passos como um compromisso. Esse é o tipo de coisa que só se aprende ao analisar treinos reais e notar quedas misteriosas de rendimento. Os primeiros 11 passos (0 a 10) também não são contados para o orçamento de tempo. é o aquecimento em que o torch.compile faz seu trabalho e os kernels CUDA são JIT. Sem essa exclusão, diferentes experimentos receberiam quantidades diferentes de treinamento "real" dependendo de quanto tempo a compilação leva para aquela configuração específica do modelo. Novamente, uma escolha de design que parece pequena, mas é fundamental para tornar os experimentos comparáveis. Agora afaste o zoom. O loop real de autopesquisa é: o agente lê program.md (um arquivo markdown que descreve seu trabalho), modifica o .py de treino, faz commits, roda por 5 minutos, verifica se val_bpb melhorou, mantém ou reverte, repete. program.md diz explicitamente "NUNCA PARE." O agente corre indefinidamente até que o humano o mate. ~12 experimentos por hora, ~100 durante a noite enquanto você dorme. ...