Математика глубокого обучения

Практика: трансформер с нуля

Собрать блок целиком, добавить RoPE, KV-кэш и LoRA — и проверить каждое утверждение блока измерением

Шаг 89 из 117 · ~50 мин

Блок в одной строке

Всё, что было в этом блоке, собирается в два выражения, и оба уже разобраны по частям.

xx+Attn(LN(x)),xx+MLP(LN(x))x \leftarrow x + \htmlData{k=attn}{\text{Attn}}\big(\htmlData{k=norm}{\text{LN}}(x)\big), \qquad x \leftarrow x + \htmlData{k=mlp}{\text{MLP}}\big(\htmlData{k=norm}{\text{LN}}(x)\big)

Единственное разделение труда, которое здесь есть: внимание работает между позициями, MLP — внутри позиции. Больше в блоке ничего нет.

Что реализовать

1. Десять операций через einsum

Начните с нотации, потому что дальше она сэкономит все ошибки с формами. Выпишите каждую операцию блока как строку индексов и посчитайте стоимость до написания кода.

операцияспецификация
проекция QQbnD,Dhd->bhnd
оценкиbhqd,bhkd->bhqk
выход вниманияbhqk,bhkd->bhqd
склейка головbhnd,hdD->bnD
MLP вверхbnD,DF->bnF
MLP внизbnF,FD->bnD
логитыbnD,VD->bnV

Проверьте себя на первой строке: она содержит переформовку (h d) -> h d внутри контракции, и именно поэтому проекция и разбиение на головы — это одна операция, а не две.

bnD, Dhd bhnd

индексразмерроль
b
1
остаётся
n
256
остаётся
D
384
суммируется
h
6
остаётся
d
64
остаётся
форма результата
[1, 6, 256, 64]
элементов
98,304
умножений-сложений
37,748,736
операций на элемент
384
Проекция и разбиение на головы одной операцией: сворачивается D, а h и d появляются как свободные индексы. Обратите внимание, что h·d = D — иначе это была бы не переформовка, а другая матрица.

2. Блок целиком

Масштаб nanoGPT: D=384D = 384, 66 слоёв, 66 голов, контекст 256256. На таком размере всё считается на одном CPU за минуты, а все эффекты блока видны.

Обязательные детали, каждая из которых разбиралась:

  • pre-norm, а не post-norm (урок 090);
  • инициализация He для MLP, выходные проекции ветвей поделены на 2L\sqrt{2L} (урок 100);
  • причинная маска большим отрицательным числом, а не -\infty (урок 080);
  • деление оценок на d\sqrt{d} (урок 070);
  • softmax со сдвигом на максимум (урок 060).

3. Пять измерений вместо доверия

Здесь главная часть работы. Каждое утверждение блока проверяется одной строкой на вашей же реализации.

Измерение 1: d\sqrt{d} действительно нужен. Отключите деление и напечатайте стандартное отклонение оценок и энтропию строк весов. Ожидание: разброс порядка d=8\sqrt{d} = 8, энтропия падает почти до нуля ещё до обучения.

Измерение 2: инициализация даёт нужный масштаб. Напечатайте стандартное отклонение активаций после каждого слоя на случайном входе. Ожидание: с 1/2L1/\sqrt{2L} на выходных проекциях итоговая дисперсия residual-потока около 22 при любом числе слоёв — проверьте на L=6L = 6 и L=24L = 24.

Измерение 3: RoPE относителен точно. Посчитайте оценку для пар (0,5)(0, 5), (10,15)(10, 15), (100,105)(100, 105). Ожидание: совпадение до последнего знака. Сделайте то же с прибавленной синусоидальной кодировкой — там расхождение будет порядка самого размаха оценок.

Измерение 4: кэш ничего не меняет в результате. Сгенерируйте двадцать токенов дважды — с кэшем и наивным пересчётом всей последовательности — и сравните полученные последовательности. Ожидание: побитовое совпадение. Если расходятся, ошибка в кэше, и это самый надёжный способ её найти.

