Перейти к содержимому

GPT-2

Реализация: llm/src/llm/models/gpt/gpt2.py · класс GPT2 Ноутбук: notebooks/gpt2.ipynb

Место в линейке: GPT-1 → GPT-2 → LLaMA → Mistral → Mixtral · Gemma

  • какую идею проверяла статья GPT-2: языковая модель как решатель задач без дообучения (zero-shot), корпус WebText, byte-level BPE;
  • какие три изменения архитектуры отличают GPT-2 от GPT-1: pre-LN, финальный LayerNorm, масштаб инициализации residual-слоёв;
  • как выглядит прямой проход GPT-2 в формулах и почему pre-LN обучается стабильнее;
  • как посчитать параметры GPT-2 (124 439 808 у самой маленькой модели) и откуда расхождение с «117M» из статьи;
  • как устроены классы GPT2 и Gpt2Decoder и как загрузить веса OpenAI.

Глава опирается на GPT-1: эмбеддинги, attention и FFN у GPT-2 те же, и их формулы здесь только напоминаются. Нужны также нормализация (post-LN и pre-LN), токенизация (BPE) и обучение (инициализация).

GPT-1 показал, что предобученную языковую модель выгодно дообучать на каждой задаче. Radford, Wu, Child, Luan, Amodei, Sutskever в статье Language Models are Unsupervised Multitask Learners (OpenAI, 2019) пошли дальше: можно ли обойтись без дообучения вообще?

Идея (разд. 2 статьи): модель, решающая задачу, оценивает p(output∣input)p(\text{output} \mid \text{input}); универсальная система должна оценивать p(output∣input,task)p(\text{output} \mid \text{input}, \text{task}). В тексте задача часто описана естественным языком прямо рядом с примером — «переведи на французский», пары «английская фраза = французская фраза». Поэтому достаточно большая языковая модель, обученная на достаточно разнообразном тексте, может научиться выполнять такие задачи, просто предсказывая следующий токен. Задача тогда задаётся промптом (prompt), а не новой головой и не градиентными шагами: например, для суммаризации в статье к статье дописывается TL;DR:.

Вклад статьи:

  1. Корпус WebText (разд. 2.1). Вместо книг (GPT-1) или неотфильтрованного Common Crawl авторы собрали исходящие ссылки с Reddit, получившие не меньше 3 karma, — как грубый фильтр «люди сочли это интересным». Около 45 млн ссылок; после дедупликации и очистки — чуть больше 8 млн документов, 40 ГБ текста. Все документы Википедии удалены, чтобы не пересекаться с тестовыми наборами.
  2. Byte-level BPE (разд. 2.2). BPE работает не над символами Unicode, а над байтами UTF-8: базовый словарь — 256 байтов, поэтому любую строку можно закодировать, и неизвестных токенов (<unk>) нет. Чтобы не появлялись токены вроде dog., dog!, dog?, слияния между разными категориями символов запрещены, кроме пробелов. Итоговый словарь — 50 257 токенов (подробно — в tokenization.md).
  3. Масштаб (разд. 2.3, табл. 2). Четыре модели одной архитектуры, от размера GPT-1 до в 10 с лишним раз большей; самую большую авторы и называют GPT-2.
  4. Zero-shot-результаты (разд. 3). По аннотации статьи, самая большая модель получила лучший известный результат на 7 из 8 проверенных наборов языкового моделирования без обучения на них, при этом всё ещё недообучена на WebText. На задачах вроде ответов на вопросы, перевода и суммаризации zero-shot-качество было ниже специализированных систем, но устойчиво росло с размером модели.

Размеры моделей из табл. 2 статьи и подсчёт параметров по формуле ниже (для конфигурации кода с tying):

Модель в статьеLLddПараметров по формуле
117M (эквивалент GPT-1)12768124 439 808
345M241024354 823 168
762M361280774 030 080
1542M (GPT-2)4816001 557 611 200

