Перейти к содержимому
Ступень 1010.1 Своя LLM от начала до конца

План выпускного проекта

Техзадание выпускного проекта — от своего токенизатора и претрейна через SFT и GRPO до квантования, сервера и агента с инструментом; варианты для CPU, одной GPU, облака и 4×GPU, бюджет по этапам и проверяемые критерии Simple/Medium/Strong/Boss.

УРОКstage-10/10-1-capstone/01-capstone-plan.mdx2 чУрок 1 из 2
После урока вы сможете
  • Разложить проект «своя LLM от начала до конца» на 7 этапов с артефактами, метриками и вехами
  • Выбрать масштаб под своё железо и посчитать бюджет вычислений и времени по этапам
  • Сформулировать проверяемую задачу для GRPO и агента так, чтобы она была по силам маленькой модели
  • Настроить репозиторий, журнал экспериментов и metrics.json так, чтобы по ним потом написать отчёт

За девять ступеней вы по отдельности собрали все детали современной LLM: токенизатор (4.2), трансформер (4.6), претрейн (5.6), SFT (6.1), GRPO (6.4), квантование и локальный запуск (7.2), оценку (7.3), агента с инструментами (8.3), а перед релизом — проверку безопасности и чек-лист ответственного релиза (9.3.4–9.3.5). Но инженера отличает не знание деталей, а умение собрать их в работающую систему и честно сказать, что получилось.

выпускной проектвыпускной проектEN capstone projectИтоговый проект, который собирает навыки всего курса в одну работающую систему. В Китае сокращённо «бишэ» (выпускной проект). — это ровно такая сборка. Вы пройдёте полный цикл, который проходят DeepSeek и Qwen, только в масштабе, доступном одному человеку: своя модель на десятки миллионов параметров вместо сотен миллиардов. Принципы те же. Китайский учебный проект MiniMind показывает, что это реально: модель на 64M параметров «за 2 часа и 3 юаня» аренды одной видеокарты (урок 5.6).

Результат — проект для портфолио: репозиторий, который запускается одной командой, модель, которой можно задать вопрос через API, и технический отчёт (следующий урок). Этот урок — техзадание: что строим, в каком порядке, как проверяем и сколько это стоит.

Маленький русскоязычный ассистент-решатель. Модель отвечает на простые вопросы и решает текстовые задачи на арифметику. Когда считать в уме трудно, она вызывает калькулятор. Задача выбрана не случайно: у неё есть проверяемый ответ (число), а значит, есть награда для GRPO и честная автоматическая оценка.

flowchart LR
D["Корпус: русский текст + немного китайского"] --> T["1. Токенизатор BPE"]
T --> P["2. Претрейн"]
P --> S["3. SFT: диалоги, решения, вызовы инструмента"]
S --> G["4. GRPO: награда за верный ответ"]
G --> Q["5. Квантование INT8 / INT4"]
Q --> V["6. Сервер /v1/chat/completions"]
V --> A["7. Агент + калькулятор"]
E["Eval-набор: заморожен с первого дня"] -.-> P
E -.-> S
E -.-> G
E -.-> Q
E -.-> A
Семь этапов проекта и eval-набор, через который проходит каждый этап

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

