Разработчик обучил языковую модель за $998 и обошёл nanochat Карпати

Независимый разработчик Хьюго Вернь с нуля обучил языковую модель на 3,848 млрд параметров (в заголовке поста округлено до «3,8B»), архитектура в духе Llama: RMSNorm, RoPE, групповое внимание GQA (24 головы запросов, 8 общих голов ключей-значений), MLP-блоки без гейта (relu²), QK-norm, softcap на логитах и дополнительные value-таблицы, слои эмбеддингов, добавляющие вклад к value-проекциям внимания через один блок (14 штук; приём из архитектуры ResFormer). Обучение заняло 65,3 млрд токенов и 43 часа на 8 арендованных GPU B200 и обошлось в $998; итоговый результат, 0,384 (точнее 0,3840) по бенчмарку CORE. В посте приведена таблица сравнения: GPT-2 от OpenAI (1,5 млрд параметров, 2019 год) набирает по CORE 0,2565; nanochat d26 Андрея Карпати (~561 млн параметров, 11,2 млрд токенов, 8×H100, ~3 часа), около 0,258; более крупный nanochat d32 (~1 млрд параметров, 8×H100, ~33 часа, $1000), 0,310. То есть при бюджете, сопоставимом с nanochat d32 ($1000 против $998), модель автора заметно опережает его по CORE. Более дешёвый вариант той же архитектуры, при контексте 1024 токена вместо 2048, уложился в $820 за 35,9 часа и 57,3 млрд токенов и дал CORE 0,338 (точнее 0,3384).

Путь к этому результату начался с провала: модель на 858 млн параметров, обученная на 16,4 млрд токенов в течение 5,8 суток на одной GPU A100, показала по PIQA лишь 60,45%, хуже, чем GPT-2 на 124 млн параметров (модель 2019 года, в 7 раз меньше по размеру), у которой PIQA около 63%. Разбор кривой обучения выявил причины: косинусный спад learning rate до нуля выхолащивал последние 30% шагов обучения (кривая «плоская» после 70% шагов), пиковый LR 2,5×10⁻⁴ был слишком консервативным для модели такого размера, для всех матричных параметров использовался только AdamW, а датасет FineWeb-Edu оказался не лучшим выбором. Пять правок дали финальный рецепт: трапецеидальный график LR (прогрев 5%, плато, линейное снижение на последних 50% шагов до 5% от пика) вместо косинусного, так модель продолжает учиться до самого конца; оптимизатор Muon для матричных параметров вместе с AdamW для остальных (Muon примерно на 25% медленнее на шаг, но при 7 шагах накопления градиента накладные расходы размываются до ~4%); датасет ClimbMix вместо FineWeb-Edu, давший резкий скачок в скорости сходимости, автор ссылается на аналогичный вывод Андрея Карпати; обучение в FP8 (torch._scaled_mm с динамическим масштабированием по всем трём матричным умножениям) вместе с паддингом словаря с 50 257 до 50 304 токенов (кратно 64, для тензорных ядер), что вместе дало +33% к пропускной способности; и сокращение контекста до 1024 токенов вместо 2048, при той же памяти это удваивает размер батча, а пропускная способность на токен почти не меняется.

