Статьи

🧠 Собираем свой ChatGPT за $100: Полное руководство по nanochat Андрея Карпаты

2025-10-14 13:39 LLM

Андрей Карпаты, легенда мира AI и один из создателей OpenAI и Tesla Autopilot, снова встряхнул сообщество. Он выложил nanochat — проект, который можно назвать одним из самых полезных репозиториев за последнее время.

В чем суть? Это полный конвейер для создания LLM-клона ChatGPT с нуля. Всего 8000 строк чистого, минималистичного кода, который демистифицирует весь процесс: от обучения токенизатора и предобучения модели до файнтюнинга и даже обучения с подкреплением (RL).

Карпаты делает "спидран": сборку языковой модели, которую можно реализовать за $100 и примерно 4 часа работы на облачном сервере с 8xH100 GPU.

Мы разберем проект nanochat по винтикам и поймем, что происходит на каждом этапе.

«Спидран» за $100

Карпаты арендует мощный сервер (например, в Lambda GPU Cloud) с восемью видеокартами H100. Такая машина стоит примерно $24 в час. Бюджет ограничен сотней долларов. Это значит, что есть чуть больше 4 часов, чтобы из хаоса интернет-текстов родилась маленькая языковая модель.

[!NOTE] Философия nanochat Весь репозиторий пронизан идеей минимализма. Карпаты сознательно избегает громоздких фреймворков и лишних зависимостей. Его цель — создать понятную, связную и легко модифицируемую кодовую базу, которую можно изучать, форкать и использовать как основу для собственных исследований.

В репозитории есть скрипт speedrun.sh, который автоматизирует весь процесс от А до Я. Но мы же здесь не для того, чтобы просто нажать кнопку, верно? Мы пройдем по этому скрипту шаг за шагом, комментируя каждую деталь.

Поехали: Пошаговый разбор

Итак, мы залогинились на наш свежеарендованный сервер. Время пошло.

Шаг 1: Подготовка окружения — uv и Rust

Первым делом — клонируем репозиторий и настраиваем рабочее окружение.

git clone https://github.com/karpathy/nanochat.git
cd nanochat

Дальше начинается интересное. Карпаты использует не pip или conda, а uv.

# Устанавливаем uv, если его еще нет
command -v uv &> /dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh
# Создаем локальное виртуальное окружение .venv
[ -d ".venv" ] || uv venv
# Устанавливаем зависимости
uv sync
# Активируем окружение
source .venv/bin/activate

[!INFO] Что за uv? uv — новый и чертовски быстрый менеджер пакетов и виртуальных окружений для Python, написанный на Rust. Он позиционируется как замена pip и venv, и его главное преимущество — скорость.

Следующий этап может удивить: нам понадобится Rust.

# Устанавливаем Rust и его пакетный менеджер Cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
# Собираем кастомный токенизатор на Rust
uv run maturin develop --release --manifest-path rustbpe/Cargo.toml

Зачем здесь Rust? Ответ прост: для обучения токенизатора. Карпаты отмечает, что его предыдущая реализация на чистом Python (minbpe) оказалась слишком медленной, а готовые решения от Hugging Face — излишне громоздкими и запутанными. В мире, где скорость — наше всё, кастомный токенизатор на Rust — это оправданный шаг. Утилита maturin как раз и служит мостом, позволяя легко собирать Rust-код в Python-модуль.

На этом подготовка завершена. Наше окружение готово к созданию мозга нашей будущей модели.

Шаг 2: Обучение токенизатора — Создаем словарь для нашего LLM

Прежде чем модель сможет "читать" текст, ей нужен словарь. Токенизатор — это и есть этот словарь. Он разбивает текст на осмысленные кусочки (токены) и переводит их в числа, понятные машине.

Сначала — данные

Чтобы обучить токенизатор, а затем и саму модель, нам нужны данные. Много данных. Карпаты использует датасет FineWeb-EDU — это гигантская коллекция отфильтрованных веб-страниц образовательного характера.

