Я провел несколько часов, просматривая репозиторий /karpathy/autoresearch построчно. Угол "искусственные агенты, занимающиеся исследованиями" привлекает все внимание, но я думаю, что более интересным является то, что на самом деле находится внутри скрипта обучения и инженерные решения, которые делают цикл поиска эффективным. Это одна из самых плотных однопользовательских установок обучения, которые я читал. Позвольте мне начать с того, что делает весь проект возможным: временной бюджет фиксирован на 300 секунд реального времени. Не фиксированные шаги, не фиксированные токены, не фиксированные операции с плавающей запятой. Секунды реального времени. Это звучит как незначительная деталь, но это вся причина, по которой автономный цикл работает. Агент может сделать модель в 3 раза больше, сократить размер пакета вдвое, заменить совершенно другую архитектуру, и результат все равно будет напрямую сопоставим с каждым другим экспериментом, потому что все они получили ровно 5 минут обучения на одном и том же GPU. Если бы вы фиксировали шаги, то большая модель получала бы меньше обновлений градиента в секунду, и вы бы несправедливо ее наказывали. Если бы вы фиксировали токены, у вас была бы та же проблема. Фиксация реального времени означает, что вы задаете правильный вопрос: учитывая это оборудование и столько времени, какую лучшую модель вы можете создать? Все остальное — это свободная переменная. Агент может исследовать всю поверхность Парето размера модели против пропускной способности против скорости сходимости, не сталкиваясь с этими компромиссами, которые были бы запутаны протоколом оценки. Метрика также тщательно выбрана. Это биты на байт, а не кросс-энтропийные потери. Кросс-энтропия зависит от размера вашего словаря. Модель с 32k токенами и модель с 8k токенами будут иметь очень разные значения потерь, даже если они одинаково хорошо сжимают данные. bpb нормализует это, суммируя кросс-энтропию по токенам в натах, суммируя длины байтов utf-8 целевых токенов и преобразуя нат на байт в биты на байт. Таким образом, даже если агент изменяет что-то, что влияет на эффективное распределение токенов, сравнение остается справедливым. Эти два выбора, фиксированное реальное время и метрика, не зависящая от словаря, превращают то, что было бы неаккуратным несравнительным поиском, в чистую задачу оптимизации. Теперь о самой модели. Это GPT, но с множеством современных приемов, которые стоит понять. Во-первых, RMSnorm повсюду. На входах блока (предварительная нормализация), а также на запросах и ключах прямо перед произведением внимания. Эта норма QK важна, потому что без нее нормы q и k могут расти без ограничений во время обучения, что приводит к резкому увеличению логитов внимания и насыщению softmax. Нормализация q и k сохраняет произведения в стабильном диапазоне независимо от того, насколько глубока сеть или как развиваются динамика обучения. Само внимание — это FA 3, загруженное через библиотеку kernels. Оно использует реализацию varunneal на hopper (sm_90) и переходит на сборку сообщества на более старых GPU. Шаблон внимания — "SSSL", что означает три слоя скользящего окна внимания (окно = половина длины последовательности), за которыми следует один слой полного причинного внимания, повторяющийся. Это разреженный к плотному шаблону, который вы видите в mistral и gemma2. Слои локального внимания вычислительно дешевы, потому что матрица внимания имеет полосовую структуру, а периодический глобальный слой позволяет информации течь через весь контекст. С 8 слоями и 4-символьным шаблоном вы получаете слои 0,1,2 локально, слой 3 глобально, слои 4,5,6 локально, слой 7 глобально. Последний слой принудительно глобальный независимо от шаблона. Вещь с вектором значений тонка и, я думаю, недооценена. Каждый другой слой получает свою собственную таблицу встраивания, полностью отдельную от основной встраивания токенов, которая сопоставляет идентификаторы токенов непосредственно с векторами значений. Эти векторы смешиваются в значения внимания через обученные ворота: v = v + 2 * sigmoid(W_gate @ x:32) * ve. Вес ворот инициализируется нулем, так что sigmoid(0) = 0.5, умноженное на 2 дает 1.0, что является нейтральной отправной точкой. В процессе обучения модель может научиться усиливать или подавлять встраивание значений на голову в зависимости от первых 32 измерений скрытого состояния. Это из линии работы ResFormer, и интуиция заключается в том, что это дает вниманию прямую короткую связь с идентичностью токена. Вектор значений может нести информацию о "какой токен находится на этой позиции", не требуя, чтобы эта информация выжила в трансформациях остаточного потока из более ранних слоев. Это по сути соединение пропуска от входа прямо в значения внимания, запирающееся, чтобы модель могла решать, когда это полезно. Также есть обучаемые скаляры на каждом слое в остаточном потоке: x = lambda_residi * x + lambda_x0i * x0, где x0 — это нормализованное встраивание из слоя 0. Каждый слой может независимо контролировать, насколько сильно он слушает текущий остаток по сравнению с оригинальным входом. Остаточные лямбды начинаются с 1.0, лямбды x0 начинаются с 0.1. Это мягкая версия идеи "разделенного остатка". В стандартном трансформере остаточный поток является суммой всех выходов предыдущих слоев и становится все более загрязненным по мере углубления. Предоставление каждому слою доступа к чистому оригинальному встраиванию означает, что ему не нужно учиться "отменять" предыдущие слои, чтобы восстановить информацию низкого уровня. Логиты мягко ограничены на 15 через tanh(logits/15)*15, что предотвращает избыточную уверенность модели на ранних этапах обучения, когда представления все еще шумные. Но, честно говоря, самая интересная часть всего файла — это оптимизатор. MuonAdamW — это комбинированный оптимизатор, который распределяет разные правила обновления в зависимости от группы параметров. Встраивания (встраивание токенов, встраивания значений, голова развертывания) и скаляры на каждом слое получают стандартный AdamW с разными скоростями обучения для каждой группы. Разброс дикий. Скорость обучения встраивания составляет 0.6, скорость обучения развертывания составляет 0.004, это разница в 150 раз, и это намеренно. Матрица встраивания видит каждый отдельный токен и нуждается в агрессивном обновлении. Матрица развертывания — это линейный зонд на финальном представлении и выигрывает от стабильности. Скорости обучения встраивания, встраивания значений и развертывания все масштабируются по (d_model / 768)^(-0.5), что является коррекцией, вдохновленной muP. По мере изменения ширины модели эти скорости обучения корректируются, чтобы сохранить динамику обучения признаков инвариантной к масштабу. Скорости обучения скаляров для лямбд на каждом слое обрабатываются отдельно и не получают этого масштабирования. 2D весовые матрицы в трансформере, проекции внимания и веса mlp получают Muon, и здесь становится действительно интересно. Muon берет градиент, применяет момент Нестерова, а затем выполняет итерацию Ньютона-Шульца, чтобы приблизить полярное разложение матрицы градиента. Полярное разложение факторизует матрицу G в G = U * S, где U — ортогональная, а S — симметричная положительно определенная. Muon вычисляет U, ближайшую ортогональную матрицу к градиенту, и использует это как направление обновления. Итерация Ньютона-Шульца составляет 5 шагов. Для высоких матриц (больше строк, чем столбцов) A = X^T @ X, затем X -> aX + X @ (bA + cA^2). Для широких матриц A = X @ X^T, затем X -> aX + (bA + cA^2) @ X. Коэффициенты жестко закодированы из предварительного вычисления. Они называют это "полярный экспресс". Все это компилируется в один объединенный ядро через torch.compile. Почему это важно? Потому что для весовых матриц градиент Фробениуса (что использует adam и sgd) геометрически неверен. "Правильное" направление наискорейшего спуска для весовой матрицы — это то, которое минимизирует потерю при условии, что обновление имеет единичную спектральную норму, а не единичную норму Фробениуса. Ортогональный полярный фактор дает вам именно это. На практике это означает, что muon делает гораздо более крупные эффективные обновления, потому что он не тратит размер шага на масштабирование сингулярных значений. Он только вращает их. Вот почему muon сходится значительно быстрее, чем adam на весовых матрицах трансформера. Muon действительно поддерживает буферы момента на каждом элементе (такой же формы, как параметры, сложенные по каждой группе формы), но в отличие от adam он не отслеживает вторые моменты на каждом элементе. Оценки второго момента — это по строкам или по столбцам после ортогонализации, а не по элементам. Вот где появляется NorMuon. Сверху базового muon есть NorMuon, схема уменьшения дисперсии. После ортогонализации он вычисляет оценки второго момента по строкам (или по столбцам в зависимости от соотношения сторон), поддерживает экспоненциальное скользящее среднее этих оценок и пересчитывает обновление, чтобы каждый выходной размер получал свой собственный адаптивный размер шага. Это по сути идея адаптивности adam, но примененная в ортогонализированной координатной системе, а не в сыром пространстве параметров. Уменьшение веса также нестандартное. Оно "осторожное", что означает, что оно уменьшает параметры только тогда, когда направление обновления muon согласуется со знаком параметра: mask = (g * params) >= 0. Это избегает известного режима сбоя, когда уменьшение веса толкает параметры к нулю против желаний обновления, что может дестабилизировать обучение. Одна небольшая деталь, которую я оценил: после самого первого шага обучения код вызывает gc.collect(), gc.freeze(), gc.disable(), чтобы полностью отключить сборщик мусора Python. GC Python работает периодически и вызывает задержки около 500 мс. Когда ваш общий бюджет составляет 300 секунд, а каждый шаг занимает около 300 мс, случайная пауза GC стоит вам почти 2 шага обучения. Они вручную вызывают gc.collect() каждые 5000 шагов в качестве компромисса. Это то, что вы можете узнать только профилированием реальных запусков обучения и замечая загадочные падения производительности. Первые 11 шагов (с 0 по 10) также не учитываются в бюджет времени. Это разогрев, когда torch.compile делает свое дело, и CUDA ядра JIT компилируются. Без этого исключения разные эксперименты получали бы разное количество "реального" обучения в зависимости от того, сколько времени занимает компиляция для данной конфигурации модели. Снова, выбор дизайна, который кажется небольшим, но критически важен для того, чтобы сделать эксперименты сопоставимыми. Теперь увеличим масштаб. Фактический цикл автопоиска: агент читает program.md (файл markdown, который описывает его задачу), модифицирует train.py, коммитит, запускает в течение 5 минут, проверяет, улучшился ли val_bpb, сохраняет или откатывает, повторяет. program.md явно говорит "НИКОГДА НЕ ОСТАНАВЛИВАЙТЕСЬ." Агент работает бесконечно, пока человек не остановит его. ~12 экспериментов в час, ~100 за ночь, пока вы спите. ...