Feed-forward сеть и активации
Второй подслой каждого блока декодера — feed-forward сеть (FFN, иногда MLP): два-три линейных слоя с нелинейностью между ними. Она проще attention, но в ней сосредоточено около двух третей параметров блока. За шесть лет от GPT-1 до Gemma FFN менялась сильнее всех остальных частей: ReLU → GELU → gated-варианты SwiGLU и GeGLU, а скрытый размер из «всегда » стал отдельным гиперпараметром.
Что вы узнаете
Заголовок раздела «Что вы узнаете»- Что делает FFN в блоке и почему её называют памятью «ключ–значение».
- Классический FFN Vaswani: формулу, размеры матриц и число параметров .
- Активации ReLU, GELU (точную и tanh-аппроксимацию — откуда константы и и какова её ошибка), SiLU/Swish.
- Семейство GLU: SwiGLU и GeGLU, три матрицы вместо двух и роль гейта.
- Как выбирают скрытый размер: с округлением у LLaMA, у Mistral, у Gemma.
- Как всё это реализовано в
FeedForward,SwiGLU,GeGLU,GELU,SiLU.
Предварительные знания
Заголовок раздела «Предварительные знания»- Место FFN в блоке, residual-связи и нормализация — Нормализация и residual-связи.
- Attention как механизм, смешивающий позиции, — Attention и его виды.
- Линейный слой , сигмоида, производная.
Роль FFN в блоке
Заголовок раздела «Роль FFN в блоке»Позиционно-независимая обработка
Заголовок раздела «Позиционно-независимая обработка»Attention — единственное место блока, где токены обмениваются информацией: выход позиции — смесь значений других позиций. FFN, наоборот, применяется к каждой позиции отдельно и одинаково — в статье трансформера она так и называется: position-wise feed-forward network (Vaswani et al., 2017, разд. 3.3):
где:
- — вход подслоя (после нормализации), — его строка для позиции ;
- — одна и та же функция с одними и теми же весами для всех позиций.
Поэтому в коде FFN — это просто nn.Linear, применённые к тензору [B, T, d]: линейный слой PyTorch действует на последнюю ось, а оси батча и позиции для него — «номер примера». Схема работы блока: attention собирает для токена контекст, FFN перерабатывает собранное.
FFN как память ключ–значение
Заголовок раздела «FFN как память ключ–значение»Запишем классический FFN (см. ниже) покомпонентно. Пусть — -й столбец первой матрицы, — -я строка второй (жирные — векторы «ключа» и «значения» нейрона; не путать с числом экспертов , размером словаря и матрицами из attention):
где:
- — число скрытых нейронов;
- — активация (ReLU, GELU, …);
- — -я компонента сдвига первого слоя, — сдвиг второго слоя;
- — насколько вход «похож» на ключ ;
- — вектор, который добавляется в residual-поток, если ключ сработал.
Это похоже на attention без softmax, где ключи и значения — не токены контекста, а обученные параметры. Geva et al., 2021 показали, что это не только аналогия: ключи многих нейронов срабатывают на понятные человеку шаблоны входа (в нижних слоях — поверхностные, например конкретное окончание слова, в верхних — смысловые), а значения, спроецированные на словарь, повышают вероятность токенов, которые обычно следуют за этим шаблоном (особенно в верхних слоях). FFN — основное хранилище «знаний» модели, и чем больше , тем больше таких пар ключ–значение.
Классический FFN
Заголовок раздела «Классический FFN»Формула
Заголовок раздела «Формула»где:
- — вход (строка; для всего тензора —
[B, T, d]); - , — расширяющий слой;
- — поэлементная нелинейность; у Vaswani — ReLU, ;
- , — сжимающий слой;
- выход — , той же размерности, что вход, чтобы его можно было прибавить к residual-потоку.
В трансформере , , т. е. ; это соотношение унаследовали GPT-1 и GPT-2 (, ).
flowchart LR
X(["x · [B, T, d]"]):::io --> L1["Linear W1<br/>d → 4d"]:::gray --> Act["активация"]:::purple --> L2["Linear W2<br/>4d → d"]:::gray --> Drop["Dropout"]:::gray --> Out(["out · [B, T, d]"]):::io
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;
Зачем нелинейность. Без два слоя схлопываются в один: — линейное отображение с матрицей размера . Расширение до стало бы бессмысленным.
Зачем расширение. Каждый скрытый нейрон — одна пара ключ–значение. Широкий скрытый слой даёт много «детекторов», а сжатие обратно до складывает их вклады.
Число параметров
Заголовок раздела «Число параметров»При : .
Для сравнения, attention с имеет параметров (четыре матрицы : ), поэтому FFN — около параметров блока.
Примеры.
- Учебный конфиг, : .
- GPT-2 small, : на блок, млн на 12 блоков.
Вычисления: на каждый токен примерно 2 FLOP на вес (умножение и сложение), т. е. FLOP — FFN доминирует и в стоимости прямого прохода при небольших .
Активации
Заголовок раздела «Активации»где — одна компонента предактивации .
Широкое распространение получила после работы Nair & Hinton (2010); в трансформере — у Vaswani. Плюсы: дёшево, градиент 1 на положительной полуоси. Минусы: излом в нуле и нулевой градиент при — нейрон, который для всех входов оказался в отрицательной области, перестаёт обучаться («мёртвый» нейрон). В репозитории — вариант activation="relu" в FeedForward.
GELU: определение
Заголовок раздела «GELU: определение»GELU (Gaussian Error Linear Unit, Hendrycks & Gimpel, 2016):
где:
- — вход (одна компонента);
- , — функция распределения стандартного нормального закона;
- — функция ошибок; связь .
Интуиция (разд. 2 статьи). ReLU умножает вход на 0 или 1 в зависимости от знака. GELU умножает его на «вероятность пропуска» : представим случайную маску — чем больше относительно других входов (которые после нормализации примерно ), тем вероятнее он пройдёт. Математическое ожидание — это и есть GELU. Получается гладкий аналог ReLU: при — , при — 0, в районе нуля — плавный переход и небольшой отрицательный «провал» (минимум при ).
Производная:
где — плотность стандартного нормального распределения. Значения: , , . В отличие от ReLU, градиент при не ровно ноль, и «мёртвых» нейронов нет.
GELU: tanh-аппроксимация
Заголовок раздела «GELU: tanh-аппроксимация»Функция не выражается через элементарные функции. Hendrycks и Gimpel предложили быстрое приближение через (разд. 2):
где . Именно её использовали OpenAI в коде GPT-1 и GPT-2, поэтому для совместимости с их весами нужна она, а не точная версия (см. бэклог, пункт 13).
Откуда берутся константы
Нужно приблизить функцией — обе нечётные, растут от до . Ищем приближение вида и подбираем и так, чтобы совпали ряды Тейлора в нуле.
Ряд для : . Подставим :
Ряд для : . Подставим ; с точностью до :
Приравниваем коэффициенты при : — отсюда первая константа.
Приравниваем коэффициенты при :
Ряд Тейлора даёт — точное совпадение около нуля. Константа из статьи немного меньше: она подобрана численно так, чтобы приближение было лучше на всей прямой, а не только около нуля. Сравнение максимальной абсолютной ошибки по сетке с шагом (float64):
| Максимальная ошибка | Где достигается | |
|---|---|---|
| (без кубического члена) | ||
| (ряд Тейлора) | ||
| (статья) |
Ошибка одинакова при : и точный GELU, и приближение удовлетворяют .
Точность. Максимальная ошибка tanh-аппроксимации — (при ), что меньше шага float16 около этих значений. Это посчитано так:
import math, torch
x = torch.linspace(-20, 20, 4_000_001, dtype=torch.float64)exact = 0.5 * x * (1 + torch.erf(x / math.sqrt(2)))approx = 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * x**3)))err = (approx - exact).abs()print(err.max().item(), x[err.argmax()].item()) # 0.000473... 2.6989...Модель, обученная с одной версией, работает и с другой, но выходы расходятся: для GPT-2 логиты с «не той» GELU отличались от эталона на (бэклог, пункт 13). В статье есть и ещё более грубое приближение (максимальная ошибка ) — оно родственно SiLU из следующего раздела.
Кто что использует.
| Модель | Вариант | Имя в HF | В репозитории |
|---|---|---|---|
| GPT-1 | tanh | afn="gelu" в openai-gpt, внутри modeling_openai — это gelu_new | activation="gelu_tanh" по умолчанию в GPT |
| GPT-2 | tanh | activation_function="gelu_new" | "gelu_tanh", зашито в Gpt2Decoder |
| Gemma (в GeGLU) | tanh | gelu_pytorch_tanh | GELU из core/gelu.py внутри GeGLU |
В PyTorch точный GELU — nn.GELU(), tanh-вариант — nn.GELU(approximate="tanh"); в репозитории tanh-вариант реализован отдельным классом GELU (см. ниже) и совпадает с approximate="tanh" до во float32.
SiLU / Swish
Заголовок раздела «SiLU / Swish»где:
- — логистическая сигмоида;
- — параметр (константа или обучаемый); SiLU — частный случай .
История. Функцию под именем SiLU (Sigmoid-weighted Linear Unit) предложили Elfwing et al., 2017 для обучения с подкреплением. Ramachandran et al., 2017 нашли её же автоматическим поиском по пространству активаций и назвали Swish. При Swish стремится к ReLU, при — к . В LLM используется , и имена SiLU и Swish употребляются как синонимы.
Идея та же, что у GELU, — вход, умноженный на «вероятность пропуска», только вместо — сигмоида. Функция немонотонна: минимум при .
Производная (правило произведения и ):
Значения: , , , — производная может быть больше 1.
Сравнение активаций
Заголовок раздела «Сравнение активаций»Вместо графиков — таблица значений (GELU и GELU-tanh различаются только в четвёртом знаке):
| ReLU | GELU (erf) | GELU (tanh) | SiLU | ||
|---|---|---|---|---|---|
| −3 | 0 | −0.0040 | −0.0036 | −0.1423 | 0.0474 |
| −2 | 0 | −0.0455 | −0.0454 | −0.2384 | 0.1192 |
| −1 | 0 | −0.1587 | −0.1588 | −0.2689 | 0.2689 |
| −0.5 | 0 | −0.1543 | −0.1543 | −0.1888 | 0.3775 |
| 0 | 0 | 0 | 0 | 0 | 0.5 |
| 0.5 | 0.5 | 0.3457 | 0.3457 | 0.3112 | 0.6225 |
| 1 | 1 | 0.8413 | 0.8412 | 0.7311 | 0.7311 |
| 2 | 2 | 1.9545 | 1.9546 | 1.7616 | 0.8808 |
| 3 | 3 | 2.9960 | 2.9964 | 2.8577 | 0.9526 |
Что видно из таблицы:
- все три гладкие функции при больших ведут себя как ReLU;
- SiLU «мягче» GELU: дольше отстаёт от на положительной полуоси и глубже уходит в минус на отрицательной;
- : столбец SiLU — произведение на последний столбец.
Gated Linear Units
Заголовок раздела «Gated Linear Units»Gated Linear Unit (Dauphin et al., 2017) — две параллельные линейные проекции, одна из которых через сигмоиду «открывает» или «закрывает» другую:
где:
- — вход;
- , — две независимые проекции в скрытое пространство размера (буквы — как в статье; здесь — матрица, а не размер словаря);
- — поэлементное умножение;
- — гейт (gate): для каждой компоненты решает, какая доля сигнала пройдёт.
У Dauphin et al. GLU применялся в свёрточной языковой модели. Аргумент авторов: градиент содержит линейный путь без производной нелинейности, и градиент через открытый гейт не затухает.
GLU-варианты для трансформера
Заголовок раздела «GLU-варианты для трансформера»Shazeer, 2020 заменил первый слой FFN на GLU с разными функциями в гейте и убрал bias:
где:
- — проекция гейта (gate; в HF —
gate_proj, у Meta —w1); - — линейная «полезная» проекция (up;
up_proj,w3); как и в GLU выше, это матрица, а не размер словаря; - — сжимающая проекция (down;
down_proj,w2); - .
В статье есть и другие варианты (ReGLU с ReLU, билинейный без функции). В экспериментах на T5 GEGLU и SwiGLU дали лучшую перплексию среди всех вариантов FFN при одинаковом числе параметров и вычислений. Теоретического объяснения автор не предлагает — выбор чисто эмпирический.
SwiGLU используют LLaMA, Mistral и Mixtral (в каждом эксперте), GeGLU (с tanh-GELU) — Gemma.
flowchart LR
X(["x · [B, T, d]"]):::io --> G["Linear W (gate)<br/>d → d_ff"]:::gray --> Act["SiLU или GELU"]:::purple --> Mul(("×")):::add
X --> U["Linear V (up)<br/>d → d_ff"]:::gray --> Mul
Mul --> D["Linear W2 (down)<br/>d_ff → d"]:::gray --> Drop["Dropout"]:::gray --> Out(["out · [B, T, d]"]):::io
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;
Интуиция гейта
Заголовок раздела «Интуиция гейта»В классическом FFN скрытый нейрон выдаёт : один вектор ключа решает и «сработал ли нейрон», и «насколько сильно». В gated-варианте это разделено: гейт решает, включён ли нейрон, а — какое значение (со знаком) он передаёт. Выход нейрона — произведение двух линейных функций входа (с мягким «выключателем»), т. е. квадратичная форма от : такой слой может выражать мультипликативные взаимодействия признаков, которые классическому FFN даются дороже.
Численный пример. Пусть и после проекций для одного токена гейт , полезная часть .
| Компонента 1 | Компонента 2 | Компонента 3 | |
|---|---|---|---|
| 2 | −1 | 0 | |
| 1.7616 | −0.2689 | 0 | |
| 1 | 3 | 5 | |
| SwiGLU: | 1.7616 | −0.8068 | 0 |
| 1.9546 | −0.1588 | 0 | |
| GeGLU: | 1.9546 | −0.4764 | 0 |
Третья компонента несёт самое большое значение , но гейт закрыт (), и она ничего не передаёт. Вторая пропущена с малым отрицательным коэффициентом. Результат (строка из трёх чисел) затем умножается на и возвращается в размерность .
Скрытый размер
Заголовок раздела «Скрытый размер»Равенство параметров: ⅔ · 4d
Заголовок раздела «Равенство параметров: ⅔ · 4d»У gated-FFN три матрицы вместо двух. Чтобы сравнение с классическим FFN было честным (одинаковые параметры и FLOP), скрытый размер уменьшают. Без bias:
где слева — и классического FFN с , справа — gated-FFN. Так делал уже Shazeer (скрытый размер 2048 вместо 3072 при ), и так делает LLaMA (Touvron et al., 2023, разд. 2.2).
Округление до multiple_of
Заголовок раздела «Округление до multiple_of» обычно не целое и неудобно для GPU: матричные умножения быстрее, когда размеры кратны большой степени двойки. Поэтому в коде Meta результат округляют вверх до кратного multiple_of (256 для LLaMA), с необязательным множителем ffn_dim_multiplier (LLaMA 2 70B). В репозитории это llama_intermediate_size в models/llama/llama.py:
def llama_intermediate_size(embed_dim: int, multiple_of: int = 256, ffn_dim_multiplier: float = None) -> int: hidden = int(2 * 4 * embed_dim / 3) # ⌊8d/3⌋ if ffn_dim_multiplier is not None: hidden = int(ffn_dim_multiplier * hidden) # необязательный множитель return multiple_of * ((hidden + multiple_of - 1) // multiple_of) # округление вверхФормула:
где — multiple_of, и — округление вверх и вниз. Выражение (hidden + m - 1) // m — стандартный приём целочисленного деления с округлением вверх.
Пример, LLaMA 7B ():
- ,
int→ . - , вверх → .
- .
Параметров FFN на слой: против у классического FFN — на 0.8 % больше из-за округления. С тем же было бы — в 1.5 раза больше (бэклог, пункт 23). Другие значения: (LLaMA 13B) → ; , ffn_dim_multiplier=1.3, multiple_of=4096 (LLaMA 2 70B) → .
from llm.models.llama import llama_intermediate_size
print(llama_intermediate_size(4096)) # 11008print(llama_intermediate_size(288, multiple_of=32)) # 768 (llama2.c stories15M)Mistral и Gemma: другие соотношения
Заголовок раздела «Mistral и Gemma: другие соотношения»Не все модели следуют правилу ⅔:
- Mistral 7B и эксперты Mixtral 8x7B: при (mistral.md). FFN в раза тяжелее классического FFN с при том же .
- Gemma: на каждую из матриц
gateиup— 16384 при (2B) и 24576 при (7B) (gemma.md). Это параметров — втрое больше классического FFN. В табл. 1 статьи Gemma указаны вдвое большие «feedforward hidden dims» (32768 и 49152) — это сумма gate и up (бэклог, пункт 44).
Модель → FFN → d_ff
Заголовок раздела «Модель → FFN → d_ff»| Модель | Тип FFN | Активация | Параметров FFN на слой | |||
|---|---|---|---|---|---|---|
| Transformer (Vaswani) | классический | ReLU | 512 | 2048 | 4 | (с bias) |
| GPT-1 | классический | GELU (tanh) | 768 | 3072 | 4 | (с bias) |
| GPT-2 small | классический | GELU (tanh) | 768 | 3072 | 4 | (с bias) |
| LLaMA 7B | SwiGLU | SiLU | 4096 | 11008 | ≈ 2.69 | |
| Mistral 7B | SwiGLU | SiLU | 4096 | 14336 | 3.5 | |
| Mixtral 8x7B | 8 экспертов SwiGLU, top-2 | SiLU | 4096 | 14336 (у каждого) | 3.5 | , активно |
| Gemma 2B | GeGLU | GELU (tanh) | 2048 | 16384 | 8 | |
| Gemma 7B | GeGLU | GELU (tanh) | 3072 | 24576 | 8 |
У LLaMA и последующих моделей bias в FFN нет. В репозитории задаётся ключом intermediate_size (по умолчанию ), bias — ключом bias (по умолчанию true); классический FeedForward GPT всегда с bias. Mixture-of-Experts разбирается в следующей главе — Mixture-of-Experts.
Dropout в FFN
Заголовок раздела «Dropout в FFN»Dropout (Srivastava et al., 2014) при обучении обнуляет каждую компоненту с вероятностью и масштабирует оставшиеся на ; при инференсе (model.eval()) ничего не делает. В трансформере Vaswani dropout применяется к выходу каждого подслоя перед сложением с residual (разд. 5.4). В GPT это resid_pdrop.
В репозитории все FFN-модули устроены одинаково: один dropout на выходе, после сжимающей проекции, перед residual-сложением. Внутри (на скрытых активациях) dropout нет.
FeedForward,SwiGLU,GeGLU:return self._dropout(out)в концеforward; вероятность — параметрdropoutконструктора (изconfig["dropout"]).MoE: эксперты-SwiGLUсоздаются сdropout=0.0, dropout — один, на выходе всего слоя, иначе выход прорежался бы дважды (core/moe.py).
Современные LLM (LLaMA, Mistral, Gemma) предобучаются на огромных корпусах почти за одну эпоху и dropout не используют: переобучение там не главная проблема. В репозитории он есть везде и отключается "dropout": 0 в конфиге — см. mistral.md, gemma.md.
Реализация в репозитории
Заголовок раздела «Реализация в репозитории»| Класс | Файл | Формула | Где используется |
|---|---|---|---|
FeedForward | core/feed_forward.py | , | GPT-1 (GptDecoder), GPT-2 (Gpt2Decoder) |
SwiGLU | core/swi_glu.py | LLaMA, Mistral, эксперты Mixtral | |
GeGLU | core/geglu.py | Gemma | |
GELU | core/gelu.py | tanh-аппроксимация | FeedForward(activation="gelu_tanh"), GeGLU |
SiLU | core/silu.py | SwiGLU |
FeedForward
Заголовок раздела «FeedForward»FeedForward(emb_size, dropout=0.1, activation="relu"):
self._layer1 = nn.Linear(emb_size, emb_size * 4) # W1, b1: d → 4dif activation == "relu": self._activation = nn.ReLU()elif activation == "gelu": self._activation = nn.GELU() # точный, через erfelif activation == "gelu_tanh": self._activation = GELU() # tanh-аппроксимация (HF: gelu_new)else: raise ValueError(f"Unknown activation: {activation}")self._layer2 = nn.Linear(emb_size * 4, emb_size) # W2, b2: 4d → dself._dropout = nn.Dropout(dropout)forward — ровно формула классического FFN: _layer1 → _activation → _layer2 → _dropout. Особенности:
- зашит как
emb_size * 4, параметра для него нет; bias есть всегда; - значение
activationпо умолчанию у самого класса —"relu", но декодеры передают другое:GptDecoder(activation="gelu_tanh")по умолчанию (модельGPTчитаетconfig.get("activation", "gelu_tanh")),Gpt2Decoderжёстко задаёт"gelu_tanh"; forwardне приводит веса к dtype входа: dtype модели меняют снаружи —model.to(torch.bfloat16)илиtorch.autocast.
SwiGLU и GeGLU
Заголовок раздела «SwiGLU и GeGLU»SwiGLU(emb_size, dropout=0.1, hidden_dim=None, bias=True); GeGLU — с той же сигнатурой.
hidden_dim = 4 * emb_size if hidden_dim is None else hidden_dim # d_ff; <= 0 → ValueErrorself._gate = nn.Linear(emb_size, hidden_dim, bias=bias) # W (gate)self._up = nn.Linear(emb_size, hidden_dim, bias=bias) # V (up)self._down = nn.Linear(hidden_dim, emb_size, bias=bias) # W2 (down)self._activation = SiLU() # в GeGLU — GELU()forward:
gate_out = self._gate(x) # xWactivation_out = self._activation(gate_out) # SiLU(xW)up_out = self._up(x) # xVout = up_out * activation_out # ⊗ — поэлементноout = self._down(out) # (…) W2return self._dropout(out)Активация применяется к ветви _gate, а не _up, — как у Shazeer и в HF (down_proj(act(gate_proj(x)) * up_proj(x))); поэтому при загрузке весов gate_proj → _gate, up_proj → _up. Отличия от статей по умолчанию: вместо / / и bias во всех трёх проекциях; оба включаются ключами intermediate_size и bias (см. llama.md).
Параметры для :
| Модуль | bias | Параметров | |
|---|---|---|---|
FeedForward(256) | 1024 | да | |
SwiGLU(256) | 1024 | да | () |
SwiGLU(256, hidden_dim=768, bias=False) | 768 = llama_intermediate_size(256) | нет |
GELU и SiLU
Заголовок раздела «GELU и SiLU»GELU в core/gelu.py — формула tanh-аппроксимации буквально:
self.sqrt_2_over_pi = torch.sqrt(torch.tensor(2.0) / math.pi)...return 0.5 * x * (1 + torch.tanh(self.sqrt_2_over_pi * (x + 0.044715 * torch.pow(x, 3))))Параметров нет; константа — обычный атрибут-тензор (0-мерный, поэтому при умножении действует как скаляр и не меняет dtype и устройство входа). Результат совпадает с nn.functional.gelu(x, approximate="tanh") до .
SiLU в core/silu.py — torch.sigmoid(x) * x, совпадает с nn.functional.silu до ошибок округления float32.
import torchfrom llm.core.gelu import GELUfrom llm.core.silu import SiLUfrom llm.core.swi_glu import SwiGLU
x = torch.tensor([-1.0, 0.0, 1.0, 2.0])print(GELU()(x)) # tensor([-0.1588, 0.0000, 0.8412, 1.9546])print(SiLU()(x)) # tensor([-0.2689, 0.0000, 0.7311, 1.7616])
ffn = SwiGLU(emb_size=256, dropout=0.0, hidden_dim=768, bias=False)print(ffn(torch.randn(2, 10, 256)).shape) # torch.Size([2, 10, 256])Типичные ошибки и тонкости
Заголовок раздела «Типичные ошибки и тонкости»- Точный GELU вместо tanh (или наоборот) при загрузке весов GPT-2 или Gemma: модель работает, но логиты расходятся с эталоном на . В HF имя
"gelu"означает разное в разных моделях — смотрите реализацию. - Перепутанные gate и up: — другая функция; веса HF в такой модуль загрузятся без ошибки, но результат будет неверным.
- Неверный : в SwiGLU делает FFN в 1.5 раза тяжелее, чем в LLaMA, и сравнение с GPT той же ширины нечестным. Для загрузки весов
intermediate_sizeдолжен совпадать с чекпоинтом — иначе не совпадут формы. - Округление вниз вместо вверх в
multiple_of: для получится вместо . - Dropout при инференсе: FFN-модули содержат
nn.Dropout; перед генерацией или сравнением логитов нуженmodel.eval()(илиdropout: 0в конфиге). - Gemma: 8d — на каждую матрицу, а не суммарно; число из таблицы статьи — сумма gate и up.
- FFN применяется к каждой позиции независимо; её можно читать как память «ключ–значение», где ключи — столбцы , значения — строки .
- Классический FFN: , , параметров, около ⅔ параметров блока.
- GELU ; tanh-аппроксимация с (совпадение ряда Тейлора) и (подбор) ошибается не более чем на ; её используют GPT-1, GPT-2 и Gemma.
- SiLU , производная .
- SwiGLU и GeGLU: — три матрицы, гейт отделяет «включён ли нейрон» от «что он передаёт».
- При равенстве параметров , в LLaMA округляется вверх до кратного 256 (11008 при ); Mistral — , Gemma — .
- В репозитории:
FeedForward(GPT),SwiGLU(LLaMA, Mistral, Mixtral),GeGLU(Gemma); размер — ключintermediate_size.
Вопросы и упражнения
Заголовок раздела «Вопросы и упражнения»- Посчитайте число параметров классического FFN (с bias, ) для и сравните с attention той же ширины ().
Ответ
FFN: . Attention: . FFN почти вдвое больше — около ⅔ параметров блока (без учёта нормализаций).
- Вычислите вручную точно () и через tanh-аппроксимацию. Какова разница?
Ответ
Точно: . Аппроксимация: аргумент , , . Разница .
- Выведите производную и найдите, при каком условии она равна нулю. Проверьте, что точка минимума ему удовлетворяет.
Ответ
. Так как , ноль — когда . При : , , .
- Найдите
llama_intermediate_size(512)иllama_intermediate_size(2048)(сmultiple_of=256). Сколько параметров у SwiGLU без bias с этим размером и у классического FFN без bias с ?
Ответ
: , , . SwiGLU: ; классический: (+12.5 % из-за округления).
: , , . SwiGLU: ; классический: (+3.1 %).
- Выведите, каким должен быть у gated-FFN, чтобы его параметры равнялись параметрам классического FFN шириной . Во сколько раз FFN Gemma () больше классического с ?
Ответ
⇒ . Gemma: против — в 3 раза; эквивалентная ширина классического FFN — .
- Для SwiGLU с гейтом и посчитайте скрытый вектор .
Ответ
, , . Произведение: .
- Почему FFN без активации бесполезна, даже если ? Каков максимальный ранг её матрицы?
Ответ
— линейное отображение с матрицей , ранг не больше (и не больше ). Такое отображение выражается одним слоем , расширение ничего не добавляет, а параметров в раз больше.
- (Обсуждение) В GLU гейт и значение считаются из одного и того же . Покажите, что выход одного нейрона SwiGLU при больших положительных — квадратичная функция , и объясните, чем это отличается от классического FFN.
Ответ
При , и нейрон выдаёт — квадратичная форма. Нейрон классического FFN при линеен: . Gated-нейрон напрямую моделирует произведения признаков, классическому FFN для этого нужно несколько нейронов.
Литература
Заголовок раздела «Литература»- Vaswani et al. Attention Is All You Need. 2017. arXiv:1706.03762
- Geva, Schuster, Berant, Levy. Transformer Feed-Forward Layers Are Key-Value Memories. 2021. arXiv:2012.14913
- Nair, Hinton. Rectified Linear Units Improve Restricted Boltzmann Machines. ICML, 2010 (на arXiv не публиковалась)
- Hendrycks, Gimpel. Gaussian Error Linear Units (GELUs). 2016. arXiv:1606.08415
- Elfwing, Uchibe, Doya. Sigmoid-Weighted Linear Units for Neural Network Function Approximation in Reinforcement Learning. 2017. arXiv:1702.03118
- Ramachandran, Zoph, Le. Searching for Activation Functions. 2017. arXiv:1710.05941
- Dauphin, Fan, Auli, Grangier. Language Modeling with Gated Convolutional Networks. 2017. arXiv:1612.08083
- Shazeer. GLU Variants Improve Transformer. 2020. arXiv:2002.05202
- Touvron et al. LLaMA: Open and Efficient Foundation Language Models. 2023. arXiv:2302.13971
- Srivastava, Hinton, Krizhevsky, Sutskever, Salakhutdinov. Dropout: A Simple Way to Prevent Neural Networks from Overfitting. JMLR, 2014. PDF