Всё началось с наивной мысли: зачем платить за API или запускать 7B-модель, если нужна небольшая модель для простых разговоров на одном языке? Большие модели умеют всё и сразу, но это избыточно. Казалось, что 0.7B, заточенная под один язык и стиль общения, справится не хуже.
Спойлер: это было наивно. Но путь оказался ценнее результата.
В этой статье — как я прошёл путь от стандартного nanoGPT до кастомной архитектуры с RoPE, SwiGLU и GQA, собрал русскоязычный корпус с нуля и реализовал распределённое обучение на бесплатных Colab-воркерах через Google Drive.
Почему не взять готовую модель?
Честный ответ: хотел понять, как это работает изнутри. Взять Qwen или Llama и сделать файнтюнинг — просто и эффективно. Но когда строишь модель с нуля, каждая деталь перестаёт быть магией.
Была и конкретная задача: нужен персонаж с определённым стилем речи на русском. Казалось логичным — меньше модель, меньше ресурсов, проще управлять поведением. Позже стало ясно: 0.7B — это слишком мало для качественной генерации связного текста.
Датасет: какой язык похож на нужный мне
Перед сбором данных встал неочевидный вопрос: какой стиль русского языка нужен? Официальные новости и научные тексты — нет. Нужен живой, разговорный, эмоциональный язык.
Корпус собрал из трёх частей:
- Taiga — готовый русскоязычный корпус: новости, журналы, художественная литература, субтитры. Хорошая база, но стиль неоднородный.
- Собственный скрейп — около 566 тысяч документов из игровых медиа, блог-платформ и литературных сообществ. Здесь живой язык: эмоции, сленг, неформальные обсуждения.
- FineWeb2 (rus_Cyrl) — веб-корпус, но сырой. Пришлось фильтровать стриминговым пайплайном.
Фильтрация FineWeb2
Веб-данные — это мусор по умолчанию: SEO-тексты, прайс-листы, резюме, битые символы. Написал стриминговый фильтр, чтобы не грузить всё в память.
Добавил список стоп-слов («резюме», «прайс-лист», «seo», «вакансия» и т.д.) — это быстро отсекает большую часть мусора до тяжёлых проверок.
Итого: около 12 млрд токенов после фильтрации.
Токенизатор
Создал кастомный BPE-токенизатор на 51 200 токенов, обученный на русскоязычном корпусе. Стандартный токенизатор GPT-2 плохо справляется с русским — слова разбиваются на мелкие части, и контекстное окено расходуется неэффективно.
Эволюция архитектуры: пять итераций
Итерация 1 — GPT-2 Small, базовый старт
Первая модель — чистый nanoGPT без изменений: 124 млн параметров, fp32, без компиляции.
Работает, loss падает, но медленно. Генерация слабая — данных слишком мало. Добавил torch.compile и переход на fp16. Это дало заметный прирост скорости без изменений в архитектуре.
Итерация 2 — GPT-2 Small, динамические гиперпараметры
Эксперимент: изменение гиперпараметров в процессе обучения без перезапуска. Постепенно увеличивал batch size и gradient accumulation, подбирал weight decay.
Прогресс есть: большой gradient accumulation даёт стабильнее обучение. Но производительность падает.
Итерация 3 — GPT-2 Medium, переход на 345 млн параметров
Та же архитектура, больше параметров. Добавил dropout во второй половине обучения.
Итерация 4 — GPT-2 Large, распределённое обучение
GPT-2 Large (774 млн параметров) влезала в одну Colab-сессию, но только с gradient checkpointing. Активации не сохраняются при forward и пересчитываются при backward. Экономия памяти, но скорость падает вдвое. Обучение стало невыносимо медленным.
Из-за этого появилась распределённая схема — об этом далее.
При 5 воркерах ускорение — около x2 относительно одного. Казалось бы, мало, но на практике это разница между «обучение идёт» и «обучение стоит». После 15 млрд токенов качество генерации стало ощутимо лучше.
Итерация 5 — кастомная архитектура
Финальная модель — переработанная архитектура с современными компонентами. По сути, это то, что отличает LLaMA от GPT-2.
Использованы:
- RoPE — относительные позиционные эмбеддинги через поворот векторов Q и K. Лучше экстраполирует на длинные контексты.
- SwiGLU — вместо одной матрицы в FFN — две с gate-механизмом. hidden_dim = int(4 * d_model * 2/3), чтобы сохранить FLOPs.
- GQA — Grouped Query Attention. Одна группа KV на несколько Q-голов. Меньше памяти, быстрее инференс, особенно с длинным контекстом.
- Flash Attention — ускорение вычислений на GPU.
Loss похож на итерацию 3, но модель видела меньше токенов. Архитектура эффективнее использует параметры, а инференс — заметно быстрее.
Детали архитектуры
RoPE заменяет абсолютные позиционные эмбеддинги — расстояние между токенами кодируется в attention.
SwiGLU в FFN даёт лучшее качество при тех же вычислительных затратах.
GQA снижает объём KV-кеша и ускоряет инференс.
Selective Gradient Checkpointing
Полный gradient checkpointing режет память вдвое, но замедляет обучение на ~30%. Решение — чекпоинтить только часть слоёв.
На практике чекпоинтинг чётных слоёв дал лучший баланс между памятью и скоростью.
Распределённое обучение на Colab через Google Drive
Самая нестандартная часть. GPT-2 Large с полным gradient checkpointing обучалась слишком медленно на одной сессии. Нужно было распараллелить.
Стандартный DDP (NCCL/Gloo) невозможен в Colab — нет постоянных IP и общей сети. Решение: использовать Google Drive как шину передачи градиентов.
Идея: Google Drive как шина градиентов
Google Диск в Colab монтируется как файловая система. Каждый воркер пишет и читает файлы. Градиенты передаются через файлы.
Каждый воркер знает свой worker_id и общее число count_workers. После backward pass сохраняет градиенты под своим ID и номером итерации, затем ждёт файлы от других.
Усреднение градиентов делает каждый воркер самостоятельно.
Инфраструктура: несколько аккаунтов Google
Каждый воркер — отдельная Colab-сессия под своим Google-аккаунтом. Общая папка на Google Drive, открытая для всех, используется как точка синхронизации. Каждый воркер монтирует свой диск, но имеет доступ к общей папке — туда и пишутся градиенты.
Два процесса на каждом воркере
В одной сессии работали два процесса. Обучение запускалось через subprocess.Popen — фоново, не блокируя ячейку.
Вторая ячейка запускала даунлоадер — следил за появлением файлов градиентов других воркеров и скачивал их локально.
Флаги как сигнал готовности
Google Drive не синхронизирует незавершённые файлы, но при скачивании файл может появиться до полной загрузки.
Решение: после скачивания даунлоадер создавал локальный .flag-файл.
Процесс обучения ждал флаг, а не сам файл. Это гарантирует, что градиенты полностью загружены.
Градиенты в float16
Файлы градиентов для Large-модели весят сотни мегабайт. Сохранение в float32 — слишком медленно. Решение: сохранять в float16, конвертировать при загрузке.
Ограничения подхода
- Скорость синхронизации: ожидание Google Drive — самый медленный этап. Несколько секунд на итерацию накапливаются.
- КПД распараллеливания: 5 воркеров дали ускорение x2, а не x5. Большая часть времени — ожидание.
- Нестабильность: Colab-сессии падают без предупреждения. Обязательно нужно уметь возобновлять обучение с чекпоинта.
- Масштаб: больше 5–6 воркеров — координация через файлы становится узким местом.
Для прода такой подход не годится. Для бесплатного обучения на Colab — работает.
Результаты и выводы
Что я понял
0.7B — это мало. Для нормальных связных диалогов нужно минимум 3–7B, и даже тогда требуется качественный файнтюнинг. Идея, что «маленькая специализированная модель заменит большую», на практике работает хуже ожиданий.
Данные важнее архитектуры. Модель v4 с простой GPT-2 Large-архитектурой и 15 млрд токенов показала лучший loss, чем v5 с современной архитектурой, но 5 млрд токенов. Качество и объём данных важнее архитектурных улучшений на малых масштабах.
Распределённое обучение — не только про скорость. Два воркера с синхронизацией через Drive позволили обучить модель, которая не помещалась в одну сессию. Это важнее ускорения.
Chinchilla-оптимум достигли только v2 и v4. Около 20 токенов на параметр. Это объясняет их отрыв от других итераций лучше, чем любые архитектурные детали. Остальные модели просто недообучены из-за нехватки данных.