Цифры статьи и точный подсчёт расходятся (117M против 124,4M). Точный подсчёт для 124M совпадает с числом параметров HF-чекпоинта openai-community/gpt2 (backlog.md), а для 345M проверен созданием модели в репозитории; в статье расхождение не объясняется. Общими для всех четырёх моделей в статье указаны словарь 50 257, контекст 1024 токена (у GPT-1 было 512) и батч 512.

Статья перечисляет их в разд. 2.3; механизмы (эмбеддинги, MHA, GELU-FFN) те же, что в GPT-1, меняется их расстановка и инициализация.

  1. Pre-LN. LayerNorm перенесён на вход каждого подблока — по аналогии с pre-activation ResNet (He et al., 2016), где нормализация и активация тоже стоят перед весовым слоем, а не после сложения.
  2. Финальный LayerNorm после последнего блока, перед выходной проекцией.
  3. Масштаб инициализации residual-слоёв: веса слоёв, пишущих в residual-поток, при инициализации умножаются на 1/N1/\sqrt{N}, где NN — число residual-слоёв.
  4. Словарь 50 257 (byte-level BPE), контекст 1024 вместо 512.

Жирной обводкой выделено то, что изменилось по сравнению с GPT-1.

%%{init: {"flowchart": {"rankSpacing": 28, "nodeSpacing": 28}}}%%
flowchart TB
    Ids(["token ids"]):::io --> TokEmb["Token Embedding"]:::blue
    Ids --> PosEmb["Position Embedding<br/>(обучаемые)"]:::purple
    TokEmb --> Sum(("+")):::add
    PosEmb --> Sum
    Sum --> Drop["Dropout"]:::gray
    subgraph Dec["Gpt2Decoder × num_layers · pre-LN"]
        direction TB
        X(["x"]):::io --> N1["LayerNorm"]:::grayHl
        N1 --> Attn["Masked Multi-Head Attention"]:::blue
        Attn --> A1(("+")):::add
        X -. residual .-> A1
        A1 --> N2["LayerNorm"]:::grayHl
        N2 --> FFN["Feed Forward<br/>Linear → GELU → Linear"]:::purple
        FFN --> A2(("+")):::add
        A1 -. residual .-> A2
    end
    Drop --> Dec
    Dec --> NF["LayerNorm<br/>(финальный)"]:::grayHl --> Lin
    Lin["Linear → vocab_size"]:::gray --> Out(["logits"]):::io
    Out -. "generate(): softmax → выбор токена" .-> Next(["следующий токен"]):::io
    style Dec 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;

Обозначения — как в gpt.md: вход x∈{0,…,V−1}B×Tx \in \{0, \dots, V-1\}^{B \times T}, формулы записаны для одной последовательности, в коде спереди добавляется ось батча.

Шаг 1. Эмбеддинги — без изменений:

H(0)=Dropout(E[x0,…,xT−1]+P[0,…,T−1])H^{(0)} = \mathrm{Dropout}\big(E[x_0, \dots, x_{T-1}] + P[0, \dots, T-1]\big)

где E∈RV×dE \in \mathbb{R}^{V \times d} (wte в оригинале) — токенные эмбеддинги, P∈RTmax⁡×dP \in \mathbb{R}^{T_{\max} \times d} (wpe) — обучаемые позиционные эмбеддинги, H(0)∈RT×dH^{(0)} \in \mathbb{R}^{T \times d}.

Шаг 2. LL pre-LN блоков. Для l=1,…,Ll = 1, \dots, L:

U(l)=H(l−1)+MHA(LN1(H(l−1))),H(l)=U(l)+FFN(LN2(U(l))),\begin{aligned} U^{(l)} &= H^{(l-1)} + \mathrm{MHA}\big(\mathrm{LN}_1(H^{(l-1)})\big), \\ H^{(l)} &= U^{(l)} + \mathrm{FFN}\big(\mathrm{LN}_2(U^{(l)})\big), \end{aligned}