Чтобы не тащить тяжеловесную библиотеку huggingface/datasets, Карпаты сделал простую вещь: он перепаковал датасет в удобные для стриминга "шарды" и выложил их на Hugging Face Hub. Каждый шард — это parquet-файл, сжатый gzip, размером около 100 МБ.

Для модели с глубиной 20 (d20) нам понадобится 240 таких шардов. Это примерно 24 ГБ данных. На облачном сервере они скачиваются довольно быстро.

# Запускаем скачивание 240 шардов данных.
# По умолчанию они попадут в ~/.cache/nanochat
python -m nanochat.dataset -n 240

[!TIP] В скрипте speedrun.sh Карпаты хитро запускает скачивание 240 шардов в фоновом режиме, в то время как обучение токенизатора начинается на первых 8 уже скачанных шардах. Это отличный пример оптимизации времени!

Запускаем обучение

Как только первые данные на месте, мы можем обучить наш словарь. Мы будем создавать словарь размером 65,536 токенов (2**16). Обучение пройдет на первых 2 миллиардах символов и займет всего около минуты.

python -m scripts.tok_train --max_chars=2000000000
python -m scripts.tok_eval

После обучения скрипт tok_eval выдает интересный отчет. Главный показатель — коэффициент сжатия (compression ratio). Он показывает, сколько в среднем символов исходного текста "упаковывается" в один токен. У нас он будет в районе 4.8.

Карпаты также сравнивает этот свежеобученный токенизатор с эталонными от OpenAI: GPT-2 и GPT-4.

  • По сравнению с GPT-2 (50,257 токенов): токенизатор сжимает текст значительно лучше почти по всем категориям, немного уступая лишь в математических формулах.
  • По сравнению с GPT-4 (100,277 токенов): Здесь он ожидаемо проигрывает, особенно на многоязычных текстах, коде и математике. У GPT-4 просто больше "словарный запас". Но что интересно, на самом датасете FineWeb наш токенизатор даже немного обгоняет GPT-4, потому что он идеально "заточен" под распределение данных, на которых обучался.

Наш собственный, кастомный и эффективный словарь готов. Модель почти готова начать учиться.

Шаг 3: Pretraining (Предобучение) — Рождение интеллекта

Это самый важный и вычислительно дорогой этап. Здесь LLM будет впитывать в себя знания из гигабайтов текста. Процесс прост по своей сути: модель учится предсказывать следующее слово (точнее, токен) в последовательности. Именно так она изучает грамматику, факты, логику и обретает "знания о мире".

Последние приготовления

Перед запуском нам нужен еще один компонент — "eval bundle". Это набор данных для оценки метрики CORE, которая измеряет "общую эрудицию" модели на 22 различных задачах (ответов на вопросы, логических задач и т.д.).

# Скачиваем и распаковываем eval_bundle
curl -L -o eval_bundle.zip https://karpathy-public.s3.us-west-2.amazonaws.com/eval_bundle.zip
unzip -q eval_bundle.zip
rm eval_bundle.zip
mv eval_bundle "$HOME/.cache/nanochat"

Для визуализации процесса обучения можно использовать wandb (Weights & Biases). Это позволит в реальном времени следить за графиками потерь и метрик.

# Если еще не сделали, залогиньтесь в wandb
wandb login

Запускаем!

Теперь все готово для запуска самой тяжелой артиллерии. Мы запускаем скрипт base_train.py на всех 8 GPU. Мы будем обучать модель-трансформер с 20 слоями (depth=20).

torchrun --standalone --nproc_per_node=8 -m scripts.base_train -- --depth=20

[!NOTE] Если вы настроили wandb, добавьте флаг --run=speedrun (или любое другое имя), чтобы логи отправлялись в ваш проект.

После запуска в консоли появится много информации:

Vocab size: 65,536
num_layers: 20
model_dim: 1280
...
Number of parameters: 560,988,160
...
Total number of training tokens: 11,219,763,200
Tokens : Params ratio: 20.00
Total training FLOPs estimate: 3.917670e+19
...
step 00001/21400 (0.00%) | loss: 10.808654 | ... | tok/sec: 807,569 | mfu: 35.64 | ...
step 00002/21400 (0.01%) | loss: 10.179083 | ... | tok/sec: 1,110,094 | mfu: 48.99 | ...
...