Отдельный раздел поста, про чистую инженерную пропускную способность, измеренную сначала на одной домашней RTX 5090 (до аренды B200): на модели 858М, bf16, с компиляцией, базовый показатель, 26 144 токена/с, финальный, 37 621 токен/с. Вклад дали: FP8 на всех трёх матричных умножениях (+25%), паддинг словаря (+33% нарастающим итогом), объединённый линейный слой и кросс-энтропия из библиотеки Liger (+44% нарастающим итогом, сам по себе приём на 6% медленнее на шаг, но освобождает 8 ГБ видеопамяти на RTX 5090, и рост микробатча с лихвой окупает это замедление; автор упоминает, что Claude, у которого он спросил совета, сходу забраковал этот приём именно из-за замедления на шаг, тем не менее автор его оставил), MLP без гейта (relu² вместо SwiGLU: 183 035 → 214 173 токена/с и на 6 ГБ меньше памяти на малой модели, при этом отношение промежуточного слоя 2,75×, обычное для SwiGLU, на relu² не переносится, там нужно 4×), и хранение master-весов оптимизатора в bf16 вместо fp32 (на отдельном конфиге в 1,5 млрд параметров, минус 27% видеопамяти и рост пропускной способности с 640 тысяч до 1,4 млн токенов/с, то есть в 2,2 раза, ценой небольшой просадки качества: CORE 0,22 против 0,23 на 4000-м шаге). Отдельно от программных оптимизаций сам переход с RTX 5090 на B200 (модель 150М, FP8) дал 2,59× по скорости, 184 662 → 477 440 токенов/с. На финальном прогоне утилизация GPU составила 92% SM-активности при 40% занятости SM и около 1047 TFLOP/s на один B200 (~25% MFU от пикового FP8 у Blackwell, ~50% от пикового bf16), обычный DistributedDataParallel без шардирования оптимизатора справился, поскольку на одном узле обмен градиентами не был узким местом.

Не всё из опробованного пригодилось. Собственную маску по границам документов (чтобы токены не «видели» соседние документы внутри одного упакованного примера) автор реализовал, а затем удалил: результат оказался неотличим от простой упаковки по принципу best-fit (около 10 строк кода) с выравниванием по BOS-токену, Андрей Карпати ранее приходил к тому же выводу о безвредности «утечки» между документами при такой упаковке, хотя автор оговаривается, что это может зависеть от конкретного датасета. Ядра RoPE и RMSNorm из библиотеки Liger тоже не прижились: RoPE в микробенчмарке было в 2,2 раза быстрее, но не дало заметного эффекта на полном цикле обучения (на этом масштабе RoPE, не узкое место), а RMSNorm оказался даже медленнее встроенной реализации PyTorch 2.9 (0,41 мс против 0,25 мс). Инициализация весов в духе nanochat (эмбеддинги N(0; 0,8), нулевая инициализация выходных проекций, голова модели N(0; 0,001)) вместо обычной для GPT-2 N(0; 0,02) по всем слоям не дала измеримой разницы в качестве, кривые обучения расходятся только на старте и сходятся уже к 1500-му шагу; автор оставил такую инициализацию просто из эстетических соображений. Потоковую загрузку датасета тоже отклонили в пользу локально скачанных шардов: даже при здоровой сети локальные шарды давали на 2, 3% больше пропускной способности и не несли риска зависания загрузки на многочасовом прогоне.

Отдельно автор проверил, действительно ли нужны value-таблицы: в его модели на них приходится 721 млн параметров из 3,848 млрд, то есть 19% всех весов. Сравнение на отметке 12 500 шагов и 29 млрд токенов: с value-таблицами loss 2,1075 против 2,1171 без них (на 0,46% лучше) и CORE 0,3147 против 0,3047 (на 3,2% лучше), а пропускная способность почти не отличалась, 479 445 против 477 908 токенов/с, поскольку эмбеддинги, это лукап-таблицы, которые почти не стоят вычислений, только памяти. Автор оценивает выигрыш в шагах: между 10 000-м и 12 500-м шагом (2500 шагов) базовый loss упал на 0,0194, а выигрыш от value-таблиц (0,0096), это примерно 1200 шагов из 25 000, то есть 19% дополнительных параметров стоят примерно 5% дополнительного обучения. Отдельное наблюдение: CORE сдвинулся примерно в 7 раз сильнее, чем loss (3,2% против 0,46%), по мнению автора, это следствие того, что CORE, в отличие от loss, метрика точности, где элементы у самой границы решения переворачиваются от небольших изменений в логитах, да ещё и считается относительно случайного базового уровня, что усиливает относительные различия при невысоких абсолютных значениях.