где:

  • H(l−1),U(l),H(l)∈RT×dH^{(l-1)}, U^{(l)}, H^{(l)} \in \mathbb{R}^{T \times d} — вход блока, состояние после attention-подблока и выход блока;
  • LN1,LN2\mathrm{LN}_1, \mathrm{LN}_2 — LayerNorm со своими γ,β∈Rd\gamma, \beta \in \mathbb{R}^{d} (normalization.md);
  • MHA\mathrm{MHA} — тот же masked multi-head attention, что в GPT-1 (формулы, attention.md);
  • FFN(x)=Dropout(GELU(xW1+b1)W2+b2)\mathrm{FFN}(x) = \mathrm{Dropout}\big(\mathrm{GELU}(x W_1 + b_1) W_2 + b_2\big), W1∈Rd×4dW_1 \in \mathbb{R}^{d \times 4d}, W2∈R4d×dW_2 \in \mathbb{R}^{4d \times d}, GELU в tanh-аппроксимации (feed-forward.md).

Шаг 3. Финальная нормализация и выходная проекция:

Z=LNf(H(L)) E⊤,p(xt+1∣x≤t)=softmax(Zt)Z = \mathrm{LN}_f\big(H^{(L)}\big)\, E^{\top}, \qquad p(x_{t+1} \mid x_{\le t}) = \mathrm{softmax}(Z_t)

где LNf\mathrm{LN}_f — финальный LayerNorm (ln_f в оригинале, _norm в коде), E⊤∈Rd×VE^{\top} \in \mathbb{R}^{d \times V} — транспонированная матрица эмбеддингов (weight tying, как в оригинале; без tying — отдельные Wout∈Rd×VW_{\text{out}} \in \mathbb{R}^{d \times V} и bout∈RVb_{\text{out}} \in \mathbb{R}^{V}), Z∈RT×VZ \in \mathbb{R}^{T \times V} — логиты.

Сводка форм — та же, что у GPT-1: [B, T] → [B, T, d] → (LL блоков) [B, T, d] → LN_f [B, T, d] → [B, T, V].

Раскроем рекурсию шага 2. Выход последнего блока — это вход плюс сумма вкладов всех 2L2L подблоков:

H(L)=H(0)+∑l=1L(MHA(LN1(H(l−1)))+FFN(LN2(U(l))))H^{(L)} = H^{(0)} + \sum_{l=1}^{L} \Big( \mathrm{MHA}\big(\mathrm{LN}_1(H^{(l-1)})\big) + \mathrm{FFN}\big(\mathrm{LN}_2(U^{(l)})\big) \Big)

Это равенство — прямое следствие двух формул шага 2: подставляем U(l)U^{(l)} в H(l)H^{(l)} и складываем по ll. В post-LN (GPT-1) такого разложения нет: каждый LayerNorm стоит на основном пути и перенормирует всю сумму.

Следствия:

  • Путь градиента. Производная H(L)H^{(L)} по H(0)H^{(0)} содержит единичное слагаемое (тождественный путь по residual-связям), поэтому градиент доходит до нижних слоёв, не проходя через нормализации. Xiong et al. (2020) показали, что у pre-LN градиенты в начале обучения хорошо ведут себя и без warmup, тогда как post-LN warmup необходим. Подробно — в normalization.md.
  • Финальный LayerNorm нужен. H(L)H^{(L)} — ненормализованная сумма 2L+12L + 1 слагаемых, её масштаб растёт с глубиной. LNf\mathrm{LN}_f приводит её к единому масштабу перед умножением на E⊤E^{\top}. В GPT-1 эту роль играл последний LN2\mathrm{LN}_2.

Из того же разложения видно, почему растёт масштаб residual-потока. Пусть в начале обучения вклады 2L2L подблоков в одну координату — независимые случайные величины со средним 0 и дисперсией σ2\sigma^2. Тогда дисперсия их суммы:

Var(∑k=12Lfk)=∑k=12LVar(fk)=2L σ2\mathrm{Var}\Big(\sum_{k=1}^{2L} f_k\Big) = \sum_{k=1}^{2L} \mathrm{Var}(f_k) = 2L\,\sigma^2

где fkf_k — вклад kk-го подблока в координату residual-потока, σ2\sigma^2 — дисперсия одного вклада, 2L2L — число residual-подблоков (attention и FFN в каждом из LL блоков). Первое равенство верно для независимых слагаемых.

Стандартное отклонение суммы растёт как 2L\sqrt{2L}: при L=12L = 12 — примерно в 4,9 раза. Вклад подблока линеен по весам его выходной проекции (WOW_O у attention, W2W_2 у FFN). Если уменьшить std этих весов в 2L\sqrt{2L} раз, дисперсия каждого вклада уменьшится в 2L2L раз, и сумма снова будет иметь дисперсию σ2\sigma^2 — независимо от глубины.

В коде N=2LN = 2L, как в GPT2PreTrainedModel._init_weights в HuggingFace:

WO, W2∼N ⁣(0, (0,022L)2)W_O,\ W_2 \sim \mathcal{N}\!\left(0,\ \left(\frac{0{,}02}{\sqrt{2L}}\right)^2\right)

Для L=12L = 12: 0,02/24≈0,004080{,}02 / \sqrt{24} \approx 0{,}00408; для учебного конфига (L=4L = 4): 0,02/8≈0,007070{,}02 / \sqrt{8} \approx 0{,}00707. Остальные Linear и Embedding — N(0, 0,022)\mathcal{N}(0,\ 0{,}02^2), bias — нули, LayerNorm — γ=1\gamma = 1, β=0\beta = 0.

КомпонентКлассФайл
Токен-эмбеддингиTokenEmbeddingscore/token_embeddings.py
Позиционные эмбеддингиPositionalEmbeddings (обучаемые, абсолютные — как в GPT-1)core/positional_embeddings.py
AttentionMultiHeadAttention (тот же класс, что и в GPT-1)core/multi_head_attention.py
FFNFeedForward с tanh-аппроксимацией GELU (activation="gelu_tanh" — как в оригинальном коде OpenAI; в HF — gelu_new), активация зашита внутри декодера и не настраивается из конфигаcore/feed_forward.py, core/gpt2_decoder.py
Блок декодераGpt2Decoder (pre-LN)core/gpt2_decoder.py
Финальная нормализацияnn.LayerNorm (GPT2._norm)models/gpt/gpt2.py
Модель целикомGPT2models/gpt/gpt2.py

Gpt2Decoder.forward:

norm1_out = Norm1(x)
attn_out = Attention(norm1_out)
out = attn_out + x
norm2_out = Norm2(out)
ffn_out = FFN(norm2_out)
result = ffn_out + out

В отличие от GPT-1, GPT2.forward добавляет финальный nn.LayerNorm после стека декодеров и перед проекцией на словарь (models/gpt/gpt2.py) — стандартная практика pre-LN трансформеров (без неё выход последнего блока не нормализован).

Gpt2Decoder — самостоятельный класс, а не переиспользование параметризуемого CachedDecoder (которым, например, пользуются LLaMA и другие более новые архитектуры в этом репозитории): FFN и pre-LN расстановка захардкожены внутри него.

По сравнению с GPT-1 (подсчёт) добавляется только финальный LayerNorm — 2d2d параметров (γ\gamma и β\beta). Блок по-прежнему содержит 12d2+13d12d^2 + 13d: attention 4d2+4d4d^2 + 4d, FFN 8d2+5d8d^2 + 5d, два LayerNorm 4d4d.