Что значат все эти цифры?

  • Parameters: Наша модель имеет ~560 миллионов параметров.
  • Tokens:Params ratio: Чтобы соответствовать закону масштабирования Chinchilla, модели такого размера нужно "скормить" примерно в 20 раз больше токенов, чем у нее параметров. Это около 11.2 миллиардов токенов.
  • FLOPs: Весь процесс обучения потребует ~4e19 (40 экзафлопс) вычислительных операций. Это и есть "мощность" будущей модели.
  • Iterations: Исходя из размера батча и общего количества токенов, понадобится ~21,400 шагов оптимизации.
  • tok/sec & mfu: tok/sec — это скорость обработки токенов в секунду. mfu (Model FLOPs Utilization) — это процент утилизации теоретической вычислительной мощности GPU. Значение в районе 50% — это очень хороший показатель.

Теперь остается только ждать. Процесс займет около 3 часов. В это время 8 видеокарт H100 будут трудиться на полную, а мы можем наблюдать за магией в wandb. График loss (ошибки) должен плавно снижаться, а метрика CORE — расти. Это значит, что модель становится умнее.

Первые результаты

По завершении предобучения наша модель — это, по сути, очень навороченный автокомплит. Она знает факты, но еще не умеет общаться в формате диалога. Мы можем проверить ее базовые знания:

# Запускаем скрипты для оценки потерь и метрик
torchrun --standalone --nproc_per_node=8 -m scripts.base_loss
torchrun --standalone --nproc_per_node=8 -m scripts.base_eval

Модель достигает метрики CORE около 0.22. Для сравнения, это чуть лучше, чем GPT-2 Large (0.21), но немного хуже, чем GPT-2 XL (0.26).

Проверим на простых запросах:

  • The capital of France is -> Paris. (Знает!)
  • The chemical symbol of gold is -> Au. (Знает!)
  • The planets of the solar system are: -> Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, (Знает!)
  • If 5*x + 3 = 13, then x is -> a positive integer. (Математику пока не осилила, но поняла, что речь о числах).

Неплохо для модели, на обучение которой ушло $72. Но это еще не чат-бот. Чтобы научить ее диалогу, мы переходим к следующим этапам.

Шаг 4: Midtraining — Учим модель правилам хорошего тона

Модель уже выучила слова и факты, но пока не умеет вести диалог. Этап midtraining (промежуточное дообучение) — это как отправить её в школу, где её научат общаться, решать задачки и пользоваться инструментами.

Этот этап гораздо быстрее предобучения и занимает всего около 8 минут. Здесь мы дообучаем нашу базовую модель на новом типе данных — на диалогах. Алгоритмически это то же самое обучение, но цели у него другие.

Чему мы учим модель на этом этапе?

  1. Формату диалога: Модель знакомится со специальными токенами, такими как <|user_start|>, <|assistant_start|> и <|assistant_end|>. Она учится понимать структуру "вопрос-ответ".
  2. Решать тесты (Multiple Choice): Маленькие модели, в отличие от гигантов вроде GPT-4, не всегда понимают сам формат тестов с вариантами A, B, C, D. На этом этапе мы "подмешиваем" в обучающие данные 100 тысяч вопросов из датасета MMLU. Модель не столько учит новые факты, сколько учится алгоритму: "увидел вопрос и варианты -> выбери правильную букву". Это критически важно, так как многие бенчмарки (MMLU, ARC) построены именно на таких тестах.
  3. Использовать инструменты: Мы учим модель пользоваться калькулятором. Это реализуется через специальные токены <|python_start|> и <|python_end|>. Модель учится "заворачивать" математические выражения в эти теги, чтобы внешний интерпретатор мог их вычислить. Это необходимо для решения математических задач из датасета GSM8K.

Запускаем Midtraining

Процесс запускается одной командой:

torchrun --standalone --nproc_per_node=8 -m scripts.mid_train