ЭтапЧто сделатьАртефактКак проверитьУроки и ДЗ
1. токенизатортокенизаторEN tokenizerbyte-level BPE на своём корпусе, словарь 4096–32 000tokenizer.jsonсжатие (символов на токен) на отложенном русском тексте; сравнение с токенизатором Qwen4.2
2. Претрейнпредобучение, претрейнEN pretrainingтрансформер с RMSNorm, RoPE, SwiGLU, GQA (как в 5.6)чекпоинт, loss.pngначальный loss ≈ ln⁡V\ln V; val loss ниже биграммной линии; осмысленная генерация5.6, s5-hw02-tiny-llm-pretrain
3. обучение с учителем на инструкцияхобучение с учителем на инструкциях (SFT)EN supervised fine-tuning (SFT)chat-шаблон, loss только на ответах; данные: диалоги, решения задач с рассуждением, примеры вызова калькуляторачекпоинтдоля ответов в нужном формате (Ответ: N, корректный <tool_call>)6.1
4. GRPOгруппа из GG ответов, награда = верный ответ + формат, KL к SFT-моделичекпоинт, графики награды и длины ответаточность на отложенных задачах до/после; pass@1 по 4–8 сэмплам6.4, s6-hw02-grpo
5. квантованиеквантованиеEN quantizationINT8 и INT4 по группам своим кодом или GGUF через llama.cppфайлы моделейразмер, скорость (токенов/с), потеря точности на eval в п.п.7.2, s7-hw01-quantization
6. СерверOpenAI-совместимый /v1/chat/completions (свой на Python или llama-server / vLLM)serve.py или команда запусказапрос через curl и клиент openai; токенов/с7.1, 7.2, 8.1
7. агентагентEN agentВ китайском встречается и «дайли» («посредник»), но в AI-контексте стандарт — «чжинэнти» (буквально «интеллектуальное тело»).цикл «думай → действуй → наблюдай», инструмент calculator через вызов инструментоввызов инструментовEN tool calling / function callingagent.pyдоля решённых задач с инструментом и без; pass^k на 3 прогонах8.3, s8-hw02-agent

Четыре оговорки, которые сэкономят вам недели:

  • Задача для GRPO должна быть по силам модели после SFT. В GRPO преимущество ответа — его награда минус средняя награда группы. Если модель не решает задачу ни разу из 8 попыток, все награды нули, преимущества нули, и учиться не на чем. Начните с того, где pass@8 после SFT хотя бы 20–30%: сложение и вычитание двузначных чисел, задачи в одно-два действия. Сложность поднимайте постепенно — от простого к сложному, как в школьной программе (это называют curriculum learning).
  • Маленькая модель плохо считает «в уме» — и это хорошо. Умножение четырёхзначных чисел модель на 25M параметров почти никогда не сделает сама, а с калькулятором — легко. Разница «с инструментом / без» будет вашим самым наглядным результатом.
  • Свой формат вызова инструмента. Используйте те же теги, что Qwen и MiniMind: <tool_call>{"name": "calculator", "arguments": {"expr": "37*48"}}</tool_call>, ответ инструмента — в <tool_response>. Добавьте эти теги как спецтокеныспециальный токен (спецтокен)EN special tokenУправляющий токен, а не кусок текста — начало/конец текста (BOS/EOS), заполнитель для батча (PAD), разметка ролей в чате (например, <|im_start|> у Qwen). на этапе 1, иначе токенизатор разобьёт их на куски.
  • Квантование своей архитектуры. Конвертер llama.cpp понимает только поддерживаемые архитектуры. Если ваша модель совпадает с Llama/Qwen по устройству и именам весов, GGUF сработает; если нет — квантуйте своим кодом из ДЗ s7-hw01. Для первого прохода проще второй путь: он не требует перекладывать веса в формат Hugging Face.

Чтобы не начинать с пустой папки, в курсе есть стартер — домашняя работа s10-capstone в папке homework/s10-capstone/. В нём уже готовы конфиги трёх масштабов, скрипты всех этапов, связь между ними, сервер на стандартной библиотеке и автотесты. Вам остаётся заполнить функции и методы с raise NotImplementedError. Их около 70, но почти все вы уже писали в домашних работах s4-hw01, s5-hw02, s6-hw02, s7-hw01 и s8-hw02. В докстринге каждой функции написано, откуда её взять, какая там формула и какой ожидается пример.

Как этапы плана привязаны к файлам стартера:

Этап планаФайл в code/Что заполнитьПроверка тестами
Данные и eval-наборdata.pyчтение корпуса, разбиение по документам, упаковка, get_batch, ChatML, маска SFTtest_units.py, test_pretrain.py
1. Токенизаторtokenizer.pyBPE из s4-hw01 на списке документов, спецтокены, сжатиеtest_units.py -k tokenizer
2. Претрейнmodel.py, pretrain.pyмодель из s5-hw02 + KV-кэш и generate; цикл обученияtest_pretrain.py
3. SFTsft.pyпримеры из диалогов, цикл SFTtest_posttrain.py -k sft
4. GRPOgrpo.pyкирпичики из s6-hw02, log π ответа, rollout, шаг обновленияtest_posttrain.py -k grpo
5. Квантованиеexport.pyINT8 и INT4 по группам из s7-hw01test_units.py -k quant
6. Серверserve.pyгенерация ответа и обработчик /v1/chat/completionstest_pipeline.py -k server
7. Агентagent.pyкалькулятор, разбор вызовов, цикл ReAct из s8-hw02test_pipeline.py -k agent
Оценкаeval.pyформат ответа, интервалы, pass@k, pass^k, контаминацияtest_units.py -k metrics

