Нормализация и residual-связи
Блок декодера любой модели репозитория — это два подслоя (attention и FFN), каждый из которых обёрнут в одну и ту же «упаковку»: residual-связь и нормализацию. Без этой упаковки стек из десятков блоков либо не обучается вовсе, либо требует очень аккуратного подбора скорости обучения. В этой главе разбираем, что именно делает каждая часть упаковки, почему она устроена так, а не иначе, и как она менялась от GPT-1 к Gemma.
Что вы узнаете
Заголовок раздела «Что вы узнаете»- Как residual-связь сохраняет градиент в глубокой сети и что такое residual-поток.
- Почему BatchNorm не прижился в языковых моделях и как устроены LayerNorm и RMSNorm — с формулами и численными примерами.
- Зачем RMSNorm нужен
eps, почему у Mistral он1e-5, а у LLaMA1e-6, и что за множитель(1 + w)у Gemma. - Почему нормализацию в половинной точности считают во float32.
- Чем post-LN отличается от pre-LN, почему post-LN требует warmup и зачем pre-LN-модели финальная нормализация.
- Как всё это реализовано в декодерах
llm/core.
Предварительные знания
Заголовок раздела «Предварительные знания»- Общая схема decoder-only трансформера — Языковое моделирование.
- Что такое подслой attention — Attention и его виды.
- Производная сложной функции (цепное правило) и понятие градиента; обратное распространение в общих чертах — Обучение.
Residual-связи
Заголовок раздела «Residual-связи»Проблема глубины
Заголовок раздела «Проблема глубины»Блок декодера — функция , которая переводит скрытые состояния в новые той же формы. Модель из блоков — композиция . По цепному правилу градиент функции потерь по входу первого блока — произведение якобианов. Если типичный «коэффициент усиления» каждого множителя меньше единицы, произведение экспоненциально стремится к нулю (затухание градиента, vanishing gradient), если больше — к бесконечности (взрыв градиента, exploding gradient). При даже множитель даёт .
Эмпирически это выглядело так: He et al. обнаружили, что простая свёрточная сеть из 56 слоёв имеет большую ошибку на обучающей выборке, чем сеть из 20 слоёв, — более глубокая модель хуже даже оптимизируется (He et al., 2016, разд. 1, рис. 1).
Определение
Заголовок раздела «Определение»Residual-связь (residual connection, skip connection) прибавляет к выходу подслоя его же вход:
где:
- — вход подслоя для одного токена (строка матрицы );
- — сам подслой (attention или FFN вместе с их нормализацией); вход и выход обязаны иметь одинаковую размерность , иначе складывать нельзя;
- — выход.
Идея из He et al., 2016 (разд. 3, формула (1)): подслою не нужно выучивать всё отображение целиком, ему достаточно выучить поправку (residual, «остаток») к тождественному отображению. Если слой бесполезен, ему проще всего выучить , и тогда блок просто пропускает сигнал дальше — добавление слоя не может сделать сеть хуже.
Почему помогает градиенту
Заголовок раздела «Почему помогает градиенту»Продифференцируем по :
где:
- — единичная матрица (производная слагаемого по самому себе);
- — якобиан подслоя.
В скалярном случае () это просто . Единица — главный смысл конструкции: даже если мал, производная блока около единицы, а не около нуля.
Для стека , , градиент от выхода к входу блока равен произведению:
где — вход блока , — выход последнего блока, — индекс блока в произведении; множители — матрицы, и произведение берётся в порядке от последнего блока к первому (при раскрытии скобок порядок на слагаемое и сумму первого порядка не влияет). Раскрывая скобки, получаем слагаемое : градиент функции потерь попадает на любой слой напрямую, без умножения на якобианы промежуточных слоёв. Этот аргумент подробно разобран в продолжении работы — He et al., 2016b.
Пример. Пусть каждый из 10 слоёв — скалярное умножение с .
| Производная одного слоя | Производная 10 слоёв | |
|---|---|---|
| без residual: | ||
| с residual: |
Без residual сигнал ошибки до первого слоя фактически не доходит.
Residual-поток
Заголовок раздела «Residual-поток»В трансформере residual-связи стоят вокруг каждого подслоя, поэтому удобно смотреть на модель так: есть residual-поток (residual stream) — вектор для каждого токена, который проходит от эмбеддинга до выходной проекции, а каждый подслой читает из него (через нормализацию) и дописывает в него поправку. Для pre-LN (см. ниже) выход стека буквально равен сумме:
где — эмбеддинг токена (с позиционным, если он есть), а в скобках — вклады attention и FFN каждого блока (их аргументы опущены). Термин и эта точка зрения популярны в работах по интерпретируемости, см. Elhage et al., 2021.
Из этой формулы видно ограничение: все подслои пишут в одно пространство размерности , и именно поэтому выходные проекции attention () и FFN (второй линейный слой) возвращают размерность к .
Проблема масштаба активаций
Заголовок раздела «Проблема масштаба активаций»Residual решает проблему градиента, но создаёт другую: масштаб residual-потока растёт с глубиной. Если вклады подслоёв примерно независимы и каждый имеет дисперсию по каждой компоненте, то после сложений:
где — одна компонента вектора после слоёв, — компонента эмбеддинга. Дисперсия суммы независимых слагаемых равна сумме дисперсий.
Пример. , 12 блоков по два подслоя, каждый добавляет дисперсию 1: , стандартное отклонение выросло с 1 до 5.
Чем это плохо:
- следующий подслой получает на вход векторы разного масштаба в зависимости от глубины — его веса должны подстраиваться под этот масштаб;
- в attention скалярные произведения растут квадратично с масштабом входа, softmax насыщается;
- при обучении распределение входов каждого слоя «плывёт» по мере изменения весов предыдущих слоёв.
Два средства применяются вместе:
- Инициализация, уменьшающая вклад подслоёв (в GPT-2 — веса выходных проекций , т. е. со стандартным отклонением , см. gpt2.md).
- Нормализация — явное приведение вектора к фиксированному масштабу перед подслоем (или после сложения). Ей посвящена оставшаяся часть главы.
BatchNorm и почему он не подходит
Заголовок раздела «BatchNorm и почему он не подходит»Первой широко используемой нормализацией был Batch Normalization (Ioffe & Szegedy, 2015). Для каждого признака он вычисляет среднее и дисперсию по батчу:
где:
- — признак примера ;
- — размер батча (для последовательностей усреднение шло бы по позициям);
- — статистики признака , общие для всего батча;
- — малая константа против деления на ноль.
Нормализация идёт вдоль столбца матрицы «примеры × признаки». Для языковых моделей это неудобно по нескольким причинам:
- Зависимость от батча. Выход для одного примера зависит от остальных примеров батча. При генерации батч часто из одной последовательности, и статистики приходится брать из скользящих средних, накопленных при обучении, — поведение при обучении и инференсе расходится.
- Утечка из будущего. Если усреднять по позициям , статистика позиции включает токены — то, что causal-маска старательно скрывает (см. Маски).
- Паддинг и разная длина. Pad-токены попадают в статистики, если их специально не исключать.
- Шум статистик. В NLP-трансформерах статистики батча сильно колеблются от шага к шагу, и BatchNorm работает заметно хуже LayerNorm (Shen et al., 2020).
Нужна нормализация, которая смотрит на один вектор одного токена и не зависит ни от батча, ни от соседних позиций.
LayerNorm
Заголовок раздела «LayerNorm»Layer Normalization (Ba, Kiros & Hinton, 2016) считает статистики по признакам одного вектора:
где:
- — скрытое состояние одного токена (одна строка );
- — среднее компонент, — их дисперсия (делится на , а не на );
- — константа устойчивости (в
nn.LayerNormпо умолчанию ); - — нормализованный вектор;
- — обучаемые масштаб (gain) и сдвиг (bias), инициализируются единицами и нулями;
- — выход.
Ось нормализации. Для тензора [B, T, d] статистики считаются по последней оси: у каждой из позиций свои и . Ни батч, ни соседние токены на результат не влияют — все проблемы BatchNorm из предыдущего раздела исчезают. В PyTorch это nn.LayerNorm(d): normalized_shape = d означает «нормализовать по последнему измерению размера ».
Интуиция. После нормализации у среднее 0 и средний квадрат почти 1, т. е. длина . Геометрически: вычитание среднего проецирует вектор на гиперплоскость, перпендикулярную вектору , а деление — растягивает до сферы радиуса . Затем и возвращают сети свободу: если ей нужен другой масштаб или сдвиг по какому-то признаку, она их выучит. Без нормализация жёстко ограничивала бы то, что может представить слой.
Инвариантность. Для любых и (при ):
где . Сдвиг уходит при вычитании среднего, множитель — при делении на стандартное отклонение. Значит, никакое растяжение residual-потока до подслоя не доходит.
Численный пример
Заголовок раздела «Численный пример»Возьмём , , , , пренебрежём.
- Среднее: .
- Отклонения: .
- Дисперсия: , .
- Выход: .
Проверка: сумма равна 0, сумма квадратов .
import torchfrom torch import nn
x = torch.tensor([[1., 2., 3., 6.]])print(nn.LayerNorm(4)(x)) # tensor([[-1.0690, -0.5345, 0.0000, 1.6036]], ...)print(nn.LayerNorm(4)(3 * x + 10)) # то же самое: инвариантность к масштабу и сдвигуRMSNorm
Заголовок раздела «RMSNorm»Определение
Заголовок раздела «Определение»RMSNorm (Root Mean Square Layer Normalization, Zhang & Sennrich, 2019) убирает из LayerNorm центрирование и сдвиг:
где:
- — скрытое состояние одного токена;
- — среднеквадратичное значение компонент (корень из среднего квадрата), — под корнем;
- — обучаемый масштаб (аналог ), инициализируется единицами; в коде — параметр
_w; - — выход.
Ось та же, что у LayerNorm, — последняя. Если среднее и , RMSNorm и LayerNorm совпадают: при дисперсия равна среднему квадрату.
Что убрано и почему
Заголовок раздела «Что убрано и почему»Убраны вычитание среднего и сдвиг . Гипотеза авторов: успех LayerNorm объясняется в первую очередь инвариантностью к масштабу (re-scaling invariance), а инвариантность к сдвигу (re-centering) не так важна. RMSNorm сохраняет первую и теряет вторую:
Вывод инвариантности к масштабу
При :
Поэтому
Со сдвигом так не выходит: не выражается через одним множителем, и направление вектора отличается от направления . При инвариантность приближённая: она нарушается, только когда средний квадрат сравним с .
Геометрически RMSNorm просто проецирует вектор на сферу радиуса (направление сохраняется), а LayerNorm сначала ещё и убирает компоненту вдоль .
Вычислительная экономия. LayerNorm требует двух редукций по вектору (среднее, затем дисперсия — или сумма и сумма квадратов в один проход), вычитания и сложения с ; RMSNorm — одной редукции (сумма квадратов) и одного умножения. Параметров вдвое меньше: вместо . По данным авторов, замена LayerNorm на RMSNorm ускоряла обучение разных моделей на 7–64 % при сопоставимом качестве (аннотация статьи). В масштабе всей LLM нормализация — малая доля FLOP, но это операция, ограниченная пропускной способностью памяти, и лишний проход по тензору заметен.
Численный пример
Заголовок раздела «Численный пример»Тот же вектор , :
- Квадраты: , средний квадрат .
- .
- Выход: .
В отличие от LayerNorm, все компоненты остались положительными: среднее не вычиталось. Сдвинем вход на 10: даёт — другой результат, а — тот же, что у .
import torchfrom llm.core.rms_norm import RMSNorm
norm = RMSNorm(4) # eps=1e-6 по умолчаниюx = torch.tensor([[1., 2., 3., 6.]])print(norm(x)) # tensor([[0.2828, 0.5657, 0.8485, 1.6971]], ...)print(norm(3 * x)) # то жеprint(norm(x + 10)) # tensor([[0.8375, 0.9137, 0.9898, 1.2182]], ...)eps и его роль
Заголовок раздела «eps и его роль»решает две задачи.
- Защита от деления на ноль. Для нулевого вектора .
- Ограничение усиления. Выход равен , где — средний квадрат. Когда , ни на что не влияет; когда вектор почти нулевой, множитель не превышает , и шум не раздувается до единичной нормы.
| Средний квадрат | Множитель при | Множитель при |
|---|---|---|
Значение — гиперпараметр, с которым модель обучалась: у LLaMA и Gemma , у Mistral 7B и Mixtral 8x7B (ключ rms_norm_eps конфига HF и этого репозитория). При типичном разница выходов — около относительно, но при загрузке чужих весов её лучше не вносить: ошибки накапливаются по нормализациям. В репозитории eps задаётся ключом rms_norm_eps (по умолчанию 1e-6) — см. бэклог, пункт 50.
Вариант Gemma: множитель (1 + w)
Заголовок раздела «Вариант Gemma: множитель (1 + w)»В Gemma (реализация HF GemmaRMSNorm) вес хранится иначе:
где — хранимый параметр. Это та же функция, что обычный RMSNorm с : при инициализации обе дают множитель 1. Разница — в параметризации: «нулевая точка» параметра соответствует тождественному масштабу. Одно из практических следствий: weight decay, стягивающий параметры к нулю, в такой параметризации тянет масштаб к 1, а не к 0 (это наше объяснение; в статье Gemma мотивация не приводится).
В репозитории отдельного класса для Gemma нет: RMSNorm умножает на сам вес, а при загрузке весов HF к ним прибавляется 1 — convert_hf_state_dict в models/gemma/hf_weights.py:
for key in result: if key.endswith("._w"): # веса RMSNorm: (1 + w) в HF → w здесь result[key] = result[key] + 1Подробнее — gemma.md и бэклог, пункт 46.
Нормализация в половинной точности
Заголовок раздела «Нормализация в половинной точности»Проблема
Заголовок раздела «Проблема»В float16 наибольшее конечное число — . Квадрат числа переполняется, как только . Значения такого порядка в residual-потоке вполне возможны: он не нормализован (в pre-LN — сумма вкладов всех слоёв), и отдельные компоненты у обученных моделей бывают большими.
Пример. Вектор во float16:
- →
inf; - средний квадрат
inf, ; - выход — вся информация о токене потеряна.
Правильный ответ: средний квадрат , , выход .
У bfloat16 диапазон как у float32 (до ), переполнения нет, но мантисса короткая — 8 значащих бит (около 2–3 десятичных знаков), и сумма тысяч квадратов теряет точность.
Решение в репозитории
Заголовок раздела «Решение в репозитории»RMSNorm.forward в core/rms_norm.py для float16/bfloat16 считает нормализацию во float32:
x_compute = x.float() if x.dtype in (torch.float16, torch.bfloat16) else x # 1. повышаем точностьrms = (x_compute.pow(2).mean(-1, keepdim=True) + self._eps) ** 0.5 # 2. RMS по последней осиnorm_x = (x_compute / rms).to(x.dtype) # 3. делим и возвращаем dtypereturn self._w * norm_x # 4. умножаем на вес g- Строка 1: только для половинной точности; для float32/float64 ничего не меняется, результат побитово прежний.
- Строка 2 — формула целиком, под корнем;
mean(-1)— ось признаков. - Строка 3: нормализованный вектор по модулю не больше , его уже можно безопасно вернуть в исходный dtype.
- Строка 4: умножение на вес — после приведения, как в
LlamaRMSNormHF.GemmaRMSNormумножает на ещё во float32, поэтому в bf16 результаты могут расходиться в последних битах (gemma.md).
import torchfrom llm.core.rms_norm import RMSNorm
h = torch.tensor([[300., 1., 1., 1.]], dtype=torch.float16)naive = h / (h.pow(2).mean(-1, keepdim=True) + 1e-6) ** 0.5print(naive) # tensor([[0., 0., 0., 0.]], dtype=torch.float16)print(RMSNorm(4).half()(h)) # tensor([[2.0000, 0.0067, 0.0067, 0.0067]], dtype=torch.float16, ...)nn.LayerNorm (GPT-1, GPT-2) заботиться об этом не нужно: встроенная реализация PyTorch сама накапливает статистики в повышенной точности.
Где ставить нормализацию: post-LN и pre-LN
Заголовок раздела «Где ставить нормализацию: post-LN и pre-LN»Post-LN
Заголовок раздела «Post-LN»В исходном трансформере (Vaswani et al., 2017, разд. 3.1) и в GPT-1 нормализация стоит после residual-сложения — post-LN:
где — вход блока, — промежуточное состояние после attention, — выход блока; все — , LN применяется к каждой строке.
%%{init: {"flowchart": {"rankSpacing": 28, "nodeSpacing": 28}}}%%
flowchart TB
subgraph Post["Блок post-LN (GPT-1)"]
direction TB
X(["x"]):::io --> Attn["Attention"]:::blue
Attn --> A1(("+")):::add
X -. residual .-> A1
A1 --> N1["LayerNorm"]:::grayHl
N1 --> FFN["FFN"]:::purple
FFN --> A2(("+")):::add
N1 -. residual .-> A2
A2 --> N2["LayerNorm"]:::grayHl
N2 --> Y(["y"]):::io
end
style Post fill:transparent,stroke:#82b366,stroke-width:2px,color:#5b9a3c
classDef io fill:#ffffff,stroke:#999999,color:#1a1a1a;
classDef add fill:#ffffff,stroke:#666666,color:#1a1a1a;
classDef blue fill:#dae8fc,stroke:#6c8ebf,color:#1a1a1a;
classDef blueHl fill:#dae8fc,stroke:#2f5f9e,stroke-width:3px,color:#1a1a1a;
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#1a1a1a;
classDef purpleHl fill:#e1d5e7,stroke:#6a3d85,stroke-width:3px,color:#1a1a1a;
classDef gray fill:#f5f5f5,stroke:#666666,color:#1a1a1a;
classDef grayHl fill:#f5f5f5,stroke:#333333,stroke-width:3px,color:#1a1a1a;
classDef gold fill:#fff2cc,stroke:#d6b656,color:#1a1a1a;
classDef rope fill:#d5f0ec,stroke:#3a9e8f,color:#1a1a1a;
classDef ropeHl fill:#d5f0ec,stroke:#1f6f63,stroke-width:3px,color:#1a1a1a;
classDef dim fill:#f5f5f5,stroke:#bbbbbb,color:#999999,stroke-dasharray:4 3;
Нормализация лежит на пути residual: сигнал от входа к выходу блока проходит через два LN. Residual-потока в чистом виде нет — после каждого сложения вектор снова приводится к единичному масштабу.
В GPT-2 нормализацию перенесли на вход подслоя — pre-LN (Radford et al., 2019, разд. 2.3, по аналогии с pre-activation ResNet из He et al., 2016b):
и после всего стека добавили финальную нормализацию:
где — выход последнего блока, — финальная нормализация, — выходная проекция на словарь.
%%{init: {"flowchart": {"rankSpacing": 28, "nodeSpacing": 28}}}%%
flowchart TB
subgraph Pre["Блок pre-LN (GPT-2, LLaMA, Mistral, Mixtral, Gemma)"]
direction TB
X(["x"]):::io --> N1["LayerNorm / RMSNorm"]:::grayHl
N1 --> Attn["Attention"]:::blue
Attn --> A1(("+")):::add
X -. residual .-> A1
A1 --> N2["LayerNorm / RMSNorm"]:::grayHl
N2 --> FFN["FFN"]:::purple
FFN --> A2(("+")):::add
A1 -. residual .-> A2
A2 --> Y(["y"]):::io
end
Y --> NF["финальная нормализация<br/>(одна на всю модель)"]:::grayHl --> Lin["Linear → vocab_size"]:::gray
style Pre fill:transparent,stroke:#82b366,stroke-width:2px,color:#5b9a3c
classDef io fill:#ffffff,stroke:#999999,color:#1a1a1a;
classDef add fill:#ffffff,stroke:#666666,color:#1a1a1a;
classDef blue fill:#dae8fc,stroke:#6c8ebf,color:#1a1a1a;
classDef blueHl fill:#dae8fc,stroke:#2f5f9e,stroke-width:3px,color:#1a1a1a;
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#1a1a1a;
classDef purpleHl fill:#e1d5e7,stroke:#6a3d85,stroke-width:3px,color:#1a1a1a;
classDef gray fill:#f5f5f5,stroke:#666666,color:#1a1a1a;
classDef grayHl fill:#f5f5f5,stroke:#333333,stroke-width:3px,color:#1a1a1a;
classDef gold fill:#fff2cc,stroke:#d6b656,color:#1a1a1a;
classDef rope fill:#d5f0ec,stroke:#3a9e8f,color:#1a1a1a;
classDef ropeHl fill:#d5f0ec,stroke:#1f6f63,stroke-width:3px,color:#1a1a1a;
classDef dim fill:#f5f5f5,stroke:#bbbbbb,color:#999999,stroke-dasharray:4 3;
Теперь путь residual — чистое тождество: от эмбеддинга до финальной нормализации сигнал только складывается, и производная по нему содержит слагаемое из раздела про residual-связи. Нормализация стоит на ответвлении и влияет лишь на то, что подслой читает.
Анализ Xiong et al.: почему post-LN нужен warmup
Заголовок раздела «Анализ Xiong et al.: почему post-LN нужен warmup»Практики давно заметили, что post-LN-трансформер плохо обучается без warmup — фазы, в которой скорость обучения линейно растёт от нуля за первые тысячи шагов (см. Обучение). Xiong et al., 2020 объяснили это теоретически, анализируя градиенты в момент инициализации:
- Post-LN (теорема 1): норма градиента по весам последнего FFN оценивается как — не уменьшается с глубиной . Более того, ожидаемая величина градиентов растёт с номером слоя: у слоёв ближе к выходу градиенты крупнее (там же; подтверждается экспериментами статьи).
- Pre-LN: та же норма — , то есть убывает как , и градиенты по слоям примерно одинаковы.
где — размерность модели, — число слоёв; оценки — с большой вероятностью при случайной инициализации.
Интуиция. В post-LN слагаемое проходит через LN каждого следующего блока. LN делит на стандартное отклонение суммы , которое больше, чем у , поэтому вклад ранних слоёв и градиент к ним последовательно «сжимаются», а последние слои получают крупные градиенты. Большой градиент у выхода плюс большая скорость обучения с первого шага — и обучение расходится. Warmup даёт время, пока оптимизатор (Adam) накопит статистики, а веса выйдут из опасной области. В pre-LN градиенты хорошо обусловлены с самого начала; авторы показывают, что pre-LN-трансформер обучается без warmup и быстрее.
Цена pre-LN — растущий масштаб residual-потока: вклады слоёв только складываются, норма увеличивается с глубиной, и каждый следующий подслой меняет поток относительно всё меньше. Для моделей из репозитория (до десятков слоёв) это не мешает; на практике pre-LN стал стандартом.
Зачем pre-LN нужна финальная нормализация
Заголовок раздела «Зачем pre-LN нужна финальная нормализация»В post-LN последняя операция блока — LN, поэтому выход стека уже нормализован и сразу идёт в выходную проекцию. В репозитории у GPT-1 финальной нормализации нет (см. gpt.md).
В pre-LN выход стека — ненормализованная сумма с масштабом, растущим с . Если подать её прямо в , логиты будут иметь масштаб, зависящий от глубины и от конкретного токена, и softmax станет неоправданно острым или плоским. Финальная нормализация ( в GPT-2 — ln_f, в LLaMA/Mistral/Mixtral/Gemma — norm) приводит состояние к фиксированному масштабу перед проекцией. GPT-2 добавил её одновременно с переходом на pre-LN (разд. 2.3 статьи).
Какие модели что используют
Заголовок раздела «Какие модели что используют»| Модель | Расстановка | Нормализация | Финальная норма | Параметров на одну норму | |
|---|---|---|---|---|---|
| Transformer (Vaswani, 2017) | post-LN | LayerNorm | — | нет | |
| GPT-1 | post-LN | LayerNorm | 1e-5 (nn.LayerNorm) | нет | |
| GPT-2 | pre-LN | LayerNorm | 1e-5 | да | |
| LLaMA | pre-LN | RMSNorm | 1e-6 | да | |
| Mistral 7B | pre-LN | RMSNorm | 1e-5 | да | |
| Mixtral 8x7B | pre-LN | RMSNorm | 1e-5 | да | |
| Gemma | pre-LN | RMSNorm, , во float32 | 1e-6 | да |
В каждом блоке две нормализации, поэтому всего в модели нормализаций у pre-LN и у post-LN. Параметров в них ничтожно мало: у LLaMA 7B (, ) — из 6,7 млрд.
Реализация в репозитории
Заголовок раздела «Реализация в репозитории»| Модель | Блок | Расстановка | Нормализация в блоке | Финальная |
|---|---|---|---|---|
| GPT-1 | GptDecoder в core/gpt_decoder.py | post-LN | nn.LayerNorm(emb_size) ×2 | нет |
| GPT-2 | Gpt2Decoder в core/gpt2_decoder.py | pre-LN | nn.LayerNorm(emb_size) ×2 | GPT2._norm = nn.LayerNorm |
| LLaMA | CachedDecoder в core/cached_decoder.py | pre-LN | norm_layer(emb_size) ×2, LLaMA передаёт partial(RMSNorm, eps=norm_eps) | Llama._norm = RMSNorm |
| Mistral | MistralDecoder в core/mistral_decoder.py | pre-LN | RMSNorm(emb_size, eps=norm_eps) ×2 | Mistral._norm = RMSNorm |
| Mixtral | MixtralDecoder в core/mixtral_decoder.py | pre-LN | RMSNorm(emb_size, eps=norm_eps) ×2 | Mixtral._norm = RMSNorm |
| Gemma | GemmaDecoder в core/gemma_decoder.py | pre-LN | RMSNorm(emb_size, eps=norm_eps) ×2 | Gemma._norm = RMSNorm |
Post-LN — GptDecoder.forward: сложение внутри вызова нормализации.
attention, kv_caches = self._heads(x, use_cache=use_cache, cache=cache)out = self._norm1(attention + x) # x' = LN1(x + Attn(x))ffn_out = self._ff(out)result = self._norm2(ffn_out + out) # y = LN2(x' + FFN(x'))Pre-LN — Gpt2Decoder.forward, CachedDecoder.forward и декодеры Mistral, Mixtral, Gemma устроены одинаково (у последних трёх — те же строки с другими модулями):
norm1_out = self._norm1(x) # LN1(x)attention, kv_caches = self._heads(norm1_out, use_cache=use_cache, cache=cache)out = attention + x # x' = x + Attn(LN1(x))norm2_out = self._norm2(out) # LN2(x')ffn_out = self._ff(norm2_out)result = ffn_out + out # y = x' + FFN(LN2(x'))Финальная нормализация применяется в forward модели после цикла по декодерам: out = self._norm(out), затем logits = self._linear(out) (например, models/gpt/gpt2.py, models/llama/llama.py).
Детали RMSNorm:
- конструктор
RMSNorm(dim, eps=1e-6);eps <= 0—ValueError; - вес —
nn.Parameter(torch.ones(dim))с именем_w(вstate_dictключи вида_decoders.0._norm1._w), поэтому конвертеры весов HF переименовываютinput_layernorm.weight→_norm1._w(models/llama/hf_weights.py); epsне параметр и не буфер — в чекпоинт не попадает, задаётся конфигом (rms_norm_eps).
Модели читают eps так: norm_eps = config.get("rms_norm_eps", 1e-6) и передают его во все нормализаций.
Типичные ошибки и тонкости
Заголовок раздела «Типичные ошибки и тонкости»- Нормализация «не по той оси». LayerNorm и RMSNorm нормализуют вектор одного токена (последнюю ось), а не столбец признака по батчу. Если по ошибке нормализовать по оси , в статистику попадут будущие токены.
- Забытая финальная нормализация в pre-LN-модели: модель обучится, но хуже, а загруженные веса дадут неверные логиты.
- Несовпадающий
epsпри загрузке весов: расхождение маленькое, но накапливается по слоям. Для Mistral/Mixtral —"rms_norm_eps": 1e-5. - Gemma без
+1: если загрузить весаGemmaRMSNormкак есть, все масштабы окажутся около нуля вместо единицы, и выход модели будет бессмысленным. - Нормализация в float16 без повышения точности: при квадрат переполняется, и выход обнуляется или становится
nan. - Post-LN без warmup расходится или обучается медленно; pre-LN к этому гораздо устойчивее.
- Weight decay на весах нормализации — распространённая практика их исключать; в варианте этот вопрос стоит иначе (см. выше).
- Residual-связь даёт производную : градиент доходит до любого слоя напрямую.
- Residual-поток растёт по масштабу с глубиной; нормализация приводит вход подслоя к фиксированному масштабу.
- BatchNorm зависит от батча и соседних позиций и в языковых моделях не используется.
- LayerNorm: центрирование, деление на стандартное отклонение, и , параметров; инвариантен к масштабу и сдвигу.
- RMSNorm: только деление на RMS и масштаб , параметров; инвариантен к масштабу, но не к сдвигу; дешевле.
- ограничивает усиление почти нулевых векторов; он — часть обученной модели (
1e-6LLaMA/Gemma,1e-5Mistral/Mixtral). - В половинной точности RMSNorm считается во float32: во float16 квадрат переполняется уже при .
- Post-LN (GPT-1) даёт крупные градиенты у выхода и требует warmup; pre-LN (GPT-2 и все последующие) — чистый residual-путь и финальная нормализация.
Вопросы и упражнения
Заголовок раздела «Вопросы и упражнения»- Посчитайте вручную LayerNorm и RMSNorm (без , , ) для .
Ответ
LayerNorm: , отклонения , , выход .
RMSNorm: средний квадрат , , выход .
- Докажите, что при и . Верно ли это при ?
Ответ
Среднее: . Отклонения: . Дисперсия: , стандартное отклонение . Итог: . При выход тот же, при — меняет знак.
- Сколько обучаемых параметров во всех нормализациях GPT-2 small (, ) и LLaMA 7B (, )?
Ответ
GPT-2: LayerNorm по параметров — . LLaMA 7B: RMSNorm по — .
- Модель из 20 слоёв, каждый слой — , . Найдите производную выхода по входу без residual-связей и с ними. Что будет с residual при ?
Ответ
Без residual: . С residual: . При каждый множитель , и производная — ноль: residual гарантирует слагаемое , но якобиан подслоя может его сократить. На практике такое вырождение при обучении не возникает, но это напоминание, что «единица» — не магия.
- Во float16 вектор . Что вернёт наивный RMSNorm (все вычисления во float16) и что —
RMSNormиз репозитория? Каким должен быть правильный ответ?
Ответ
— переполнение, средний квадрат inf, наивный выход . RMSNorm из репозитория считает во float32: средний квадрат , , выход — это и есть правильный ответ.
- Почему у GPT-1 в репозитории нет финальной нормализации, а у GPT-2 есть? Что произойдёт, если удалить
_normиз обученной GPT-2?
Ответ
В post-LN последняя операция каждого блока — LayerNorm, выход стека уже нормализован. В pre-LN выход — ненормализованная сумма вкладов всех слоёв, её масштаб растёт с глубиной. Без _norm на вход _linear пойдут векторы гораздо большего и разного масштаба, чем при обучении, и логиты станут неверными (слишком «острое» распределение).
- В чекпоинте Gemma вес нормализации . Какие значения окажутся в
_wпослеconvert_hf_state_dictи какой множитель применится к нормализованному вектору?
Ответ
_w ; RMSNorm умножает на _w, т. е. на те же , что GemmaRMSNorm.
- (Обсуждение) Xiong et al. показывают, что в post-LN градиенты у выхода не уменьшаются с глубиной, а в pre-LN убывают как . Объясните, почему из этого следует, что post-LN нужна меньшая начальная скорость обучения (warmup), а pre-LN — нет.
Ответ
Шаг обновления весов пропорционален скорости обучения, умноженной на градиент (для SGD буквально, для Adam — через ещё не накопленные моменты). Если градиенты последних слоёв post-LN крупные, при обычной скорости обучения первые шаги слишком велики и выводят модель в плохую область. Warmup делает начальные шаги маленькими. В pre-LN градиенты с самого начала меньше и равномерны по слоям, поэтому полная скорость обучения безопасна сразу.
Литература
Заголовок раздела «Литература»- He, Zhang, Ren, Sun. Deep Residual Learning for Image Recognition. 2016. arXiv:1512.03385
- He, Zhang, Ren, Sun. Identity Mappings in Deep Residual Networks. 2016. arXiv:1603.05027 — pre-activation, анализ прямого пути градиента
- Ioffe, Szegedy. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. 2015. arXiv:1502.03167
- Ba, Kiros, Hinton. Layer Normalization. 2016. arXiv:1607.06450
- Zhang, Sennrich. Root Mean Square Layer Normalization. 2019. arXiv:1910.07467
- Shen, Yao, Gholami, Mahoney, Keutzer. PowerNorm: Rethinking Batch Normalization in Transformers. 2020. arXiv:2003.07845
- Xiong et al. On Layer Normalization in the Transformer Architecture. 2020. arXiv:2002.04745
- Vaswani et al. Attention Is All You Need. 2017. arXiv:1706.03762
- Radford, Wu, Child, Luan, Amodei, Sutskever. Language Models are Unsupervised Multitask Learners. OpenAI, 2019. PDF
- Elhage et al. A Mathematical Framework for Transformer Circuits. Anthropic, 2021. transformer-circuits.pub