Перейти к содержимому
Ступень 55.6 Проект — своя мини-LLM

MiniMind — LLM за 2 часа

Разбираем китайский учебный проект MiniMind — архитектуру Dense и MoE, токенизатор и весь конвейер pretrain → SFT → LoRA → DPO/GRPO — и решаем, что из этого повторить на CPU и на одной GPU.

УРОКstage-5/5-6-minimind/01-minimind-overview.mdx1 ч 30 минУрок 1 из 2
После урока вы сможете
  • Описать архитектуру minimind-3 и пересчитать её 64M параметров
  • Объяснить, что делает каждый этап конвейера MiniMind и на каких данных он учится
  • Выбрать посильный вариант для своего железа — CPU, одна GPU или облако
  • Читать код MiniMind и сопоставлять его с уроками курса

В модуле 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-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-v108–09.202426M, 108M, MoE 4 × 26M
minimind204.202526M (small), 104M, 145M (MoE)
minimind-301.04.202664M (Dense), 198M-A64M (MoE)

У проекта есть «родственники»: MiniMind-V (зрение), MiniMind-O (омни-модальность), экспериментальные диффузионная языковая модель и модель с линейным вниманием. Дальше говорим о текущей основной ветке minimind-3.

Автор прямо пишет, что структура выровнена под экосистему Qwen3: так проще конвертировать модель в форматы transformers, llama.cpp и vLLM. Получается тот же «современный блок», что вы разбирали в уроке 4.5.3:

ПараметрЗначениеГде в курсе
Типdecoder-only, Pre-Norm4.5.1
НормализацияRMSNorm, eps = 1e-64.5.3
Позицииротационные позиционные эмбеддингиротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM., θ=106\theta = 10^6, до 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-Norm4.5.3, 5.2.1
FFNSwiGLU, скрытый размер ⌈768π/64⌉⋅64=2432\lceil 768\pi / 64 \rceil \cdot 64 = 24324.5.3
Словарь6400 токенов, эмбеддинги общие с LM-головой4.2, 4.6.2
Слоёв × ширина8 × 768—
Bias в линейных слояхнет4.5.3

Две детали, которых не было в нашей GPT из 4.6. QK-Norm (вы видели его в уроке 4.5.3) — RMSNorm, применённый к qq и kk каждой головы перед RoPE: он не даёт attention-логитам разрастаться и стабилизирует обучение (так делает Qwen3). И необычный размер FFN: вместо привычных 83d\frac{8}{3} d автор берёт π⋅d\pi \cdot d, округлённое вверх до кратного 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["Логиты"]
Архитектура minimind-3 (Dense); пунктир — остаточные связи

Хороший способ проверить, что вы поняли архитектуру, — посчитать параметры руками. Этот код запускается на чистом 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.9M
minimind-3-moe всего 198.4M, активных 63.9M
CPU-версия всего 5.0M, активных 5.0M
d=512, 8 слоёв всего 30.0M, активных 30.0M

Из 63.9M параметров на SwiGLU приходится 44.8M (70%), на внимание — 14.2M (22%), на эмбеддинги — 4.9M (7.7%).

Почему словарь такой маленький. У Qwen2 словарь 151 643 токена. Будь он у minimind-3, одна матрица эмбеддингов заняла бы 151 643⋅768≈116151\,643 \cdot 768 \approx 116M параметров — почти вдвое больше всей модели. Маленькой модели выгоднее потратить параметры на слои, а за маленький словарь заплатить более длинными последовательностями.

Почему 8 слоёв по 768, а не 16 по 512. В README автор ссылается на MobileLLM (Meta, 2024): при фиксированном числе параметров маленьким моделям «глубокая и узкая» форма обычно выгоднее «широкой и мелкой». Но у узости есть предел: по опыту MiniMind при dmodeld_{model} меньше 512 качество заметно страдает. Восемь слоёв по 768 — компромисс между скоростью (мелкая сеть быстрее), стабильностью и качеством.

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. с коэффициентом 5⋅10−45 \cdot 10^{-4}. Интересный приём для 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 к учителю"]
Этапы обучения MiniMind (основные — pretrain и SFT, остальное по желанию)
ЭтапСкриптДанныеЧему учитУрок курса
Pretraintrain_pretrain.pypretrain_t2t_mini (1.2 ГБ) или pretrain_t2t (10 ГБ)продолжать текст5.1, 5.6
Full SFTtrain_full_sft.pysft_t2t_mini (1.6 ГБ) или sft_t2t (14 ГБ)диалог, роли, вызов инструментов6.1
LoRAtrain_lora.pyваши диалоги (например, медицина)дешёвая адаптация под тему6.2
DPOtrain_dpo.pydpo.jsonl (53 МБ), пары ответовпредпочтения6.3
PPO, GRPO, CISPOtrain_ppo.py, train_grpo.pyrlaif.jsonl (24 МБ) + reward-модельRL на автоматической награде6.3, 6.4
Agentic RLtrain_agent.pyagent_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 бинарная награда «верно/неверно» почти всегда равна нулю: все ответы в группе неверны, преимущество A=r−b≈0A = r - b \approx 0 (насколько награда rr ответа выше средней bb — модуль 6.3), градиента нет. Поэтому MiniMind берёт reward-модель InternLM2-1.8B-Reward, которая отличает «плохо» (−3.0) от «чуть лучше» (−2.8). Это та же проблема «вырожденных групп» GRPO, которую вы встретите в модуле 6.4.

Цикл предобучения — около 160 строк. Главное:

  • Оптимизатор AdamW, η0=5⋅10−4\eta_0 = 5 \cdot 10^{-4}, обрезка градиента по норме 1.0, смешанная точность BF16 через autocast. По умолчанию — 2 эпохи по данным (--epochs 2).
  • Батч: 32 последовательности × 8 шагов накопления градиентов × max_seq_len 340 = 87 040 токенов на шаг оптимизатора.
  • Расписание — косинус без разогрева, от η0\eta_0 до 0.1 η00.1\,\eta_0:
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.jsonl
cd trainer
python 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 стартует около ln⁡6400≈8.8\ln 6400 \approx 8.8 и уверенно падает.

  1. 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 и штрафом за повторы).
  2. dataset/lm_dataset.py: PretrainDataset, а затем SFTDataset — посмотрите, как loss считается только по ответам ассистента.
  3. trainer/train_pretrain.py и trainer/trainer_utils.py: цикл, расписание get_lr, чекпоинты и продолжение обучения.
  4. Дальше по конвейеру: train_full_sft.py → train_lora.py → train_dpo.py → train_grpo.py. Каждый скрипт читайте вместе с соответствующим уроком курса.
Проверь себя5 вопросов
1 Сколько миллионов параметров заняли бы эмбеддинги minimind-3 (d=768d = 768), если взять словарь Qwen2 на 151 643 токена? Округлите до целого.
2 Зачем minimind-3 использует 4 KV-головы при 8 головах запросов?
3 Какие утверждения о конвейере MiniMind верны?Можно выбрать несколько ответов
4 Почему MiniMind в RL использует reward-модель с непрерывной оценкой, а не правило «ответ верен — 1, иначе 0»?
5 Сколько токенов обрабатывает train_pretrain.py за один шаг оптимизатора с настройками по умолчанию (batch 32, накопление 8, max_seq_len 340)?
  • 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 на русских текстах.