Самое методологически интересное наблюдение поста касается самого бенчмарка CORE. При контексте в 1024 токена из 22 задач бенчмарка три фактически не засчитывались. SQuAD, из-за формата: это 10-shot задача (десять примеров-подсказок перед реальным вопросом), медианная длина промпта, 1998 токенов, и целиком не влезает ни один; харнесс при обрезке промпта сохраняет хвост, так что модель почти всегда видела вопрос и отрывок (около 169 токенов), но почти никогда, сами примеры, показывающие формат ответа; а поскольку SQuAD оценивается точным совпадением токенов, беглый, но не в том формате ответ засчитывается как ноль. При этом результат SQuAD не просто стагнировал, а монотонно падал до ровно нуля по ходу обучения (0,1478 → 0,0617 → 0,0099 → 0,0007 → 0,0000): по мере того как модель училась говорить более гладким языком, она переставала случайно генерировать короткие обрывистые ответы, которые иногда случайно совпадали с эталоном, а именно за такие случайные совпадения и начислялись баллы. boolq показал более мягкий вариант того же: пик 0,6294 на 10 000-м шаге, затем снижение до 0,5131. bigbench_language_id почти не отрывался от уровня случайного угадывания. Повторный прогон того же рецепта с контекстом 2048 вместо 1024 (прогон пришлось остановить примерно на 28 000-м из запланированных 32 000 шагов, чтобы сэкономить на аренде, снижение learning rate не успело завершиться, так что итоговое число, по сути, нижняя граница) поднял CORE с 0,3384 до 0,3840, хотя loss на 20 000-м шаге у обоих запусков совпадал до четвёртого знака (2,0160 против 2,0164), автор называет это неожиданно слабой корреляцией между CORE и loss на датасете ClimbMix. Из всего прироста 83% дали именно SQuAD (с 0,0000 до 0,3114, доля обрезанных промптов упала со 100% до 47%) и boolq (с 0,5131 до 0,7095, обрезка снизилась с 99,8% до 3,2%, сырой прирост здесь +0,196, но после центрирования CORE относительно случайного уровня 0,5 для этой задачи превращается в +0,517). bigbench_language_id, несмотря на падение доли обрезанных промптов с 99,7% до 14%, сдвинулся всего на +0,005 и остался для модели самой сложной задачей бенчмарка. Остальные 19 задач суммарно дали только +0,008, примерно то, что и должны дать лишние 14% токенов обучения (65,3 против 57,3 млрд); две задачи даже просели, commonsense_qa на 0,072 и cs_algorithms на 0,031, без объяснения автора. Переход на контекст 2048 стоил 9% пропускной способности (с 480 000 до 437 000 токенов/с). Вывод автора: увеличение контекста было оправдано как решение по измерению, а не по качеству модели, 1024 достаточно для обучения хорошей модели, а 2048 нужен в основном для того, чтобы честно засчитать задачи с длинными промптами.

Автор прямо перечисляет, что осталось непроверенным: пиковый learning rate унаследован от формулы nanochat sqrt(768/d_model) (как и LR оптимизатора Muon, 0,02) и отдельно не подбирался; переход с косинусного графика на трапецеидальный не сравнивался с другими вариантами; QK-norm использовался всегда и ни разу не отключался для проверки; соотношение голов в GQA выбрано ради экономии памяти, а не проверено экспериментально. По его собственным словам, часть архитектурных решений унаследована от nanochat, а не протестирована самостоятельно, то есть он доверяет переносимости результатов Карпати на свою модель, данные и масштаб. Среди открытых вопросов на будущее: сравнить value-таблицы не с их отсутствием, а с альтернативой потратить те же 721 млн параметров иначе; прогнать контексты 1024 и 2048 токенов при одинаковом времени обучения, а не при одинаковом числе шагов; разобраться, почему на длинном контексте просел commonsense_qa; и попробовать шардировать оптимизатор Muon по примеру nanochat, сейчас у автора обычный DistributedDataParallel, при котором каждая GPU хранит полную копию состояния оптимизатора и заново пересчитывает одну и ту же ортогонализацию Ньютона, Шульца; экономия памяти очевидна и превратится в больший батч, а вот избавит ли шардирование от дублирующихся вычислений, зависит от способа шардирования, поскольку Muon требует полную матрицу градиента целиком. Опубликованы ли код фреймворка little-lm, веса модели или сами шарды датасета ClimbMix, в посте не сказано: единственная ссылка на репозиторий в тексте ведёт на nanochat Андрея Карпати, а не на код автора.