Конфиги лежат в configs/: xs.yaml (CPU, минуты), m.yaml (одна видеокарта, часы), xl.yaml (4 карты, ночь) и tiny.yaml для автотестов. Весь конвейер запускается одной командой из корня репозитория:

Окно терминала
bash homework/s10-capstone/run_all.sh configs/xs.yaml # этапы по очереди, сервер — фоновым процессом
python homework/s10-capstone/code/pipeline.py --config homework/s10-capstone/configs/xs.yaml # то же одним процессом (Windows)

Каждый этап пишет homework/s10-capstone/artifacts/<запуск>/<этап>/metrics.json, а последний собирает их в общий metrics.json с ключами из раздела «Критерии» ниже.

Уровни здесь двух видов, и их важно не путать:

  • Уровни автогрейдера проверяют код. python homework/check.py s10-capstone гоняет весь конвейер на крошечной конфигурации tiny на CPU за пару минут. Simple — интерфейсы и маленькие юниты, Medium — токенизатор и мини-претрейн (loss падает), Strong — SFT и шаг GRPO, Boss — весь конвейер до сервера и агента. Качество модели тесты не оценивают.
  • Уровни проекта проверяют результат: числа вашего прогона против порогов из раздела «Критерии». Их печатает python homework/s10-capstone/code/eval.py --levels homework/s10-capstone/artifacts/xs/metrics.json.

Пройденный Boss автогрейдера значит, что конвейер собран правильно. Уровень проекта зависит от масштаба: на XS он честно «не пройден», Simple и Medium достижимы на M.

С нуля писать почти ничего не нужно: каждый этап вы уже делали в уроке или домашней работе. Выпускной проект — это сборка готовых кусков в один конвейер. Переносите свои решения (или эталоны из solutions/) в заготовки стартера homework/s10-capstone/code/ — или в папку capstone/src/, если собираете репозиторий сами:

ЭтапЧто взять за основуЧто дописать
Данные для претрейнаурок 5.6.2, «Шаг 1»: FineWeb-2 rus_Cyrl через datasets, очистка из урока 5.1.2, формат corpus.jsonlничего — тот же корпус
1. Токенизаторурок 5.6.2, «Шаг 2» (byte-level BPE на tokenizers) или ваш homework/s4-hw01-bpe-tokenizer/code/bpe.pyспецтокены <tool_call>, </tool_call>, <tool_response>, </tool_response> и токены chat-шаблона
2. Претрейнmodel.py и train.py из урока 5.6.2 (или ДЗ s5-hw02-tiny-llm-pretrain: model.py, train.py, data.py)только конфиг; для 4 карт — DDP-правки из того же урока
3. SFTмаска loss «только ответы ассистента» из урока 6.1.2 (браузерное задание 6-1-sft-loss-mask) и тот же цикл обучениясборку диалогов из задач генератора ниже
4. GRPOgroup_advantages, kl_k3, grpo_loss из ДЗ s6-hw02-grpo/code/grpo.py; цикл — урок 6.4.5; генерация с KV-кэшем — урок 4.6.4функцию награды reward из генератора ниже
5. Квантованиеquantize_model и QuantLinear из ДЗ s7-hw01-quantization/code/qlinear.pyзамер размера, скорости и точности на eval_v1
6. Серверmock_server.py из практики урока 8.1.1 — готовый OpenAI-совместимый сервер на стандартной библиотекезаменить строку с фейковым ответом на генерацию вашей модели
7. АгентAgent, ToolRegistry, calculator, parse_tool_calls из ДЗ s8-hw02-agent (понимает теги <tool_call> как у Qwen)подключить к своему серверу из этапа 6
Оценкаexact match и pass@k (урок 7.3.2, задание 7-3-pass-at-k), интервалы (урок 7.3.1), n-граммная проверка контаминации (урок 7.3.1)run_eval.py, который пишет числа в metrics.json

