Неделя 8. Собрать трансформер целиком
Учиться в приложении: тьютор, задачи с кодом →
Ядро: полный проход и лосс, инициализация, сверка логитов с настоящей моделью, трансформер с пустого файла · Глубина: трек D и вторая половина трека C · ≈ 11 ч ядро / 19 ч всё
Детали из недель 6–7 становятся моделью, и появляется единственная настоящая проверка: совпадение логитов (сырых выходов модели до softmax) с чужой эталонной реализацией. Всё дальше (обучение, инференс, сэмплирование) молча предполагает, что модель верна. Ошибка здесь не падает, а тихо ухудшает всё, что построено сверху.
Шаг 1. Полный проход и лосс
- Полный forward: embed → L×(RMSNorm → attn → residual → RMSNorm → FFN → residual) → norm → head.
Формы: id
(B, T)→ эмбеддинги(B, T, D)→ блоки форму не меняют → финальная норма → логиты(B, T, V) - Лосс со сдвигом: позиция
tпредсказывает токенt+1, то естьlogits[:, :-1]противtargets[:, 1:]. Проверка на старте: необученная модель даёт почти равномерное распределение, лосс ≈ln V(приV = 32 000это 10.37). Если сильно выше, сломан масштаб инициализации
Шаг 2. Инициализация
На пальцах. В residual stream каждый из 2L подслоёв прибавляет свой вклад. Если вклады
независимы и у каждого дисперсия 1, дисперсии складываются. При L = 8 это 16 добавок: дисперсия
потока 1 + 16 = 17, разброс вырос в √17 ≈ 4 раза. Чем глубже модель, тем сильнее разгон.
Если уменьшить каждый вклад в √(2L) раз, сумма всех добавок имеет дисперсию 1 при любой глубине.
- Инициализация, scaled init для residual-веток: все матрицы
N(0, 0.02²), а выходные проекции веток (w_o,w_down) получаютstd = 0.02/√(2L). Вывод (D7):Var(x_L) = Var(x_0) + 2L·σ², иσ² ∝ 1/(2L)делает сумму независимой от глубины - Dropout: почему в современных LLM его почти нет. Dropout борется с запоминанием повторяющихся примеров, а при предобучении модель видит почти каждый токен один раз. Запоминать нечего, а шум замедляет сходимость. Его иногда возвращают при дообучении на маленьких данных
Шаг 3. Сверка логитов с настоящей моделью
На пальцах. Две бухгалтерские книги с разными итогами не пересчитывают целиком. Идут по страницам и ищут первую, где суммы разошлись: всё до неё верно, ошибка на ней. Со сверкой модели так же: сравнивают активации после эмбеддингов, после каждого блока, внутри блока, и ищут первое место, где расхождение превысило шум округления.
- Порядок: fp32, одинаковые id на входе, forward-хуки на каждом блоке, допуск ~1e-4
- Где ломается чаще всего. RoPE у HF-реализаций Llama/Qwen поворачивает пары
(i, i + H/2), а в nanolm соседние(2i, 2i+1): строкиW_qиW_kвнутри головы надо переставить. В nanolm Swish стоит наw_up, а у HF наgate_proj: значитgate_proj → w_up,up_proj → w_gate. Дальше проверяйεнормы,rope_theta, связаны ли эмбеддинги, явноеFвместо8D/3
Типичные ошибки
- Потерять сдвиг на один токен: модель учится копировать вход, лосс подозрительно быстро падает к нулю.
Отдельного теста нет. Утечку будущего через маску ловит
test_model_is_causal - Скопировать матрицу вместо связывания: два тензора вместо одного, ловит
test_weight_tying_shares_one_tensor - Слой создан, но не подключён к графу:
test_gradients_reach_every_parameter. Общая проверка здоровьяtest_model_can_overfit_one_batch: если модель не запоминает один батч, дальше не идти
Код → nanolm/model.py: полная модель: Block, NanoLM (_init_weights, scaled init в __init__,
forward(input_ids, targets, cache) со сдвигом, num_parameters). Загрузить веса реальной маленькой модели
(например, Qwen3-0.6B) в свою реализацию и сверить логиты. Это единственная настоящая проверка.
У Qwen3 есть QK-norm (в Attention она есть, но Block её не включает), а размерность головы
задана отдельно: N·H ≠ D. Проще начать с модели архитектуры Llama, где H = D/N и QK-norm нет.
Тесты tests/test_model.py импортируют эталон напрямую: для своей модели скопируй их и поменяй импорт.
Трек E стартует здесь и идёт до конца программы: каждую пятницу с пустого файла пишешь трансформер на время. Цель: уйти с ~90 минут до ~25. Разбор 57 интервью, с которым сверялась программа, говорит прямо: имплементация и отладка трансформера всплывают на интервью так часто, что терять на них баллы просто нельзя.
Математика (трек D): D7: рост дисперсии в residual stream и выбор масштаба инициализации;
D8: ранг матрицы внимания, почему softmax поднимает ранг QKᵀ, а линейное внимание нет.
Интервью-вопрос недели: «Твои логиты расходятся с эталоном на 1e-2. Как будешь искать?»
Структура на 3 минуты: те же id и токенизатор → fp32 → хуки, первый расходящийся слой →
внутри слоя: норма (ε, fp32), q и k после RoPE (соглашение о парах), маска, порядок repeat_kv,
гейт SwiGLU → вернуть исходную точность и назвать ожидаемый допуск.
Источники: Radford et al., GPT-2 (2019), масштабированная инициализация; Touvron et al., LLaMA (2023).
Результаты недели
- Могу написать полный forward трансформера и загрузить в него веса реальной модели с совпадением логитов.
- Могу найти причину расхождения логитов, сверяя активации слой за слоем.
- Могу объяснить, зачем уменьшают инициализацию выходных проекций residual-веток с ростом глубины.
- Могу написать трансформер с пустого файла и зафиксировать своё стартовое время для трека E.
Самопроверка
- Перечисли по порядку все операции forward pass с формой тензора после каждой.
- Что станет с дисперсией residual stream после
Lслоёв без scaled init? - Почему в современных LLM почти нет dropout?