Итоговая рамка автора: GPT-2 1,5B был передовым результатом мощной лаборатории в 2019 году; сегодня его результат на CORE удалось превзойти с большим отрывом в одиночку и меньше чем за $1000 арендованного времени GPU, по мнению автора, граница того, что раньше требовало ресурсов целой лаборатории, для инженера-одиночки заметно сдвинулась.

Ключевые факты

  • Независимый разработчик Хьюго Вернь с нуля обучил языковую модель на 3,848 млрд параметров (архитектура в духе Llama, с GQA и value-таблицами) на 65,3 млрд токенов за 43 часа на 8 арендованных GPU B200 и $998, получив результат 0,384 по бенчмарку CORE.
  • При сопоставимом бюджете (~$1000) это выше показателя nanochat d32 Андрея Карпати (0,310) и намного выше GPT-2 от OpenAI (1,5 млрд параметров, 2019 год, 0,2565); более дешёвый вариант той же модели при контексте 1024 токена обошёлся в $820 и дал CORE 0,338.
  • Ключевой рецепт: трапецеидальный график learning rate вместо косинусного, оптимизатор Muon для матричных параметров, датасет ClimbMix вместо FineWeb-Edu и обучение в FP8 с паддингом словаря, вместе они вывели автора из провального первого запуска (858 млн параметров, 5,8 суток на одной GPU A100, результат по PIQA хуже, чем у GPT-2 на 124 млн параметров) к финальному результату.
  • Отдельный эксперимент показал: value-таблицы (721 млн параметров, 19% модели) почти не стоят вычислений (это лукап-таблицы), но дают 3,2% прироста по CORE против 0,46% прироста по loss, CORE сдвинулся примерно в 7 раз сильнее, чем loss, на одном и том же сравнении.
  • При контексте 1024 токена 3 из 22 задач CORE (включая SQuAD) почти не засчитывались, потому что их промпты туда не помещались; результат SQuAD даже монотонно падал до нуля по ходу обучения. Увеличение контекста до 2048 подняло итоговый CORE с 0,338 до 0,384 (83% прироста дали всего две задачи), но стоило 9% скорости обучения.

Почему это важно

Пост фиксирует не эффектный рекорд, а сдвиг в экономике обучения моделей: конкурентоспособная по качеству языковая модель с нуля перестаёт быть привилегией лаборатории с бюджетом в миллионы. Независимый инженер, работавший по вечерам, на арендованном по часам железе и за $998 получил результат по CORE (0,384), который превосходит nanochat d32 Андрея Карпати (0,310) при сопоставимом бюджете (~$1000) и с большим отрывом обходит GPT-2 1,5B (0,2565), модель, которая в 2019 году была передовым результатом хорошо финансируемой лаборатории. Это конкретная точка на кривой удешевления «фронтира»: то, что ещё недавно требовало ресурсов лаборатории, за несколько лет стало по силам одному человеку с несколькими тысячами долларов.

Кому это важно