Измерение 5: LoRA стартует ровно с исходной модели. Добавьте BABA к проекциям внимания при B=0B = 0 и сравните логиты до и после добавления. Ожидание: побитовое равенство. Затем сделайте один шаг оптимизации и убедитесь, что логиты изменились.

4. Генерация с кэшем

Реализуйте два режима и сравните не только результат, но и время:

режимчто считает на шаге
наивныйвесь прямой проход для всех nn токенов
с кэшемодин токен, ключи и значения читаются

Ожидание из урока 140: разница растёт линейно по длине. Заодно посчитайте размер кэша своей модели: 2nLdkv2 n L d_{\text{kv}} элементов — на этом масштабе он мал, и полезно увидеть, во что он превращается при D=4096D = 4096 и L=32L = 32.

Отдельная деталь, которую легко пропустить: на шаге генерации логиты нужны только для последней позиции. Проверьте по таблице einsum выше, сколько работы экономит этот срез — на маленькой модели со словарём 5025750257 это самая дорогая операция.

5. Обучение на маленьком корпусе

Возьмите любой текст в несколько мегабайт и обучите до осмысленных фрагментов. Что стоит наблюдать:

  • лосс на первом шаге обязан быть около lnV\ln V — это энтропия равномерного распределения по словарю (блок 4, урок 010). Отклонение означает ошибку в инициализации выходного слоя;
  • перплексия elosse^{\text{loss}} — «во сколько раз модель неуверенна» (блок 4, урок 020);
  • warmup и cosine-расписание (блок 5, урок 090) — сравните с постоянным шагом;
  • градиентный клиппинг: посмотрите, как часто он срабатывает в начале и в конце.

Куда смотреть, если не сходится

симптомчто проверять первым
лосс на первом шаге lnV\ne \ln Vинициализацию выходного слоя
лосс nan через несколько шаговсдвиг в softmax, маску -\infty, слишком большой шаг
лосс стоит на lnV\ln Vмаску (не видит ли модель будущее наоборот), знак в residual
качество внезапно ниже на коротких строкахpadding-маску
генерация с кэшем расходится с наивнойпорядок применения RoPE: позиции в кэше и у нового токена
активации растут по слояммножитель 1/2L1/\sqrt{2L} на выходных проекциях

Пятая строка — самая частая ошибка в реализациях с кэшем: при повороте нового ключа нужно взять его абсолютную позицию, а не нулевую. Проверяется измерением 4 и никак иначе.

Чем это стоит закончить

Прогоните готовую модель через утверждения блока и убедитесь, что все пять измерений дают ожидаемое. Именно этот набор — а не работающая генерация — показывает, что блок понят: генерация получается и при трёх ошибках, которые взаимно компенсируются.

Источники

Проверки

0 из 2
  1. Диагностика собственной реализации

    Отметьте все верные утверждения о сборке и отладке трансформера.

  2. Где уходит работа в вашей модели

    Реализуйте block_cost(context, model_dim, expansion, vocab, layers) — верните [block_params, attn_macs, mlp_macs, logit_macs, attn_share, logit_ratio]. Обозначим nn — контекст, DD — ширину модели, ee — коэффициент расширения MLP, VV — размер словаря, LL — число слоёв.

    • block_params = (4+2e)D2(4 + 2e)D^2 — четыре квадратные проекции внимания плюс две матрицы MLP размера D×eDD \times eD (смещения не считаем);
    • attn_macs = 2n2D2n^2D — две контракции внимания для всех nn позиций, один слой;
    • mlp_macs = 2enD22enD^2 — MLP для всех nn позиций, один слой;
    • logit_macs = nDVnDV — выходной слой, один раз на всю последовательность;
    • attn_share — доля attn_macs в работе одного слоя, то есть от суммы attn_macs, mlp_macs и проекций 4nD24nD^2;
    • logit_ratio — отношение logit_macs к работе всех LL слоёв.

    Проверить себя можно так: attn_share обязана равняться nn+(2+e)D\dfrac{n}{n + (2 + e)D} — сократите формулы и убедитесь.

    функция block_cost

    Загрузка редактора…

    Ctrl/⌘ + Enter