Задачи для eval, SFT и GRPO нигде скачивать не нужно: их генерирует скрипт с фиксированным seed. Он сразу исключает eval-вопросы из обучающих и даёт функцию награды:

# data/make_tasks.py — арифметические задачи для eval, SFT и GRPO + функция награды
import json
import random
import re
NAMES = [("Аня", "а"), ("Борис", ""), ("Вера", "а"), ("Глеб", "")] # имя и окончание глагола
ITEMS = [("тетради", "тетрадей"), ("ручки", "ручек"), ("яблока", "яблок")] # «2–4 …» и «5–9 …»
def make_task(rng, kind):
"""Одна задача: вопрос, числовой ответ, тип и эталонное решение для SFT."""
if kind == "one_step": # одно действие, числа до 100
a, b, op = rng.randint(10, 99), rng.randint(10, 99), rng.choice("+-")
ans = a + b if op == "+" else a - b
q, sol = f"Сколько будет {a} {op} {b}?", f"{a} {op} {b} = {ans}."
elif kind == "word": # текстовая задача в два действия
(name, end), (few, many) = rng.choice(NAMES), rng.choice(ITEMS)
n, price = rng.randint(2, 9), rng.randint(5, 99)
item = few if n < 5 else many # 3 яблока, но 7 яблок
ans = 1000 - n * price
q = f"{name} купил{end} {n} {item} по {price} руб. и заплатил{end} 1000 руб. Сколько сдачи?"
sol = f"{n} · {price} = {n * price}; 1000 − {n * price} = {ans}."
else: # большие числа — нужен калькулятор
a, b = rng.randint(1000, 9999), rng.randint(100, 999)
ans = a * b
q = f"Сколько будет {a} * {b}?"
sol = '<tool_call>{"name": "calculator", "arguments": {"expr": "%d*%d"}}</tool_call>' % (a, b)
return {"q": q, "answer": ans, "type": kind, "solution": sol}
def make_set(seed, n, exclude=frozenset()):
"""n уникальных задач с фиксированным seed; вопросы из exclude (eval) пропускаем."""
rng, kinds = random.Random(seed), ["one_step", "word", "big"]
out, seen = [], set(exclude)
while len(out) < n:
t = make_task(rng, kinds[len(out) % 3]) # типы по кругу — набор сбалансирован
if t["q"] not in seen: # ни дублей, ни пересечения с eval
seen.add(t["q"])
out.append(t)
return out
def reward(text, answer):
"""Награда для GRPO и проверка в eval: 1.0, если последнее «Ответ: N» совпало с правильным числом."""
found = re.findall(r"Ответ:\s*(-?\d+)", text) # все числа после «Ответ:»
return 1.0 if found and int(found[-1]) == answer else 0.0
if __name__ == "__main__":
eval_set = make_set(seed=2026, n=300) # замораживаем: seed больше не меняем
eval_q = frozenset(t["q"] for t in eval_set)
train_set = make_set(seed=1, n=20_000, exclude=eval_q)
for path, rows in [("eval_v1.jsonl", eval_set), ("train_tasks.jsonl", train_set)]:
with open(path, "w", encoding="utf-8") as f:
f.writelines(json.dumps(t, ensure_ascii=False) + "\n" for t in rows)
print("eval:", len(eval_set), "| train:", len(train_set),
"| пересечений:", len(eval_q & {t["q"] for t in train_set}))
for t in eval_set[:3]:
print(f'{t["type"]:8} | {t["q"]} → {t["answer"]}')
print(reward("1000 − 440 = 560. Ответ: 560", 560), reward("Ответ: 561", 560), reward("не знаю", 560))
eval: 300 | train: 20000 | пересечений: 0
one_step | Сколько будет 25 + 50? → 75
word | Борис купил 8 яблок по 78 руб. и заплатил 1000 руб. Сколько сдачи? → 376
big | Сколько будет 9975 * 962? → 9595950
1.0 0.0 0.0