В первую очередь, инженерам и исследователям, которые сами обучают небольшие модели на ограниченном бюджете: пост даёт конкретный, проверенный на практике набор решений по расписанию learning rate, выбору оптимизатора, данным и точности вычислений. Во вторую, всем, кто читает результаты по бенчмаркам вроде CORE: разбор поста показывает, как длина контекста при оценке, а не качество модели, может радикально исказить итоговый балл на отдельных задачах. И в третью, независимым разработчикам и небольшим командам, которые прикидывают, что реально можно сделать с бюджетом в $1000, 2000 без доступа к инфраструктуре крупной компании.

Как это применить

Из поста можно забрать готовый, проверенный набор решений для обучения небольших decoder-only моделей: трапецеидальный график learning rate (прогрев 5%, плато, линейное снижение на последних 50% шагов до 5% от пика) вместо косинусного спада к нулю, так модель продолжает учиться до конца, а не «простаивает» на последней трети бюджета; оптимизатор Muon для матричных параметров при AdamW для остального, при накоплении градиента за несколько шагов его накладные расходы почти исчезают; обучение в FP8 плюс паддинг словаря до размера, кратного 64, заметный прирост пропускной способности почти бесплатно; отказ от гейта в MLP (relu² вместо SwiGLU, с шириной 4× вместо обычных для SwiGLU 2,75×) и хранение master-весов оптимизатора в bf16 вместо fp32, оба приёма экономят память ценой минимальной просадки качества; локальная закачка шардов датасета вместо потоковой передачи, на многочасовом прогоне это надёжнее и немного быстрее. Отдельный практический урок, про сам бенчмарк: если в оценке участвуют задачи с длинными few-shot промптами (как SQuAD), короткий контекст на инференсе может занизить итоговый балл до нуля независимо от качества модели, контекст для оценки стоит выбирать исходя из того, что реально требуют задачи бенчмарка, а не только из бюджета на обучение.

Можно ли доверять

Это личный отчёт с личного блога: код фреймворка little-lm, веса модели и шарды датасета ClimbMix публично не подтверждены, единственная ссылка на репозиторий в тексте ведёт на nanochat Андрея Карпати, а не на код автора, независимой проверки или рецензирования нет. При этом сам отчёт написан подробно и самокритично: приведён полный конфиг обучения, таблица по трём из 22 задач CORE (девятнадцать остальных, одной агрегированной строкой), и автор честно отмечает, что прогон с контекстом 2048 остановлен раньше срока (на 28 000-м из 32 000 шагов), поэтому его результат, нижняя граница, а не финальное число, и прямо перечисляет нерешённые вопросы вроде необъяснённой просадки на commonsense_qa. Автор также признаёт, что часть архитектурных решений (пиковый learning rate, QK-norm, соотношение голов в GQA) унаследована от nanochat без самостоятельной проверки, то есть часть результата опирается на доверие к чужим более ранним экспериментам, а не только на собственные.

Риски и подводные камни

Главное число из заголовка ($998, CORE 0,384) получено на прогоне, остановленном раньше срока ради экономии на аренде, это нижняя граница, а не гарантированный потолок при полностью завершённом цикле обучения. Сам автор фиксирует случай слабой корреляции: у прогонов на 1024 и 2048 токенах loss на 20-тысячном шаге совпадал до четвёртого знака, а CORE расходился на 0,034, то есть loss и итоговый бенчмарк-балл в этой связке далеко не всегда двигаются синхронно, и полагаться на loss как на единственный сигнал качества рискованно. Есть и показательная деталь про инструменты ИИ в самой разработке: когда автор спросил совета у Claude по одной из оптимизаций (объединённый линейный слой и кросс-энтропия), тот сразу забраковал приём, просто потому, что тот измерялся на 6% медленнее на шаг; автор всё равно его оставил, поскольку высвобождённая видеопамять с лихвой окупалась ростом батча. Узкая метрика в отрыве от общей картины дала неверный вердикт, стоит держать в уме, когда опираешься на оценку ИИ-ассистента по локальному, а не сквозному критерию.

«То, для чего раньше требовалась лаборатория, теперь под силу одному инженеру по вечерам.»

— Хьюго Вернь, автор проекта little-lm