После завершения этого короткого, но важного этапа, модель превращается из простого автокомплита в полноценного чат-ассистента. Теперь мы можем оценить ее производительность на специализированных "чатовых" бенчмарках.

torchrun --standalone --nproc_per_node=8 -m scripts.chat_eval -- -i mid

Флаг -i mid указывает скрипту, что нужно оценивать модель после этапа midtraining. Результаты будут примерно такими:

  • MMLU: 0.3111 (на 31% вопросов отвечает верно, что лучше случайного угадывания 25%)
  • GSM8K: 0.0250 (решает 2.5% простых математических задач)
  • HumanEval: 0.0671 (решает 6.7% задач на программирование)

Показатели пока скромные, но мы видим, что модель уже приобрела новые навыки. Чтобы "затянуть гайки" и еще немного улучшить ее производительность, переходим к следующему этапу.

Шаг 5: Supervised Finetuning (SFT) — Финальная полировка

SFT (контролируемое дообучение) — это еще один короткий раунд файнтюнинга на диалогах. Он длится около 7 минут и служит для финальной "полировки" модели.

В чем отличие от Midtraining?

  1. Качество данных: В идеале, на этапе SFT используются только самые качественные, "красивые" примеры диалогов. Здесь же обычно происходит обучение безопасности (например, как правильно отказывать в выполнении вредоносных запросов). Наша модель пока не настолько умна, чтобы представлять опасность, так что мы этот аспект опускаем.
  2. Формат данных: Главное техническое отличие — на этом этапе данные подаются в модель точно в таком же виде, как и во время реального использования (инференса). На этапах pretraining и midtraining для эффективности длинные тексты и диалоги "склеивались" в большие батчи. SFT устраняет это небольшое несоответствие, что дает маленький, но заметный прирост в качестве.

Запускаем SFT и последующую оценку:

torchrun --standalone --nproc_per_node=8 -m scripts.chat_sft
torchrun --standalone --nproc_per_node=8 -m scripts.chat_eval -- -i sft

Результаты снова немного подрастают:

  • MMLU: 0.3151 (было 0.3111)
  • GSM8K: 0.0455 (было 0.0250)
  • HumanEval: 0.0854 (было 0.0671)

Время поговорить!

Наконец-то! Наша модель полностью готова к диалогу. Мы можем пообщаться с ней прямо в терминале или через удобный веб-интерфейс.

# Вариант 1: Чат в командной строке
python -m scripts.chat_cli

# Вариант 2: Веб-интерфейс в стиле ChatGPT
python -m scripts.chat_web

При запуске chat_web скрипт поднимет веб-сервер. Чтобы получить доступ, нужно в браузере ввести публичный IP-адрес вашего сервера и порт 8000, например: http://209.20.xxx.xxx:8000/.

И вот он, результат трудов:

Конечно, такая модель не выиграет конкурс поэзии или олимпиаду по физике. Но сам факт, что можно с нуля создать вполне сносного собеседника, потратив меньше $100 и 4 часов, впечатляет.

Шаг 6 Reinforcement Learning (RL) — Тренировка на результат

Последний этап, который в speedrun.sh по умолчанию закомментирован, — это обучение с подкреплением (Reinforcement Learning). В больших моделях RLHF (RL from Human Feedback) используется для тонкой настройки и согласования ответов модели с человеческими предпочтениями.

В нашем масштабе это не так критично. Но есть одна область, где RL может дать заметный прирост, — это задачи с четким и объективным критерием правильности. Например, математические задачи из GSM8K. Ответ либо правильный, либо нет.

Скрипт chat_rl запускает простой цикл RL:

  1. Модель генерирует несколько вариантов решения для задачи из GSM8K.
  2. Каждое решение проверяется: если ответ верный, решение получает "награду" (reward) = 1, иначе 0.
  3. Модель дообучается на тех вариантах, которые получили высокую награду.

Этот процесс "натаскивает" модель на генерацию правильных ответов.

# Запускаем RL-обучение
torchrun --standalone --nproc_per_node=8 -m scripts.chat_rl
# Оцениваем результат только на GSM8K
torchrun --standalone --nproc_per_node=8 -m scripts.chat_eval -- -i rl -a GSM8K