Как из задачи получается SFT-пример: вопрос — реплика пользователя, поле solution плюс строка Ответ: N — реплика ассистента. Для задач типа big ассистент сначала просит калькулятор, ваш код подставляет ответ инструмента, и только потом модель пишет ответ. В разметке ChatML (урок 6.1.2) один такой диалог выглядит так:

<|im_start|>user
Сколько будет 9975 * 962?<|im_end|>
<|im_start|>assistant
<tool_call>{"name": "calculator", "arguments": {"expr": "9975*962"}}</tool_call><|im_end|>
<|im_start|>tool
<tool_response>9595950</tool_response><|im_end|>
<|im_start|>assistant
Ответ: 9595950<|im_end|>

Loss считается только на репликах assistant — строку <tool_response> пишет среда, а не модель (урок 8.3.4). Формат роли tool выбираете вы; главное — один и тот же в SFT, GRPO и агенте. Для GRPO берите из train_tasks.jsonl только вопросы и ответы: решение модель должна найти сама, а reward проверит последнее Ответ: N.

CPU (ноутбук)Одна GPU 8–24 ГБОблакоВаша машина, 4×GPU
МодельS из урока 5.6: 11.9MM из урока 5.6: 25.8M (или своя на 64M, как minimind-3)как «одна GPU»100–250M, плотная или MoE
Токенов претрейнадесятки миллионов0.5–1.5 млрд0.5–1.5 млрднесколько миллиардов
GRPOгруппа 4, сотни шагов, короткие ответыгруппа 8, 500+ шаговкак «одна GPU»генерация на 1 карте (через vLLM, если архитектура совместима с Llama/Qwen), обучение на 3
Время на этапы 2–4 с отладкой и повторными запускамивечера недели1–2 дня1–2 дня аренды1–2 ночи
Уровень, на который реально выйтиSimple, MediumMedium, StrongMedium, StrongStrong, Boss

Облако из России: Yandex DataSphere (есть пробный грант), Kaggle (около 30 GPU-часов в неделю, нужна верификация телефона), Colab. Лимиты и условия меняются — проверяйте перед стартом. «3 юаня» в MiniMind — это аренда одной RTX 3090 примерно на 2.3 часа претрейна и SFT, по 1.3 юаня в час (урок 5.6).

Вычислительная стоимость считается по формуле из урока 5.1: обучение на DD токенах стоит C≈6NDC \approx 6ND FLOPs. Генерация одного токена — примерно 2N2N FLOPs (только прямой проход). Посчитаем все этапы для конфигурации M на одной GPU:

def stage_flops(n_params, pretrain_tokens, sft_tokens, grpo_steps, prompts_per_step, group, gen_len):
"""FLOPs по этапам выпускного проекта (оценка «по порядку величины»)."""
pre = 6 * n_params * pretrain_tokens # прямой + обратный проход: 6·N·D
sft = 6 * n_params * sft_tokens
rollout_tokens = grpo_steps * prompts_per_step * group * gen_len
grpo = (2 + 2 + 6) * n_params * rollout_tokens # генерация + референс-модель + обновление
return {"pretrain": pre, "SFT": sft, "GRPO": grpo}, rollout_tokens
# Конфигурация M из урока 5.6 (25.8M параметров), одна GPU
flops, rollout = stage_flops(25.8e6, pretrain_tokens=0.5e9, sft_tokens=20e6,
grpo_steps=500, prompts_per_step=32, group=8, gen_len=64)
total = sum(flops.values())
for name, f in flops.items():
print(f"{name:8} {f:8.1e} FLOPs {100 * f / total:5.1f}%")
print(f"сгенерировать для GRPO: {rollout / 1e6:.1f}M токенов")
# Время: FLOPs делим на реальную скорость; генерацию — на токены в секунду
tflops_eff = 20e12 # реальная скорость обучения на вашей GPU (замерьте, урок 5.6)
gen_tok_s = 3000 # скорость генерации батчем (замерьте, урок 7.1)
train_h = (flops["pretrain"] + flops["SFT"]) / tflops_eff / 3600
gen_h = rollout / gen_tok_s / 3600
print(f"pretrain + SFT ≈ {train_h:.1f} ч, генерация для GRPO ≈ {gen_h:.1f} ч")
pretrain 7.7e+16 FLOPs 93.7%
SFT 3.1e+15 FLOPs 3.7%
GRPO 2.1e+15 FLOPs 2.6%
сгенерировать для GRPO: 8.2M токенов
pretrain + SFT ≈ 1.1 ч, генерация для GRPO ≈ 0.8 ч

