MiniMind — LLM за 2 часа
Разбираем китайский учебный проект MiniMind — архитектуру Dense и MoE, токенизатор и весь конвейер pretrain → SFT → LoRA → DPO/GRPO — и решаем, что из этого повторить на CPU и на одной GPU.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В модуле 4.6 вы собрали GPT на PyTorch и обучили её на русских текстах. Это был трансформер образца 2019 года и только одна стадия — предобучениепредобучение, претрейнEN pretraining. Настоящая LLM 2026 года отличается в двух местах: блок модели современнее (RMSNorm, RoPE, SwiGLU, GQA, иногда MoE), а после претрейна идёт длинный конвейер пост-обучения.
MiniMind — лучшая «карта» этого пути. Это открытый китайский проект разработчика Гун Цзинъяо (GitHub: jingyaogong), лицензия Apache-2.0, один из самых популярных учебных LLM-репозиториев на GitHub. В нём есть весь конвейер от токенизатора до RL с инструментами, написанный на чистом PyTorch, и модели настолько маленькие, что их можно обучить дома. Автор формулирует идею так: «собрать самолёт из Lego самому гораздо интереснее, чем лететь в первом классе».
В этом уроке разберём, что внутри MiniMind. В следующем — спланируем свой проект: мини-LLM на русском.
Что такое MiniMind
Заголовок раздела «Что такое MiniMind»Главное обещание проекта: обучить с нуля маленькую языковую модель за пару часов и несколько юаней. По таблице автора
для minimind-3 на одной RTX 3090 предобучение на pretrain_t2t_mini занимает около 1.2 часа, SFT на sft_t2t_mini —
около 1.1 часа. Вместе это примерно 2.3 часа и 3 юаня аренды (по 1.3 юаня за час). Модель на 64M параметров примерно
в 2700 раз меньше GPT-3.
Важная особенность: все ключевые алгоритмы написаны с нуля на PyTorch. LoRA, DPO, PPO, GRPO, CISPO, дистилляция —
без обёрток trl и peft. При этом модели совместимы с экосистемой: их можно загрузить через transformers и запустить
в llama.cpp, vLLM или Ollama.
| Версия | Дата | Модели |
|---|---|---|
| minimind-v1 | 08–09.2024 | 26M, 108M, MoE 4 × 26M |
| minimind2 | 04.2025 | 26M (small), 104M, 145M (MoE) |
| minimind-3 | 01.04.2026 | 64M (Dense), 198M-A64M (MoE) |
У проекта есть «родственники»: MiniMind-V (зрение), MiniMind-O (омни-модальность), экспериментальные диффузионная языковая модель и модель с линейным вниманием. Дальше говорим о текущей основной ветке minimind-3.
Архитектура minimind-3
Заголовок раздела «Архитектура minimind-3»Автор прямо пишет, что структура выровнена под экосистему Qwen3: так проще конвертировать модель в форматы
transformers, llama.cpp и vLLM. Получается тот же «современный блок», что вы разбирали в уроке 4.5.3:
| Параметр | Значение | Где в курсе |
|---|---|---|
| Тип | decoder-only, Pre-Norm | 4.5.1 |
| Нормализация | RMSNorm, eps = 1e-6 | 4.5.3 |
| Позиции | ротационные позиционные эмбеддингиротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM., , до 32 768 позиций, YaRN на инференсе | 4.5.2, 5.5.1 |
| Внимание | внимание с группами запросоввнимание с группами запросов (GQA)EN grouped-query attention (GQA)Головы запросов делятся на G групп, у каждой группы своя пара K/V. G = H — это MHA, G = 1 — MQA. Стандарт Llama 3 и Qwen (Ainslie и др., 2023).: 8 голов запросов, 4 KV-головы, размер головы 96; QK-Norm | 4.5.3, 5.2.1 |
| FFN | SwiGLU, скрытый размер | 4.5.3 |
| Словарь | 6400 токенов, эмбеддинги общие с LM-головой | 4.2, 4.6.2 |
| Слоёв × ширина | 8 × 768 | — |
| Bias в линейных слоях | нет | 4.5.3 |
Две детали, которых не было в нашей GPT из 4.6. QK-Norm (вы видели его в уроке 4.5.3) — RMSNorm, применённый к и каждой головы перед RoPE: он не даёт attention-логитам разрастаться и стабилизирует обучение (так делает Qwen3). И необычный размер FFN: вместо привычных автор берёт , округлённое вверх до кратного 64.
flowchart TB
T["Токены (словарь 6400)"] --> E["Эмбеддинги 6400 × 768"]
E --> N1
subgraph BL ["Блок minimind-3, повторяется 8 раз"]
N1["RMSNorm"] --> A["GQA: 8 Q-голов, 4 KV-головы<br/>QK-Norm + RoPE"]
A --> R1(("+"))
R1 --> N2["RMSNorm"] --> F["SwiGLU: 768 → 2432 → 768"]
F --> R2(("+"))
R1 -.-> R2
end
E -.-> R1
R2 --> N3["RMSNorm"] --> H["LM-голова (веса = эмбеддинги)"] --> L["Логиты"]Пересчитаем 64M параметров
Заголовок раздела «Пересчитаем 64M параметров»Хороший способ проверить, что вы поняли архитектуру, — посчитать параметры руками. Этот код запускается на чистом Python:
import math
def minimind_params(vocab=6400, d=768, layers=8, q_heads=8, kv_heads=4, experts=0): hd = d // q_heads # размер головы: 768 / 8 = 96 inter = math.ceil(d * math.pi / 64) * 64 # FFN: ceil(768·π / 64)·64 = 2432 attn = d * q_heads * hd + 2 * d * kv_heads * hd + q_heads * hd * d + 2 * hd # Q, K, V, O + QK-Norm ffn = 3 * d * inter # SwiGLU: gate, up, down moe = experts * ffn + d * experts if experts else ffn # эксперты + роутер layer = attn + moe + 2 * d # + два RMSNorm total = layers * layer + vocab * d + d # + эмбеддинги (общие с LM-головой) + финальный RMSNorm active = layers * (attn + ffn + 2 * d) + vocab * d + d return total, active
for name, kw in [("minimind-3", {}), ("minimind-3-moe", {"experts": 4}), ("CPU-версия", {"d": 256, "layers": 4}), ("d=512, 8 слоёв", {"d": 512})]: total, active = minimind_params(**kw) print(f"{name:15s} всего {total / 1e6:6.1f}M, активных {active / 1e6:5.1f}M")minimind-3 всего 63.9M, активных 63.9Mminimind-3-moe всего 198.4M, активных 63.9MCPU-версия всего 5.0M, активных 5.0Md=512, 8 слоёв всего 30.0M, активных 30.0MИз 63.9M параметров на SwiGLU приходится 44.8M (70%), на внимание — 14.2M (22%), на эмбеддинги — 4.9M (7.7%).
Почему словарь такой маленький. У Qwen2 словарь 151 643 токена. Будь он у minimind-3, одна матрица эмбеддингов заняла бы M параметров — почти вдвое больше всей модели. Маленькой модели выгоднее потратить параметры на слои, а за маленький словарь заплатить более длинными последовательностями.
Почему 8 слоёв по 768, а не 16 по 512. В README автор ссылается на MobileLLM (Meta, 2024): при фиксированном числе параметров маленьким моделям «глубокая и узкая» форма обычно выгоднее «широкой и мелкой». Но у узости есть предел: по опыту MiniMind при меньше 512 качество заметно страдает. Восемь слоёв по 768 — компромисс между скоростью (мелкая сеть быстрее), стабильностью и качеством.
MoE-версия
Заголовок раздела «MoE-версия»minimind-3-moe заменяет FFN в каждом слое на смесь экспертовсмесь экспертов (MoE)EN mixture of experts (MoE) из 4 экспертов с маршрутизацией top-1:
каждый токен идёт ровно в одного эксперта. Всего 198.4M параметров, активных — те же 63.9M. В версии 3 автор убрал
общего эксперта (shared expert), чтобы совпасть с форматом Qwen3-MoE.
Остальное знакомо по модулю 5.3: роутер — линейный слой и softmax, к loss добавляется вспомогательный лосс балансировкивспомогательный лосс балансировки (aux loss)EN auxiliary load-balancing lossДобавка к основному лоссу, штрафующая неравномерную загрузку экспертов. Классическая форма из Switch Transformer — α·N·Σ f_i·P_i. с коэффициентом . Интересный приём для top-1: вес выбранного эксперта в прямом проходе равен ровно 1, а градиент к роутеру проходит «насквозь» (straight-through).
Автор честно предупреждает о подвохе. MoE «быстрее на инференсе», но в чистом PyTorch обучение медленнее: токены раскладывают по экспертам и считают каждого отдельно, а накладные расходы на запуск ядер растут с числом экспертов. Конфигурация «4 эксперта, top-1» по его замерам всего примерно на 50% медленнее плотной модели. Для большего числа экспертов нужны специальные ядра (Triton, DeepSpeed-MoE, Megatron-LM). Для той же задачи в промышленном масштабе DeepSeek выложила библиотеки DeepEP и DeepGEMM.
Токенизатор
Заголовок раздела «Токенизатор»MiniMind использует byte-level BPE (байтовый BPEбайтовый BPE (byte-level BPE)EN byte-level BPE (BBPE)BPE, у которого базовый словарь — 256 байтов UTF-8. Любой текст кодируется без <unk>. Используется в GPT-2, GPT-4o, Llama 3, Qwen, DeepSeek., урок 4.2) на 6400 токенов, обученный библиотекой
HF tokenizers (скрипт trainer/train_tokenizer.py). Из них 36 — специальные токеныспециальный токен (спецтокен)EN special tokenУправляющий токен, а не кусок текста — начало/конец текста (BOS/EOS), заполнитель для батча (PAD), разметка ролей в чате (например, <|im_start|> у Qwen).:
<|endoftext|>(id 0) — паддинг и неизвестный токен;<|im_start|>(id 1) и<|im_end|>(id 2) — начало и конец реплики, как в ChatML у Qwen;<think>,</think>,<tool_call>,</tool_call>,<tool_response>,</tool_response>— для рассуждений и вызова инструментов;- токены в стиле Qwen для картинок, видео, аудио и речи (
<|vision_start|>,<|audio_start|>и другие) — задел под мультимодальные версии; - зарезервированные
<|buffer…|>на будущее.
Сжатие по оценке автора: 1.5–1.7 символа на токен для китайского и 4–5 для английского. Автор советует не переобучать токенизатор без нужды: сменив словарь, вы теряете совместимость весов и данных. А сравнивать модели с разными токенизаторами предлагает не по perplexity, а по битам на байтбит на байт (BPB)EN bits per byte (BPB)Loss, пересчитанный на байт исходного текста; в отличие от perplexity, его можно сравнивать между моделями с разными токенизаторами..
Конвейер обучения
Заголовок раздела «Конвейер обучения»flowchart LR P["Pretrain<br/>pretrain_t2t"] --> S["Full SFT<br/>диалоги + tool calls"] S --> L["LoRA<br/>своя тема"] S --> D["DPO<br/>chosen / rejected"] S --> R["RLAIF<br/>PPO · GRPO · CISPO"] R --> A["Agentic RL<br/>многоходовые инструменты"] S -.-> K["Дистилляция<br/>CE + KL к учителю"]
| Этап | Скрипт | Данные | Чему учит | Урок курса |
|---|---|---|---|---|
| Pretrain | train_pretrain.py | pretrain_t2t_mini (1.2 ГБ) или pretrain_t2t (10 ГБ) | продолжать текст | 5.1, 5.6 |
| Full SFT | train_full_sft.py | sft_t2t_mini (1.6 ГБ) или sft_t2t (14 ГБ) | диалог, роли, вызов инструментов | 6.1 |
| LoRA | train_lora.py | ваши диалоги (например, медицина) | дешёвая адаптация под тему | 6.2 |
| DPO | train_dpo.py | dpo.jsonl (53 МБ), пары ответов | предпочтения | 6.3 |
| PPO, GRPO, CISPO | train_ppo.py, train_grpo.py | rlaif.jsonl (24 МБ) + reward-модель | RL на автоматической награде | 6.3, 6.4 |
| Agentic RL | train_agent.py | agent_rl.jsonl | многоходовое использование инструментов | 8.3 |
| Дистилляция | train_distillation.py | ответы и логиты учителя | подражать сильной модели | 6.4.4 |
Обязательны только первые два этапа: после них получается «Zero»-модель, которая уже поддерживает диалог.
Форматы данных простые. Претрейн — по документу на строку (в датасетах MiniMind тексты китайские, здесь — пример в переводе):
{"text": "Transformer моделирует связи в контексте через self-attention и служит основой современных больших языковых моделей."}SFT — многоходовые диалоги с ролями system, user, assistant и tool:
{"conversations": [ {"role": "user", "content": "Привет"}, {"role": "assistant", "content": "Привет!"}]}Несколько интересных решений автора:
- Tool calling входит в обычный SFT. Около 100 тыс. примеров вызова инструментов синтезированы моделью Qwen3-4B, поэтому отдельная стадия не нужна.
- Режим «думать или нет» задаётся шаблоном. Флаг
open_thinkingвставляет в шаблон чата<think>или пустой блок<think></think>. Отдельную «рассуждающую» модель больше не учат. - Награда в RL — непрерывная. Для модели в 0.1B бинарная награда «верно/неверно» почти всегда равна нулю: все ответы в группе неверны, преимущество (насколько награда ответа выше средней — модуль 6.3), градиента нет. Поэтому MiniMind берёт reward-модель InternLM2-1.8B-Reward, которая отличает «плохо» (−3.0) от «чуть лучше» (−2.8). Это та же проблема «вырожденных групп» GRPO, которую вы встретите в модуле 6.4.
Что внутри train_pretrain.py
Заголовок раздела «Что внутри train_pretrain.py»Цикл предобучения — около 160 строк. Главное:
- Оптимизатор AdamW, , обрезка градиента по норме 1.0, смешанная точность BF16 через
autocast. По умолчанию — 2 эпохи по данным (--epochs 2). - Батч: 32 последовательности × 8 шагов накопления градиентов ×
max_seq_len340 = 87 040 токенов на шаг оптимизатора. - Расписание — косинус без разогрева, от до :
def get_lr(current_step, total_steps, lr): return lr * (0.1 + 0.45 * (1 + math.cos(math.pi * current_step / total_steps)))- Loss = cross-entropy +
aux_loss(у плотной модели он нулевой). - Данные: каждый документ — это
[<|im_start|>] + токены + [<|im_end|>], дополненные паддингом доmax_seq_len. У паддинга метка −100, и в loss он не участвует:F.cross_entropyпо умолчанию пропускает позиции с такой меткой (параметрignore_index=-100).
Последний пункт — сознательный компромисс в пользу простоты. Короткие документы дополняются паддингом, и часть вычислений уходит впустую. В своём проекте мы сделаем иначе: склеим документы в единый поток (упаковка последовательностейупаковка последовательностейEN sequence packingДокументы склеивают в один поток токенов через разделитель и режут на окна фиксированной длины — без паддинга.), как в nanoGPT.
Что можно повторить у себя
Заголовок раздела «Что можно повторить у себя»Обучить полноценную minimind-3 на процессоре за вечер не получится, но многое доступно:
- Запуск готовой модели: скачайте
minimind-3с ModelScope или Hugging Face и запуститеpython eval_llm.py --load_from ./minimind-3. - LoRA: по словам автора,
train_lora.pyна CPU обычно проходит довольно быстро. Ему нужны готовые веса SFT в папкеout/(по умолчаниюfull_sft_768.pth) — свои или скачанные: автор выкладывает.pth-веса на ModelScope (gongjy/minimind-3-pytorch). - Крошечный претрейн для понимания кода: возьмите первые 20 тыс. строк данных и уменьшите модель (около 5M параметров):
head -n 20000 dataset/pretrain_t2t_mini.jsonl > dataset/pretrain_tiny.jsonlcd trainerpython train_pretrain.py --data_path ../dataset/pretrain_tiny.jsonl --epochs 1 \ --hidden_size 256 --num_hidden_layers 4 --max_seq_len 128 --batch_size 8Полный список флагов — в python train_pretrain.py --help.
Главное на CPU — убедиться, что loss стартует около и уверенно падает.
Это основной сценарий автора. Скачайте pretrain_t2t_mini.jsonl и sft_t2t_mini.jsonl в ./dataset, затем:
cd trainerpython train_pretrain.py --epochs 1 # ≈1.2 ч на RTX 3090 → out/pretrain_768.pthpython train_full_sft.py --epochs 1 # ≈1.1 ч → out/full_sft_768.pthcd .. && python eval_llm.py --weight full_sftОценки автора даны для одной эпохи; без --epochs 1 скрипты пройдут по данным дважды, и времени уйдёт вдвое больше.
Все скрипты умеют продолжать прерванное обучение с флагом --from_resume 1.
Карты T4 не поддерживают BF16, поэтому запускайте с --dtype float16: скрипт сам включит GradScaler
для масштабирования loss (урок 5.4.1). Обучение будет в несколько раз медленнее, чем на 3090, поэтому начните с
уменьшенной модели (--hidden_size 512) или части данных. Для нескольких карт — torchrun --nproc_per_node N train_pretrain.py.
Как читать код
Заголовок раздела «Как читать код»model/model_minimind.py(≈300 строк):MiniMindConfig→RMSNorm→precompute_freqs_cis(RoPE и YaRN) →Attention(GQA, QK-Norm,scaled_dot_product_attention) →FeedForward(SwiGLU) →MOEFeedForward→MiniMindBlock→MiniMindForCausalLM(сдвиг меток в loss,generateс top-k, top-p и штрафом за повторы).dataset/lm_dataset.py:PretrainDataset, а затемSFTDataset— посмотрите, как loss считается только по ответам ассистента.trainer/train_pretrain.pyиtrainer/trainer_utils.py: цикл, расписаниеget_lr, чекпоинты и продолжение обучения.- Дальше по конвейеру:
train_full_sft.py→train_lora.py→train_dpo.py→train_grpo.py. Каждый скрипт читайте вместе с соответствующим уроком курса.
Проверь себя
Заголовок раздела «Проверь себя»- MiniMind — полный учебный конвейер LLM на чистом PyTorch: от токенизатора до RL с инструментами; Apache-2.0.
- minimind-3 (64M): 8 слоёв × 768, GQA 8/4 с QK-Norm, SwiGLU, RoPE, RMSNorm, словарь 6400. MoE-версия: 4 эксперта, top-1, 198M всего и 64M активных.
- У маленьких моделей словарь и форма сети (глубина и ширина) критичны: эмбеддинги легко съедают бюджет параметров.
- Обязательные этапы — pretrain и SFT (около 2.3 часа на RTX 3090); LoRA, DPO, GRPO и Agentic RL добавляются по желанию.
- Токенизатор MiniMind не подходит для русского — для своего проекта нужен свой.
- MiniMind принимает внешние исправления: найденный в курсе баг можно превратить в свой первый pull request (урок 11.4.2), а учёт экспериментов вместо SwanLab или W&B — в уроке 11.1.3.
Дальше: практикум — планируем и обучаем свою мини-LLM на русских текстах.