Процесс RL довольно медленный, так как модель получает очень мало "битов" информации (только 1 или 0 на целое решение). По умолчанию он занимает около 1.5 часов. Но результат заметен: точность на GSM8K может вырасти еще, например, с 4.5% до 7.5%.

Карпаты отмечает, что этот этап пока не очень хорошо настроен и создает модель, "заточенную" под математику, а не универсального чат-бота. Поэтому он и является опциональным.

Что мы получили за свои $100?

По завершении всех этапов в директории проекта появляется файл report.md. Это своего рода "табель успеваемости" спидрана, содержащий всю ключевую информацию о запуске и итоговые метрики.

Отчет начинается с раздела, который показывает, насколько минималистичен сам проект:

  • Characters: 333,989
  • Lines: 8,304
  • Files: 44
  • Dependencies (uv.lock lines): 2,004

Всего 8 тысяч строк кода и минимум зависимостей для создания целого ChatGPT-клона!

Но самое интересное — это итоговая таблица с метриками на каждом этапе.

Metric BASE MID SFT RL
CORE 0.2219 - - -
ARC-Challenge - 0.2875 0.2807 -
ARC-Easy - 0.3561 0.3876 -
GSM8K - 0.0250 0.0455 0.0758
HumanEval - 0.0671 0.0854 -
MMLU - 0.3111 0.3151 -
ChatCORE - 0.0730 0.0884 -

Что здесь видно:

  • BASE: предобученная модель имеет CORE-метрику 0.22, подтверждая уровень чуть выше GPT-2 Large.
  • MID -> SFT: Мы видим четкий прогресс. После midtraining модель уже показывает базовые способности в решении задач (MMLU, GSM8K, HumanEval). После SFT эти показатели еще немного подрастают.
  • RL: Опциональный этап RL дает самый большой прирост в конкретной задаче, на которой он обучался — GSM8K (с 4.5% до 7.5%).

И главный итог: Total wall clock time: 3h51m

С учетом стоимости сервера $24/час, общие затраты составили (3 + 51/60) * 24 = $92.4.

Что дальше?

Самое прекрасное в nanochat то, что Карпаты дает в руки не просто инструмент, а целую песочницу для экспериментов. Вы можете менять все: токенизатор, данные для обучения, гиперпараметры.

Самый простой способ получить более мощную модель — увеличить ее глубину. Это делается с помощью одного-единственного параметра --depth на этапе base_train. Код устроен так, что все остальные параметры (ширина слоев, learning rate и т.д.) подстроятся автоматически.

Например, чтобы получить модель уровня GPT-2 (CORE-метрика ~0.25), можно попробовать depth=26. Но для этого потребуется больше данных, и, что важнее, придется уменьшить размер батча на каждом устройстве, чтобы не словить ошибку нехватки видеопамяти (OOM).

Пример для обучения модели d26 (~12 часов, ~$300):

# Уменьшаем device_batch_size с 32 до 16
torchrun --standalone --nproc_per_node=8 -m scripts.base_train -- --depth=26 --device_batch_size=16

Код автоматически поймет, что для достижения целевого общего размера батча ему теперь нужно делать 2 шага градиентной аккумуляции вместо одного.

Пример для обучения модели d30:

# Уменьшаем device_batch_size еще сильнее
torchrun --standalone --nproc_per_node=8 -m scripts.base_train -- --depth=30 --device_batch_size=8

Не бойтесь читать и изменять код. Он написан предельно чисто и хорошо прокомментирован.

Заключение

В итоге, nanochat — это мощнейший образовательный инструмент, который снимает завесу тайны с процесса создания больших языковых моделей. Он демократизирует доступ к технологиям, которые еще вчера казались уделом мегакорпораций.

Проект Карпаты доказывает, что для серьезных экспериментов и получения работающих моделей не всегда нужны многомиллионные бюджеты. Он дает в руки сообществу "сильный базовый" стек (strong baseline), который можно использовать как отправную точку для собственных исследований, обучения и создания новых продуктов.

Экспериментируйте!