Два вывода. Первый: почти все FLOPs уходят на претрейн, как и у больших моделей — у DeepSeek-V3 пост-трейнинг занял 5 тыс. GPU-часов из 2.788 млн. Второй: GRPO по FLOPs копеечный, но по времени сравним с претрейном. Генерация идёт последовательно, по одному токену за шаг, и на каждом шаге видеокарта почти простаивает: у больших моделей она ждёт чтения весов из памяти (урок 7.1), у крошечных — накладных расходов на запуск каждого шага. Поэтому в больших RL-системах генерацию отдают быстрому движку (vLLM, SGLang), а у вас на 4 картах — отдельной карте.

Числа tflops_eff и gen_tok_s — условные. На первой неделе замерьте свои и пересчитайте план.

Модуль рассчитан примерно на 30 часов. Разбейте их на вехивеха (milestone)EN milestoneПромежуточная цель с проверяемым результатом — артефактом или числом. Проект без вех трудно довести до конца. — промежуточные цели с проверяемым результатом. Для каждой заранее запишите определение готовности (definition of done): какой артефакт появится и какое число он должен показать.

НеделяВехаГотово, когда…
1Скелет и данныерепозиторий, корпус, eval-набор v1 заморожен; весь конвейер XS проходит от начала до конца
2Токенизатор и претрейнval loss ниже биграммной линии, график loss без всплесков, генерация из русских слов
3SFT и скрипт оценки (harness)доля ответов в нужном формате ≥ 80%; run_eval.py считает точность и печатает интервалы
4GRPOточность на отложенных задачах выросла; графики награды и средней длины ответа
5Квантование, сервер, агентINT4-модель отвечает через curl; агент с калькулятором решает задачи, недоступные модели без него
6Отчёт и защитаотчёт по шаблону урока 10.1.2, 10-минутный доклад с живой демонстрацией
  1. Заведите журнал экспериментов (журнал экспериментовжурнал экспериментовEN experiment logЗапись каждого запуска — дата, коммит, конфиг, гипотеза, результат, вывод. Из него потом пишется отчёт.): дата, коммит, конфиг, гипотеза, результат, вывод. Одна строка на запуск. Через месяц вы не вспомните, почему у запуска run-17 был lr=6e-4, а отчёт пишется именно из этого журнала.

  2. Фиксируйте случайность. torch.manual_seed, seed для данных, версия библиотек в requirements.txt (воспроизводимостьвоспроизводимостьEN reproducibilityВозможность получить тот же результат, повторно запустив тот же код на тех же данных.). Результат, который нельзя повторить, нельзя и защитить.

  3. Каждый этап пишет metrics.json. Это контракт между обучением, оценкой и отчётом: графики и таблицы отчёта строятся из этих файлов, а не переписываются руками.

Как те же правила устроены в команде, где прогонов сотни, — папка на каждый прогон с конфигом и git-хэшем, трекеры, лабораторный журнал и сравнение по нескольким seed — в уроке 11.1.3.

Структура репозитория:

capstone/
├── configs/ # xs.yaml, m.yaml — все гиперпараметры, никаких «магических чисел» в коде
├── data/ # prepare.py; сами данные — в .gitignore
├── eval/
│ ├── eval_v1.jsonl # заморожен в первую неделю
│ └── run_eval.py # точность, exact match, pass@k, интервалы (урок 7.3)
├── src/ # tokenizer.py, model.py, train_pretrain.py, train_sft.py,
│ # train_grpo.py, quantize.py, serve.py, agent.py
├── runs/<этап>/ # metrics.json, loss.png, конфиг запуска
├── report/ # отчёт и рисунки (урок 10.1.2)
├── LOG.md # журнал экспериментов
└── Makefile # make all: от данных до агента одной командой