NGPT-2=Vd+Tmax⁡d+L (12d2+13d)+2d+{0,с weight tyingVd+V,без негоN_{\text{GPT-2}} = V d + T_{\max} d + L\,(12 d^2 + 13 d) + 2d + \begin{cases} 0, & \text{с weight tying} \\ V d + V, & \text{без него} \end{cases}

где VV — словарь, dd — размерность модели, Tmax⁡T_{\max} — контекст, LL — число блоков, 2d2d — финальный LayerNorm.

GPT-2 124M (V=50 257V = 50\,257, Tmax⁡=1024T_{\max} = 1024, d=768d = 768, L=12L = 12, с tying):

V·d = 50 257 · 768 = 38 597 376
T_max·d = 1 024 · 768 = 786 432
12 блоков = 12 · 7 087 872 = 85 054 464
ln_f = 2 · 768 = 1 536
итого 124 439 808

Совпадает с числом параметров HF-модели openai-community/gpt2 (backlog.md). Без tying: 124 439 808+38 597 376+50 257=163 087 441124\,439\,808 + 38\,597\,376 + 50\,257 = 163\,087\,441. Эмбеддинги здесь — 31,6 % модели, у GPT-1 — 27 %: словарь больше, а блоки те же. С ростом dd доля блоков растёт: у модели 1542M они дают около 95 % параметров.

Учебный конфиг gpt2_train.json (d=256d = 256, L=4L = 4, Tmax⁡=128T_{\max} = 128, при vocab_size = 1000): на 2d=5122d = 512 больше, чем у GPT-1, — 3 448 320 с tying и 3 705 320 без него (по умолчанию).

from llm.models.gpt import GPT2
cfg = {"vocab_size": 50257, "embed_dim": 768, "num_heads": 12, "num_layers": 12,
"max_position_embeddings": 1024, "dropout": 0.0, "tie_word_embeddings": True}
print(sum(p.numel() for p in GPT2(cfg).parameters())) # 124439808

Пример из experiments/llm_only/configs/gpt2_train.json — набор параметров тот же, что у GPT-1, кроме activation (у GPT-2 активация не настраивается):

ПараметрЗначение в примереЗначение в GPT-2 124MСмысл
vocab_size(из токенизатора)50257размер словаря VV
embed_dim256768размерность эмбеддингов dd
num_heads412число attention-голов HH
head_size(нет в примере)64необязательный размер головы; по умолчанию embed_dim // num_heads
num_layers412число блоков Gpt2Decoder LL
max_position_embeddings1281024максимальная длина последовательности Tmax⁡T_{\max}
dropout0.10.1dropout на эмбеддингах и на выходах attention и FFN перед residual
attention_dropout(нет в примере)0.1необязательный dropout на весах внимания после softmax, по умолчанию 0.0; в HF — attn_pdrop = 0.1
initializer_range(нет в примере)0.02необязательное стандартное отклонение начальных весов, по умолчанию 0.02
tie_word_embeddings(нет в примере)данеобязательный: true — выходная проекция без bias делит веса с wte, как в оригинале и HF (см. ниже); по умолчанию false — отдельный Linear с bias

Значения dropout для 124M — из конфига HF (resid_pdrop, embd_pdrop, attn_pdrop); в статье GPT-2 dropout не описан. Число голов в статье тоже не приводится; 12 — из конфига HF.

Как в GPT-1 (gpt.md), веса Linear и Embedding — N(0, 0,022)\mathcal{N}(0,\ 0{,}02^2), bias — нули. Дополнительно выходные проекции, которые пишут в residual-поток, — выход attention и второй слой FFN, по две на блок, — инициализируются со стандартным отклонением 0,02/2L0{,}02/\sqrt{2L}, где LL = num_layers. Статья GPT-2 (разд. 2.3) масштабирует веса residual-слоёв на 1/N1/\sqrt{N}, где NN — число residual-слоёв, чтобы дисперсия residual-потока не росла с глубиной; здесь N=2LN = 2L — как в GPT2PreTrainedModel._init_weights в HuggingFace (обоснование — в разделе Масштаб инициализации residual-слоёв). В коде OpenAI wpe инициализируется с 0.01, здесь, как в HF, — 0.02.

В коде это две строки GPT2.__init__: сначала self.apply(partial(init_normal_, std=std)) для всех модулей, затем scale_residual_projections_ (core/weight_init.py) переинициализирует decoder._heads._layer и decoder._ff._layer2 каждого блока со std =std/2L= \text{std} / \sqrt{2L}. Bias этих слоёв остаются нулевыми.

В оригинале (gpt-2/src/model.py: tf.matmul(h, wte, transpose_b=True)) и в HF (GPT2LMHeadModel) выходная проекция — та же матрица, что wte, без bias. Здесь это ключ "tie_word_embeddings": true, как в GPT-1 (gpt.md). Для конфигурации 124M он экономит около 38,6M параметров (50257 · 768 плюс bias): 124,4M вместо 163,1M.

С ним загружаются веса openai-community/gpt2:

from transformers import GPT2LMHeadModel
from llm.models.gpt import GPT2, convert_hf_state_dict
hf = GPT2LMHeadModel.from_pretrained("openai-community/gpt2")
model = GPT2({"vocab_size": 50257, "embed_dim": 768, "num_heads": 12, "num_layers": 12,
"max_position_embeddings": 1024, "dropout": 0.0, "tie_word_embeddings": True})
model.load_state_dict(convert_hf_state_dict(hf.state_dict()))

Логиты совпадают с HF с точностью до ~1e-4 (при значениях логитов порядка 100), greedy-генерация — токен в токен. Для текста нужен токенизатор GPT-2 из transformers (GPT2Tokenizer): собственный BPE репозитория (tokenization.md) даёт другие индексы.

convert_hf_state_dict (models/gpt/hf_weights.py) — та же функция, что для GPT-1; как она переименовывает ключи, транспонирует веса Conv1D и режет c_attn на Q, K, V, разобрано в gpt.md. Специфичны для GPT-2 только верхнеуровневые имена: wte → _token_embeddings._embedding, wpe → _position_embeddings.embedding, ln_f → _norm. Имена внутри блоков (h.{i}.attn.c_attn, h.{i}.mlp.c_fc, h.{i}.ln_1 …) у GPT-1 и GPT-2 в HF совпадают, различается только смысл ln_1/ln_2: в GPT-2 они стоят перед подблоками, а не после. Функции это безразлично — она переносит параметры, а расстановку задаёт класс блока.

models/gpt/gpt2.py, класс GPT2. Конструктор повторяет GPT.__init__ (разбор) с тремя отличиями:

Строка кодаЧто делаетОтличие от GPT-1
Gpt2Decoder(num_heads=..., emb_size=..., head_size=..., max_seq_len=..., dropout=..., attention_dropout=...)LL pre-LN блоковнет аргумента activation — GELU (tanh) зашит в блоке
self._norm = nn.LayerNorm(config["embed_dim"])LNf\mathrm{LN}_fновый модуль
scale_residual_projections_([...], num_layers=..., std=std)std /2L/\sqrt{2L} для WOW_O и W2W_2новая инициализация

Gpt2Decoder.__init__ (core/gpt2_decoder.py) создаёт те же четыре модуля, что GptDecoder: _heads, _ff (с activation="gelu_tanh"), _norm1, _norm2. Имена модулей совпадают, поэтому convert_hf_state_dict годится для обеих моделей, а scale_residual_projections_ находит проекции по decoder._heads._layer и decoder._ff._layer2.

Отличается от GPT.forward одной строкой перед проекцией:

for i, decoder in enumerate(self._decoders): # H^(l) = pre-LN блок
...
out = self._norm(out) # LN_f(H^(L)) [B, T, d]
logits = self._linear(out) # Z [B, T, V]

Gpt2Decoder.forward реализует шаг 2 буквально: out = attention + x — это U(l)U^{(l)} (attention применён к self._norm1(x)), result = ffn_out + out — это H(l)H^{(l)} (FFN применён к self._norm2(out)).

Две тонкости:

  • Сигнатура GPT2.forward(x, use_cache=False, cache=None, attention_mask=None) отличается порядком аргументов от GPT.forward(x, attention_mask=None, use_cache=False, cache=None). Передавайте их по имени.
  • У Gpt2Decoder.forward use_cache по умолчанию True (у GptDecoder — False). GPT2.forward всегда передаёт его явно, так что на модель это не влияет, но при прямом вызове блока вернётся кэш.
ЧтоСтатья / код OpenAI / HFЭтот репозиторий
Weight tyingестьключ tie_word_embeddings, по умолчанию выключен
Dropout вниманияattn_pdrop = 0.1 в HFattention_dropout, по умолчанию 0
Инициализация wpe0.01 в коде OpenAI0.02, как в HF
Токенизаторbyte-level BPE, 50 257 токеновсобственный BPE (tokenization.md); для весов OpenAI нужен токенизатор HF
Активация FFNGELU (tanh-аппроксимация)то же, не настраивается
Размер FFN4d4dвсегда 4d4d

GPT2.generate(...) — та же унифицированная сигнатура, что у всех моделей репозитория (кратко — gpt.md, подробно — generation.md). Промпт в духе статьи — то, как GPT-2 решает задачи без дообучения: задача описывается текстом, модель его продолжает.

import torch
from llm.models.gpt import GPT2
model = GPT2({"vocab_size": 1000, "embed_dim": 256, "num_heads": 4, "num_layers": 4,
"max_position_embeddings": 128, "dropout": 0.1})
model.eval()
out = model.generate(torch.randint(0, 1000, (1, 8)), max_new_tokens=20,
do_sample=True, temperature=0.8, top_k=40) # [1, 28]
  • обучаемые абсолютные позиционные эмбеддинги → RoPE (относительное, ротационное позиционное кодирование, встроено в attention);
  • LayerNorm → RMSNorm;
  • GELU-FFN → SwiGLU;
  • pre-LN и финальная нормализация сохраняются;
  • attention остаётся стандартным multi-head (см. llama.md) — GQA появится только в Mistral.

Подробности — в llama.md.

  • GPT-2 проверил идею: достаточно большая языковая модель на разнообразном тексте (WebText, 40 ГБ) решает задачи zero-shot, по промпту; лучший результат на 7 из 8 наборов языкового моделирования без обучения на них.
  • Byte-level BPE на 50 257 токенов кодирует любую строку без <unk>.
  • Архитектура та же, что у GPT-1, но: pre-LN, финальный LayerNorm, инициализация residual-проекций со std 0,02/2L0{,}02/\sqrt{2L}, контекст 1024.
  • Pre-LN превращает выход стека в сумму вкладов подблоков — градиент идёт по тождественному пути, глубокие стеки обучаются стабильнее.
  • Параметры: формула GPT-1 плюс 2d2d; для 124M — 124 439 808 (в статье — «117M»).
  • В репозитории — классы GPT2 и Gpt2Decoder; веса OpenAI загружаются той же convert_hf_state_dict.
  1. Раскройте формулу шага 2 для L=2L = 2 и запишите H(2)H^{(2)} как H(0)H^{(0)} плюс сумма четырёх слагаемых.
Ответ

H(2)=H(0)+MHA1(LN(H(0)))+FFN1(LN(U(1)))+MHA2(LN(H(1)))+FFN2(LN(U(2)))H^{(2)} = H^{(0)} + \mathrm{MHA}_1(\mathrm{LN}(H^{(0)})) + \mathrm{FFN}_1(\mathrm{LN}(U^{(1)})) + \mathrm{MHA}_2(\mathrm{LN}(H^{(1)})) + \mathrm{FFN}_2(\mathrm{LN}(U^{(2)})), где у каждого LN свои параметры.

  1. Каким std инициализируются _heads._layer.weight и _ff._layer2.weight у GPT-2 1542M (L=48L = 48)? Во сколько раз это меньше обычных 0,02?
Ответ

0,02/96≈0,002040{,}02 / \sqrt{96} \approx 0{,}00204, то есть в 96≈9,8\sqrt{96} \approx 9{,}8 раза меньше.

  1. Посчитайте параметры модели 345M по формуле и проверьте, какая доля приходится на эмбеддинги.
Ответ

Vd=50 257⋅1024=51 463 168V d = 50\,257 \cdot 1024 = 51\,463\,168; Tmax⁡d=1 048 576T_{\max} d = 1\,048\,576; блок 12⋅10242+13⋅1024=12 596 48012 \cdot 1024^2 + 13 \cdot 1024 = 12\,596\,480, 24 блока — 302 315 520302\,315\,520; LNf\mathrm{LN}_f — 2048. Итого 354 823 168354\,823\,168. Эмбеддинги: 52 511 74452\,511\,744, около 14,8 %.

  1. Почему при переходе от post-LN к pre-LN пришлось добавить финальный LayerNorm, и что будет, если его убрать у обученной модели?
Ответ

В pre-LN выход стека — ненормализованная сумма вкладов, её масштаб растёт с глубиной. Обученная модель рассчитывает, что в проекцию придёт нормализованный (и масштабированный γ,β\gamma, \beta из ln_f) вектор; без ln_f логиты получат другой масштаб и сдвиг, и предсказания испортятся.

  1. Почему byte-level BPE не нужен токен <unk>? Какой ценой?
Ответ

Любая строка — последовательность байтов UTF-8, а все 256 байтов есть в базовом словаре, поэтому любую строку можно закодировать. Цена — редкие символы (например, кириллица или иероглифы, мало представленные в корпусе) разбиваются на несколько байтовых токенов, и последовательности становятся длиннее.

  1. (Код.) Создайте GPT2 с учебным конфигом и проверьте, что std весов _decoders[0]._ff._layer2.weight близко к 0,02/80{,}02/\sqrt{8}, а std _decoders[0]._ff._layer1.weight — к 0,02.
Ответ
m = GPT2({"vocab_size": 1000, "embed_dim": 256, "num_heads": 4, "num_layers": 4,
"max_position_embeddings": 128, "dropout": 0.1})
print(m._decoders[0]._ff._layer2.weight.std()) # ≈ 0.0071
print(m._decoders[0]._ff._layer1.weight.std()) # ≈ 0.020
  1. В каком порядке надо передать аргументы, чтобы model(x, None, True) одинаково работал у GPT и GPT2? Почему лучше так не делать?
Ответ

Никак: у GPT второй позиционный аргумент — attention_mask, у GPT2 — use_cache. model(x, None, True) у GPT означает attention_mask=None, use_cache=True, а у GPT2 — use_cache=None, cache=True. Поэтому аргументы нужно передавать по имени: model(x, use_cache=True).

Основная статья:

  • Radford, Wu, Child, Luan, Amodei, Sutskever. Language Models are Unsupervised Multitask Learners. OpenAI, 2019. PDF (на arXiv не публиковалась)

Компоненты и связанные работы:

  • Radford, Narasimhan, Salimans, Sutskever. Improving Language Understanding by Generative Pre-Training. OpenAI, 2018. PDF (на arXiv не публиковалась)
  • Xiong et al. On Layer Normalization in the Transformer Architecture. 2020. arXiv:2002.04745 — почему pre-LN обучается стабильнее post-LN
  • He, Zhang, Ren, Sun. Identity Mappings in Deep Residual Networks. 2016. arXiv:1603.05027 — pre-activation ResNet, образец для pre-LN
  • Sennrich, Haddow, Birch. Neural Machine Translation of Rare Words with Subword Units. 2016. arXiv:1508.07909 — BPE-токенизация
  • Hendrycks, Gimpel. Gaussian Error Linear Units (GELUs). 2016. arXiv:1606.08415
  • Ba, Kiros, Hinton. Layer Normalization. 2016. arXiv:1607.06450