Makefile — файл для утилиты make, которая запускает команды этапов по порядку. Если make вам незнаком, начните с обычного bash-скрипта — он делает то же самое (имена файлов — ваши из src/):

#!/usr/bin/env bash
# run_all.sh — весь конвейер одной командой: bash run_all.sh configs/xs.yaml
set -e # остановиться на первой же ошибке, а не идти дальше
CFG=$1 # путь к конфигу — первый аргумент команды
python data/make_tasks.py # задачи для eval, SFT и GRPO
python src/tokenizer.py --config $CFG
python src/train_pretrain.py --config $CFG
python src/train_sft.py --config $CFG
python src/train_grpo.py --config $CFG
python src/quantize.py --config $CFG
python eval/run_eval.py --config $CFG # пишет runs/*/metrics.json

Аргумент --config читают модулем argparse (урок 0.2.4), а YAML-файл — библиотекой pyyaml (pip install pyyaml): yaml.safe_load(open(path)) вернёт обычный словарь. В уроке 5.6.2 гиперпараметры записаны прямо в начале train.py — перенесите их в YAML, чтобы XS, M и XL отличались только файлом конфига.

Уровни устроены лестницей, как в домашних заданиях курса и у Ли Хунъи: следующий засчитывается, только если пройден предыдущий. Все критерии — проверяемые числа, а не «модель стала лучше».

УровеньЧто нужноПороги
SimpleВсе 7 этапов работают одной командой, хотя бы на крошечной моделиSFT: ≥ 80% ответов в формате; GRPO: +10 п.п. точности на отложенных задачах; квантование: потеря ≤ 3 п.п.; агент решает ≥ 50% задач
MediumНастоящий масштаб для вашего железапретрейн ≥ 0.5 млрд токенов; GRPO: +15 п.п.; агент с калькулятором лучше агента без него на ≥ 20 п.п.; eval-набор ≥ 200 задач
StrongИсследование, а не только сборка≥ 2 абляцииабляционный эксперимент, абляцияEN ablation studyУбрать или заменить один компонент и сравнить с полной моделью, чтобы измерить вклад этого компонента. (например, награда за формат: есть/нет; группа 4 или 8); 95% интервалы у всех ключевых чисел; проверка контаминации train/eval; отчёт ≥ 6 страниц
BossМасштаб и надёжностьобучение на ≥ 2 GPU (DDP/FSDP); модель ≥ 100M параметров (или MoE); агент: pass^3 ≥ 0.5; публичная защита с живой демонстрацией

Пороги проверяются автоматически. Пример самопроверки по metrics.json:

import json
# Пороги уровней: (метрика, условие, значение). Всё, что ниже, — проверяемые числа из metrics.json.
LEVELS = {
"Simple": [("pipeline_ok", "==", True), # все 7 этапов запускаются одной командой
("sft_format_rate", ">=", 0.80), # доля ответов в нужном формате
("grpo_gain_pp", ">=", 10), # прирост точности после GRPO, п.п.
("quant_drop_pp", "<=", 3), # потеря точности после квантования, п.п.
("agent_success", ">=", 0.50)], # доля задач, решённых агентом с инструментом
"Medium": [("pretrain_tokens_b", ">=", 0.5),
("grpo_gain_pp", ">=", 15),
("tool_gain_pp", ">=", 20), # агент с калькулятором vs без него
("eval_size", ">=", 200)],
"Strong": [("n_ablations", ">=", 2),
("has_confidence_intervals", "==", True),
("contamination_checked", "==", True),
("report_pages", ">=", 6)],
"Boss": [("n_gpus", ">=", 2),
("params_m", ">=", 100),
("agent_pass_hat_3", ">=", 0.5), # pass^3: агент надёжен, а не «иногда»
("defense_done", "==", True)],
}
OPS = {"==": lambda a, b: a == b, ">=": lambda a, b: a >= b, "<=": lambda a, b: a <= b}
def level(metrics):
"""Уровни идут лестницей: Medium засчитывается, только если пройден Simple, и т. д."""
achieved = "не пройден"
for name, rules in LEVELS.items():
failed = [f"{k} {op} {v} (у вас {metrics.get(k)})" for k, op, v in rules
if k not in metrics or not OPS[op](metrics[k], v)]
if failed:
print(f"{name}: не хватает → " + "; ".join(failed))
break
achieved = name
return achieved
metrics = json.loads("""{
"pipeline_ok": true, "sft_format_rate": 0.91, "grpo_gain_pp": 17.5, "quant_drop_pp": 1.2,
"agent_success": 0.64, "pretrain_tokens_b": 1.3, "tool_gain_pp": 38.0, "eval_size": 300,
"n_ablations": 1, "has_confidence_intervals": true, "contamination_checked": true, "report_pages": 7
}""")
print("Ваш уровень:", level(metrics))
Strong: не хватает → n_ablations >= 2 (у вас 1)
Ваш уровень: Medium
  1. Возьмите стартер homework/s10-capstone/ (или создайте репозиторий по структуре выше) и заведите LOG.md с первой записью: «цель проекта, железо, выбранный уровень».
  2. Соберите eval_v1.jsonl: 200–300 задач трёх типов — арифметика в одно действие, текстовые задачи в два-три действия, задачи с большими числами (для калькулятора). Ответы — числа. Сгенерируйте их скриптом make_tasks.py из раздела «Откуда взять код и данные» (seed фиксирован) и сразу проверьте пересечение с обучающими данными 8-граммами (код из урока 7.3.1). Осторожно с шаблонами: если обучающие задачи для SFT и GRPO делает тот же генератор, длинные n-граммы шаблона совпадут у всех задач. Поэтому сохраните множество eval-задач (шаблон + числа) и исключайте их при генерации обучающих данных.
  3. Пересчитайте бюджет stage_flops для своей конфигурации и железа, запишите в LOG.md.
  4. Прогоните конвейер на конфигурации XS: 200 шагов претрейна, 100 шагов SFT, 20 шагов GRPO, INT8, сервер, один вызов агента. Качество не важно — важно, что make all доходит до конца. В стартере это bash homework/s10-capstone/run_all.sh configs/xs.yaml, а перед этим — python homework/check.py s10-capstone хотя бы до уровня Boss.
  5. Запишите в metrics.json первые числа и прогоните самопроверку уровней (в стартере — python homework/s10-capstone/code/eval.py --levels homework/s10-capstone/artifacts/xs/metrics.json).
Проверь себя5 вопросов
1 После SFT модель решает 0 задач из 8 попыток на каждой задаче обучающего набора GRPO. Что произойдёт при обучении GRPO?
2 Модель M (25.8M параметров) обучают на 0.5 млрд токенов. Сколько это FLOPs по формуле 6ND6ND? Ответ дайте в единицах 101610^{16}, округлив до десятых.
3 Что из этого — проверяемое определение готовности вехи?Можно выбрать несколько ответов
4 Почему eval-набор замораживают в первую неделю, до обучения?
5 Где в GRPO для маленькой модели узкое место по времени, если FLOPs на него мало?
  • Выпускной проект — 7 этапов: токенизатор → претрейн → SFT → GRPO → квантование → сервер → агент. У каждого есть артефакт, метрика и урок, где вы это уже делали.
  • Eval-набор замораживается в первую неделю. Через него проходит каждый этап.
  • Масштаб выбирайте под железо, но сначала прогоните весь конвейер на крошечной модели.
  • Задача для GRPO и агента должна быть проверяемой и посильной: арифметика с числовым ответом и калькулятор.
  • Уровни Simple → Boss задаются числами из metrics.json и проверяются скриптом.

Если сервер и агента выпускного проекта вы будете собирать вместе с ИИ-агентом для кода, начните со спецификаций: как превратить план проекта в требования, сценарии и задачи, которые агент выполняет по одной, — в программе SDD, её выпускной проект — урок SDD.4.4.

Дальше: как превратить журнал экспериментов и metrics.json в технический отчёт по образцу DeepSeek и Qwen и как подготовиться к защите.