Перейти к содержимому
Ступень 44.5 Архитектура трансформера

Блок трансформера

Из каких четырёх деталей собран блок трансформера, почему нормализацию ставят перед подслоем, сколько в блоке параметров и как из одного блока получились BERT, T5 и GPT — и почему победил GPT.

УРОКstage-4/4-5-transformer/01-transformer-block.mdx1 ч 40 минУрок 1 из 4
После урока вы сможете
  • Нарисовать блок трансформера по памяти и объяснить роль каждой детали — внимания, FFN, остаточной связи и нормализации
  • Записать Pre-Norm и Post-Norm формулами и объяснить, почему почти все современные LLM используют Pre-Norm
  • Посчитать параметры блока и всей GPT-2 small (124 млн) руками и проверить счёт кодом
  • Отличить encoder-only, encoder-decoder и decoder-only по маске внимания и цели обучения и объяснить, почему победил decoder-only

В модуле 4.4 вы собрали многоголовое внимание. Но внимание — это ещё не модель. Любая современная LLM устроена как стопка одинаковых блоков: токены превращаются в эмбеддинги, проходят через NN блоков подряд, а в конце LM-головаLM-голова (языковая голова)EN LM head (language modeling head)Последний линейный слой GPT: превращает вектор ширины d в V логитов, по одному на токен словаря. Часто делит матрицу с эмбеддингами (weight tying). (последний линейный слой d→Vd \to V, как выходной слой классификатора из модуля 2.2) выдаёт логиты следующего токена. У GPT-2 small таких блоков 12, у Qwen3-8B — 36, у DeepSeek-V3 — 61. Блоки отличаются только весами, устройство у всех одно.

Значит, если понять один блок трансформераблок трансформераEN Transformer block (layer)Внимание + FFN, каждое с остаточной связью и нормализацией. Вход и выход — матрица (T, d), поэтому блоки складываются в стопку (12 у GPT-2 small, 36 у Qwen3-8B, 61 у DeepSeek-V3)., вы поймёте почти всю архитектуру трансформератрансформерEN TransformerАрхитектура из статьи «Attention Is All You Need» (2017) — стопка одинаковых блоков из внимания и FFN без рекуррентности. В китайских текстах название обычно не переводят.. В этом уроке соберём блок из четырёх деталей, разберёмся, почему порядок деталей поменялся с 2017 года, посчитаем параметры GPT-2 руками, а в конце выясним, как из одного блока выросли три семейства моделей — и почему ChatGPT, Qwen и DeepSeek принадлежат одному из них.

flowchart TB
T["Токены: «Карп прыгает через»"] --> E["Эмбеддинги + позиции<br/>(T, d)"]
E --> B1["Блок 1"] --> B2["Блок 2"] --> D["…"] --> BN["Блок N"]
BN --> N["Финальная нормализация"] --> H["LM-голова: d → V"] --> L["Логиты (T, V)"]
Языковая модель — это эмбеддинги, стопка одинаковых блоков и LM-голова. Форма (T, d) не меняется от блока к блоку.

Обратите внимание на формы: вход блока — матрица (T,d)(T, d), по строке на токен, и выход — тоже (T,d)(T, d). Именно поэтому блоки можно ставить друг за другом сколько угодно раз.

Две половины блока: общение и размышление

Заголовок раздела «Две половины блока: общение и размышление»

Внутри блока две части, и у них разные роли.

  1. Внимание — единственное место во всей модели, где токены обмениваются информацией. Токен «через» узнаёт, что перед ним «прыгает» и «карп».
  2. Полносвязный блок (полносвязный блокполносвязный блок (FFN)EN feed-forward network (FFN), feed-forward network) — каждый токен обрабатывается отдельно, одной и той же маленькой нейросетью. Он «обдумывает» то, что собрало внимание.

Андрей Карпатый в видео «Let’s build GPT» описывает это коротко: сначала общение (communication), потом вычисление (computation). Внимание смешивает строки матрицы (T,d)(T, d) между собой, FFN преобразует каждую строку по отдельности.

FFN: двухслойный перцептрон на каждый токен

Заголовок раздела «FFN: двухслойный перцептрон на каждый токен»
FFN(x)=φ(xW1+b1) W2+b2,W1∈Rd×4d, W2∈R4d×d\mathrm{FFN}(x) = \varphi(x W_1 + b_1)\, W_2 + b_2, \qquad W_1 \in \mathbb{R}^{d \times 4d},\ W_2 \in \mathbb{R}^{4d \times d}

Запись W1∈Rd×4dW_1 \in \mathbb{R}^{d \times 4d} читается «W1W_1 — матрица из dd строк и 4d4d столбцов с вещественными числами» (урок 1.0.3). Что здесь написано: вектор токена размера dd сначала расширяют в 4 раза, пропускают через нелинейность φ\varphi и сжимают обратно до dd. Например, при d=768d = 768 (GPT-2) вектор из 768 чисел становится вектором из 3072 чисел, а потом снова из 768. Это обычный двухслойный перцептрон из ступени 3, только применённый к каждой позиции. В статье 2017 года φ\varphi — ReLU, в GPT-2 и BERT — GELUGELUEN Gaussian error linear unit (GELU)GELU(z) = z·Φ(z), где Φ — функция распределения N(0, 1). Гладкая активация BERT и GPT-2 (в GPT-2 — tanh-приближение).:

xx−2-2−1-1001122
ReLU00012
GELU =x Φ(x)= x\,\Phi(x)−0.046-0.046−0.159-0.15900.8411.954

GELU — «сглаженный ReLU»: Φ(x)\Phi(x) — вероятность, что стандартная нормальная величина меньше xx. Положительные числа почти не трогает, отрицательные плавно гасит. В уроке 4.5.3 его сменит SwiGLU.

Сколько это стоит. Две матрицы по d⋅4dd \cdot 4d — это 8d28d^2 параметров. А внимание, как вы считали в уроке 4.4.3, стоит 4d24d^2. Значит, две трети параметров блока живут в FFN. Для d=768d = 768 это 4 722 432 параметра FFN против 2 362 368 у внимания (со смещениями).

Что там хранится. Гева и соавторы (2020) показали, что FFN работает как память «ключ–значение». Каждый из 4d4d скрытых нейронов — это пара векторов: столбец W1W_1 служит «ключом», который срабатывает на определённый шаблон во входе, а соответствующая строка W2W_2 — «значением», которое FFN дописывает к токену с весом «насколько сработал ключ». Условный пример: на шаблон «столица Франции —» срабатывает нейрон, который подталкивает модель к «Париж». Поэтому говорят, что внимание находит, а FFN знает. Эта идея приведёт нас к смеси экспертов (MoE) в модуле 5.3: если FFN — это память, её можно разбить на много «экспертов» и включать только нужных.

Остаточные связи: поток, в который все пишут

Заголовок раздела «Остаточные связи: поток, в который все пишут»

Каждая из двух частей блока не заменяет вход, а прибавляет к нему поправку:

x←x+F(x)x \leftarrow x + F(x)

Это остаточная связьостаточная связьEN residual connection из урока 3.4.2. Вспомним две причины, по которым без неё глубокую сеть не обучить:

  • Градиент. Производная x+F(x)x + F(x) по xx равна 1+F′(x)1 + F'(x). Единица — это «скоростная трасса»: градиент от loss доходит до первого слоя, даже если F′F' маленькая.
  • Старт с тождества. При инициализации поправки F(x)F(x) малы, и вся стопка из 36 блоков почти ничего не портит. Обучение начинается с разумной точки и постепенно учится полезным поправкам.

Удобно смотреть на это так. Если развернуть все блоки, выход модели — это вход плюс сумма всех поправок:

xL=x0+∑l=12NFl(… )x_L = x_0 + \sum_{l=1}^{2N} F_l(\dots)

Вектор xx, который течёт сквозь модель, называют остаточным потокомостаточный потокEN residual streamВектор шириной d, который течёт сквозь все блоки. Каждый подслой читает из него и дописывает поправку, так что выход = вход + сумма всех поправок. (residual stream). Это общая «шина» шириной dd: каждый подслой читает из неё (через нормализацию), что-то вычисляет и дописывает результат обратно. У GPT-2 small в шину пишут 24 «автора»: 12 вниманий и 12 FFN. Такой взгляд популяризировала команда интерпретируемости Anthropic в работе «A Mathematical Framework for Transformer Circuits» (2021), и он пригодится на ступени 9: DeepSeek и Kimi переделывают саму шину (mHC и Attention Residuals), а в уроке 9.3.1 мы через неё заглянем внутрь модели — logit lens и разложение ответа на вклады голов.

Четвёртая деталь — нормализация слоянормализация слояEN layer normalization (LayerNorm). Она берёт вектор одного токена, вычитает его среднее, делит на стандартное отклонение и умножает на обучаемые γ\gamma, β\beta:

LN(x)=γ⊙x−μσ2+ε+β\mathrm{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \varepsilon}} + \beta

Здесь μ\mu и σ2\sigma^2 — среднее и дисперсия чисел внутри вектора xx, ε\varepsilon — крошечное число (например, 10−510^{-5}), чтобы не делить на ноль, а ⊙\odot — поэлементное умножение (как * для массивов NumPy). Пример: x=(2,4,6,8)x = (2, 4, 6, 8), μ=5\mu = 5, σ≈2.24\sigma \approx 2.24, после нормализации (при γ=1\gamma = 1, β=0\beta = 0) — (−1.34,−0.45,0.45,1.34)(-1.34, -0.45, 0.45, 1.34). Зачем она нужна и чем отличается от BatchNorm, разобрано в уроке 3.4.1. Там же вы впервые встретили вопрос, к которому мы сейчас вернёмся уже на настоящем блоке трансформера: куда её поставить.

В «Attention Is All You Need» нормализация стоит после сложения:

xl+1=LN(xl+F(xl))x_{l+1} = \mathrm{LN}\big(x_l + F(x_l)\big)

Это Post-NormPost-Norm (нормализация после сложения)EN post-normalization (Post-LN)Norm(x + F(x)), как в исходном трансформере 2017 года и BERT. Без разогрева learning rate нестабилен на глубине.. Выглядит логично: после каждого шага поток приводится к стандартному масштабу. Но остаточная «трасса» больше не чистая: сигнал снизу проходит через 2N2N нормализаций, и каждая его перемасштабирует. Сюн и соавторы (2020) показали, что у Post-Norm при инициализации градиенты слоёв у выхода получаются большими, и с обычным learning rate обучение разваливается. Спасает только разогрев (warmupразогрев learning rate (warmup)EN learning rate warmupПервые сотни–тысячи шагов learning rate линейно растёт от нуля до пика, пока статистики Adam не накопились.): несколько тысяч шагов с маленьким шагом в начале. Чем глубже модель, тем капризнее Post-Norm.

В GPT-2 (2019) нормализацию перенесли внутрь ветки, на вход подслоя. Идея встречалась и раньше, в работах по машинному переводу, но именно после GPT-2 она стала стандартом языковых моделей:

xl+1=xl+F(LN(xl))x_{l+1} = x_l + F\big(\mathrm{LN}(x_l)\big)

Это Pre-NormPre-Norm (нормализация на входе подслоя)EN pre-normalization (Pre-LN)x + F(Norm(x)): нормализуется только вход ветки, остаточный путь чистый. Стандарт с GPT-2 (2019); требует финальной нормализации перед LM-головой.. Теперь подслой получает аккуратно нормированный вход, а сам поток xx никто не трогает: остаточный путь — чистая сумма x0+∑Flx_0 + \sum F_l, как в формуле выше. Поскольку поток к концу ничем не нормирован, после последнего блока добавляют финальную нормализацию перед LM-головой. Сюн и соавторы показали, что Pre-Norm обучается без разогрева и с большим learning rate.

Pre-Norm — стандарт, но не догма. Некоторые семейства моделей добавляют нормализацию ещё и на выход подслоя, перед сложением с потоком: так устроены Gemma 2 и Gemma 3 (нормализация до и после подслоя) и OLMo 2 (нормализация только на выходе подслоя). Цель та же — устойчивость обучения больших моделей; основа у всех — чистый остаточный поток.

В уроке 3.4.1 вы смотрели на градиенты последнего слоя. Теперь посмотрим на прямой проход. Возьмём стопку случайных (необученных) блоков трансформера и измерим, сколько исходного сигнала доходит до верха: косинус между входом и выходом стопки. И заодно — во сколько раз выросла норма потока. На свободном процессоре скрипт работает несколько секунд; если ждать приходится долго, уберите 64 из списка глубин.

import torch
import torch.nn as nn
import torch.nn.functional as F
class TinyBlock(nn.Module):
"""Блок с переключателем: pre=True — Pre-Norm, pre=False — Post-Norm (как в статье 2017 года)."""
def __init__(self, d, n_heads, pre):
super().__init__()
self.pre, self.h = pre, n_heads
self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d)
self.qkv, self.proj = nn.Linear(d, 3 * d), nn.Linear(d, d) # W_Q, W_K, W_V одной матрицей и W_O
self.ffn = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d)) # FFN: d → 4d → d
def attn(self, x):
"""Многоголовое причинное внимание из урока 4.4.3."""
B, T, D = x.shape
# qkv(x) имеет форму (B, T, 3D); split(D, -1) режет её на три куска (B, T, D) — это q, k, v;
# каждый кусок раскладываем на головы: (B, T, D) → (B, H, T, d_h)
q, k, v = (t.view(B, T, self.h, D // self.h).transpose(1, 2) for t in self.qkv(x).split(D, -1))
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return self.proj(y.transpose(1, 2).reshape(B, T, D))
def forward(self, x):
if self.pre: # Pre-Norm: x + f(LN(x))
x = x + self.attn(self.ln1(x))
return x + self.ffn(self.ln2(x))
x = self.ln1(x + self.attn(x)) # Post-Norm: LN(x + f(x))
return self.ln2(x + self.ffn(x))
def probe(pre, n_layers, d=64, seed=0):
"""Косинус «вход–выход» стека (сколько исходного сигнала дошло до верха) и рост нормы потока."""
torch.manual_seed(seed)
# nn.Sequential(*список) — стопка блоков, которые применяются по очереди
blocks = nn.Sequential(*[TinyBlock(d, 4, pre) for _ in range(n_layers)])
x0 = torch.randn(4, 16, d) # 4 текста по 16 «токенов»
with torch.no_grad(): # только прямой проход, градиенты не нужны
y = blocks(x0)
cos = F.cosine_similarity(x0, y, dim=-1).mean().item() # косинус вход↔выход для каждого токена, среднее
growth = (y.norm(dim=-1) / x0.norm(dim=-1)).mean().item() # во сколько раз выросла длина вектора
return cos, growth
for L in (2, 8, 32, 64):
(c_post, g_post), (c_pre, g_pre) = probe(False, L), probe(True, L)
print(f"слоёв {L:2d}: Post-Norm cos {c_post:.2f}, норма ×{g_post:.1f} Pre-Norm cos {c_pre:.2f}, норма ×{g_pre:.1f}")
слоёв 2: Post-Norm cos 0.92, норма ×1.0 Pre-Norm cos 0.93, норма ×1.1
слоёв 8: Post-Norm cos 0.72, норма ×1.0 Pre-Norm cos 0.77, норма ×1.3
слоёв 32: Post-Norm cos 0.24, норма ×1.0 Pre-Norm cos 0.51, норма ×2.0
слоёв 64: Post-Norm cos 0.04, норма ×1.0 Pre-Norm cos 0.36, норма ×2.8

Как это читать:

  • Post-Norm забывает вход. После 64 слоёв косинус 0.04: от исходного сигнала почти ничего не осталось, а норма всё время равна исходной — её держит LayerNorm. Прямой «трассы» от входа к выходу нет: сигнал проходит через 2N2N перемасштабирований подряд, и градиенту на обратном пути к нижним слоям приходится так же трудно.
  • Pre-Norm вход не теряет, а разбавляет. Заметьте: 0.36≈1/2.80.36 \approx 1 / 2.8. Так и должно быть, если x64=x0+(сумма поправок)x_{64} = x_0 + (\text{сумма поправок}), а поправки почти перпендикулярны x0x_0. Исходный сигнал лежит в потоке целиком, просто поток стал длиннее.

Отсюда и цена Pre-Norm. Норма потока растёт с глубиной, и поправка каждого следующего слоя на её фоне всё мельче. Глубокие слои влияют на результат слабее, чем могли бы. Поэтому исследователи периодически возвращаются к Post-Norm с хитрыми поправками (DeepNorm от Microsoft Research Asia позволил обучить трансформер в 1000 слоёв) или переделывают сам остаточный поток (mHC у DeepSeek, Attention Residuals у Kimi — ступень 9). Но базовый выбор индустрии с 2019 года — Pre-Norm: стабильное обучение ценнее небольшого выигрыша в качестве, который иногда даёт тонко настроенный Post-Norm.

Post-Norm (2017)Pre-Norm (GPT-2 и почти все LLM)
ФормулаLN(x+F(x))\mathrm{LN}(x + F(x))x+F(LN(x))x + F(\mathrm{LN}(x))
Остаточный путьискажён нормализациямичистая сумма
Разогрев LRобязателенжелателен, но не критичен
Глубокие моделинестабильны без трюковобучаются
Финальная норма перед головойне нужнанужна
Слабое местообучениерост нормы потока, «размытые» глубокие слои

Соберём всё вместе. Pre-Norm блок GPT-2:

h=x+MHA(LN1(x)),y=h+FFN(LN2(h))h = x + \mathrm{MHA}\big(\mathrm{LN}_1(x)\big), \qquad y = h + \mathrm{FFN}\big(\mathrm{LN}_2(h)\big)
ШагОперацияФорма
входостаточный поток(B,T,d)(B, T, d)
1LN1\mathrm{LN}_1 — по каждому токену отдельно(B,T,d)(B, T, d)
2многоголовое причинное внимание (урок 4.4.3)(B,T,d)(B, T, d)
3h=x+…h = x + \dots(B,T,d)(B, T, d)
4LN2\mathrm{LN}_2(B,T,d)(B, T, d)
5FFN: d→4d→dd \to 4d \to d(B,T,4d)→(B,T,d)(B, T, 4d) \to (B, T, d)
6y=h+…y = h + \dots(B,T,d)(B, T, d)

Единственная операция, которая смешивает позиции, — шаг 2. Всё остальное работает с каждым токеном отдельно.

Напишем блок на PyTorch и посчитаем параметры GPT-2 small: d=768d = 768, 12 голов, 12 блоков, словарь 50 257 токенов, контекст 1024.

import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
class CausalSelfAttention(nn.Module):
"""Многоголовое причинное внимание из урока 4.4 (со смещениями, как в GPT-2)."""
def __init__(self, d_model, n_heads):
super().__init__()
self.h = n_heads
self.qkv = nn.Linear(d_model, 3 * d_model) # W_Q, W_K, W_V одной матрицей
self.proj = nn.Linear(d_model, d_model) # выходная проекция W_O
def forward(self, x):
B, T, D = x.shape
q, k, v = self.qkv(x).split(D, dim=-1) # (B, T, 3D) → три тензора (B, T, D)
q, k, v = (t.view(B, T, self.h, D // self.h).transpose(1, 2) for t in (q, k, v)) # → (B, H, T, d_h)
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return self.proj(y.transpose(1, 2).reshape(B, T, D))
class FFN(nn.Module):
"""Полносвязный блок: расширяем в 4 раза, GELU, сжимаем обратно. Работает с каждым токеном отдельно."""
def __init__(self, d_model):
super().__init__()
self.up = nn.Linear(d_model, 4 * d_model)
self.down = nn.Linear(4 * d_model, d_model)
def forward(self, x):
return self.down(F.gelu(self.up(x)))
class Block(nn.Module):
"""Pre-Norm блок трансформера (GPT-2)."""
def __init__(self, d_model, n_heads):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = FFN(d_model)
def forward(self, x):
x = x + self.attn(self.ln1(x)) # токены обмениваются информацией
x = x + self.ffn(self.ln2(x)) # каждый токен «обдумывает» полученное
return x
block = Block(d_model=768, n_heads=12)
count = lambda m: sum(p.numel() for p in m.parameters()) # numel() — число элементов тензора; суммируем по всем весам
print(f"внимание: {count(block.attn):>9,}")
print(f"FFN: {count(block.ffn):>9,}")
print(f"нормы: {count(block.ln1) + count(block.ln2):>9,}")
print(f"блок: {count(block):>9,}")
x = torch.randn(2, 10, 768) # батч 2, 10 токенов
print("вход:", tuple(x.shape), "-> выход:", tuple(block(x).shape))
# Причинность: меняем последний токен — ранние позиции не меняются
x2 = x.clone()
x2[:, -1] += 10 # сильно меняем только последний токен каждого текста
print("ранние позиции не изменились:", torch.allclose(block(x)[:, :-1], block(x2)[:, :-1], atol=1e-5))
# Вся GPT-2 small: 12 блоков + эмбеддинги токенов и позиций + финальная норма
total = 12 * count(block) + 50257 * 768 + 1024 * 768 + 2 * 768
print(f"GPT-2 small: {total:,} параметров")
внимание: 2,362,368
FFN: 4,722,432
нормы: 3,072
блок: 7,087,872
вход: (2, 10, 768) -> выход: (2, 10, 768)
ранние позиции не изменились: True
GPT-2 small: 124,439,808 параметров

Ровно 124 439 808 — столько параметров у официального чекпоинта GPT-2 small (LM-голова делит веса с матрицей эмбеддингов, поэтому отдельно её не считаем). Из них 85 млн — в блоках и 39 млн — в эмбеддингах. Формула на будущее: блок ≈12d2\approx 12d^2, модель ≈12Nd2\approx 12 N d^2 плюс эмбеддинги.

Исходный трансформер 2017 года делал машинный перевод и состоял из двух стопок блоков. Энкодер читал исходное предложение с двунаправленным вниманием: каждый токен видит всё предложение. Декодер писал перевод с причинной маской и вдобавок смотрел на энкодер через перекрёстное вниманиеперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности. (урок 4.4.1). Уже через год стопки разошлись:

flowchart LR
T["Transformer 2017<br/>энкодер + декодер, перевод"] --> E["Encoder-only<br/>BERT, 2018"]
T --> ED["Encoder-decoder<br/>T5, 2019"]
T --> D["Decoder-only<br/>GPT, 2018 → GPT-3, 2020"]
D --> L["Llama, Qwen, DeepSeek,<br/>Kimi, GLM-4.5, MiniMax"]
E --> R["эмбеддинги и реранкеры<br/>для поиска (BGE)"]
Как исходный трансформер разделился на три семейства
  • Encoder-onlyencoder-only (только энкодер)EN encoder-only architectureДвунаправленное внимание, обучение через маскированное языковое моделирование (BERT, RoBERTa). Сегодня — модели эмбеддингов, реранкеры, классификаторы. — BERT (Google, 2018). Только энкодер, маска двунаправленная. Цель обучения — маскированное языковое моделированиемаскированное языковое моделирование (MLM)EN masked language modeling (MLM)Цель обучения BERT — закрыть около 15% токенов и угадать их по контексту с обеих сторон. Сигнал дают только замаскированные позиции. (MLM): закрыть 15% токенов и угадать их по контексту с обеих сторон. Модель отлично понимает текст (классификация, поиск именованных сущностей), но генерировать не умеет: она никогда не училась писать слева направо.
  • Encoder-decoderэнкодер–декодерEN encoder-decoder architectureДвунаправленный энкодер читает вход, причинный декодер генерирует выход и смотрит на энкодер через cross-attention (исходный Transformer, T5, Whisper). — T5 (Google, 2019). Обе стопки, любая задача записывается как «текст на входе → текст на выходе». Цель — восстанавливать выкинутые куски текста. Естественный выбор для перевода и суммаризации; по этой схеме устроен и Whisper для распознавания речи (урок 9.1.5).
  • Decoder-onlydecoder-only (только декодер)EN decoder-only architectureОдна стопка блоков с причинной маской, цель — следующий токен. Архитектура GPT, Llama, Qwen, DeepSeek, Kimi, GLM-4.5. — GPT (OpenAI, 2018). Только декодер без cross-attention, маска причинная, цель — предсказание следующего токенапредсказание следующего токенаEN next-token predictionГлавная задача языковой модели — по началу текста оценить вероятности следующего токена. Ответ чат-бота собирается из таких шагов по одному токену.. Сюда относятся практически все современные LLM.

Есть и гибрид — префиксная языковая модельпрефиксная языковая модель (Prefix LM)EN prefix language model (Prefix LM)Внутри префикса (например, запроса) внимание двунаправленное, дальше — причинное. UniLM, GLM, ChatGLM-6B; начиная с ChatGLM2 Zhipu перешла на причинную маску. (Prefix LM): одна стопка, но внутри «префикса» (например, вопроса пользователя) внимание двунаправленное, а дальше причинное. Так устроены UniLM от Microsoft и GLM из Университета Цинхуа (см. «Китайский угол»). Разница между всеми вариантами — только в маске внимания:

import numpy as np
T, P = 5, 2 # 5 токенов, из них первые 2 — «префикс» (запрос пользователя)
bidirectional = np.ones((T, T), dtype=int) # BERT, энкодер T5: все видят всех
causal = np.tril(np.ones((T, T), dtype=int)) # GPT, Llama, Qwen, DeepSeek: только прошлое и себя
prefix = causal.copy()
prefix[:, :P] = 1 # Prefix LM (GLM, ChatGLM-6B): префикс виден целиком
for name, m in [("двунаправленная", bidirectional), ("причинная", causal), ("префиксная", prefix)]:
print(name)
print(m)
# Сколько позиций дают обучающий сигнал на тексте из 1000 токенов
T_text = 1000
print("decoder-only:", T_text - 1, "предсказаний; BERT (15% масок):", int(0.15 * T_text))
двунаправленная
[[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]]
причинная
[[1 0 0 0 0]
[1 1 0 0 0]
[1 1 1 0 0]
[1 1 1 1 0]
[1 1 1 1 1]]
префиксная
[[1 1 0 0 0]
[1 1 0 0 0]
[1 1 1 0 0]
[1 1 1 1 0]
[1 1 1 1 1]]
decoder-only: 999 предсказаний; BERT (15% масок): 150

Строка ii показывает, на какие позиции смотрит токен ii (1 — можно). В префиксной маске два первых токена видят друг друга, а всё, что после префикса, видит только прошлое.

Encoder-onlyEncoder-decoderDecoder-only
ПримерBERT, RoBERTa, BGET5, исходный Transformer, WhisperGPT, Llama, Qwen, DeepSeek
Маскадвунаправленнаядвунаправленная + причиннаяпричинная
Цель обученияугадать 15% замаскированныхвосстановить пропускиследующий токен
Генерациянетдада
Где сегодняэмбеддинги, реранкеры, классификаторыперевод, речьвсе чат-модели и агенты

В 2019 году ставка на GPT не была очевидной: BERT лидировал почти во всех бенчмаркахбенчмаркEN benchmark понимания текста. Аргументов за decoder-only накопилось несколько.

  1. Сигнал с каждой позиции. Из текста в 1000 токенов decoder-only получает 999 обучающих примеров, BERT — около 150. При одинаковых данных первый учится на всём тексте сразу (вывод из кода выше).
  2. Один интерфейс для всех задач. Перевод, ответ на вопрос, код — всё это «продолжи текст». GPT-3 (2020) показала, что большой модели хватает нескольких примеров прямо в запросе (обучение в контекстеобучение в контекстеEN in-context learning (ICL)Модель решает новую задачу по примерам в промпте, без изменения весов. Ярко показано в статье о GPT-3 (2020).), без дообучения и отдельных «голов» под задачи.
  3. Простота и масштаб. Одна однородная стопка, одна маска, одна функция потерь. Законы масштабирования (модуль 5.1) изучали в основном на decoder-only, и под эту схему выросла вся инфраструктура: GPU-ядра, параллелизм, сервинг (обслуживание запросов к модели, ступень 7).
  4. Дешёвая генерация и диалог. С причинной маской состояния старых токенов не меняются, когда приходят новые. Ключи и значения прошлого можно посчитать один раз и хранить — это KV-кэшKV-кэшEN KV cache (урок 4.6.4). В диалоге кэш прошлых реплик переиспользуется. С двунаправленной маской каждая новая реплика меняет состояния всех предыдущих токенов, и кэш приходится пересчитывать. Проверим на одном слое:
import numpy as np
rng = np.random.default_rng(0)
d = 8
W_q, W_k, W_v = (rng.normal(size=(d, d)) / np.sqrt(d) for _ in range(3))
def layer(X, causal):
"""Один слой self-attention (без FFN — для наглядности хватит)."""
T = len(X)
S = (X @ W_q) @ (X @ W_k).T / np.sqrt(d)
if causal:
S = np.where(np.tril(np.ones((T, T), dtype=bool)), S, -np.inf)
A = np.exp(S - S.max(axis=1, keepdims=True))
return X + (A / A.sum(axis=1, keepdims=True)) @ (X @ W_v) # с остаточной связью
dialog = rng.normal(size=(6, d)) # 6 токенов уже обработаны
new = rng.normal(size=(1, d)) # пришёл 7-й токен
for causal in (True, False):
old = layer(dialog, causal) # состояния 6 токенов до прихода нового
upd = layer(np.vstack([dialog, new]), causal)[:6] # те же 6 токенов, когда в тексте уже 7
print("причинная" if causal else "двунаправленная",
"— старые состояния не изменились:", np.allclose(old, upd))
причинная — старые состояния не изменились: True
двунаправленная — старые состояния не изменились: False

Почему это важно именно для кэша: выход слоя — это вход следующего, и из него следующий слой считает свои ключи и значения. Если выходы старых токенов поменялись (двунаправленная маска), поменяются и их KK, VV во всех слоях выше — сохранённый кэш устарел. С причинной маской старые выходы те же, значит, и кэш верен.

  1. Теоретический довод Су Цзяньлиня. В статье 2023 года автор RoPE замечает: матрица причинного внимания треугольная с положительной диагональю, а значит, всегда полного ранга. У двунаправленного внимания такой гарантии нет, и оно может «терять ранг», то есть выразительность. В его эксперименте на моделях около 1 млрд параметров двунаправленный префикс (UniLM) не дал выигрыша над обычной GPT. Полный ранг треугольной матрицы — строгий факт, а вот связь «ранг → качество модели» — гипотеза, хотя с практикой она согласуется.

Честная оговорка: decoder-only выигрывает не везде. Ван и соавторы (2022, arXiv:2204.05832) сравнили архитектуры при равном бюджете: сразу после претрейна лучше всех в zero-shot (задача в запросе без единого примера решения) была причинная decoder-only модель, а после многозадачного дообучения — модели с двунаправленным вниманием на входе (encoder-decoder и префиксные), обученные восстанавливать замаскированные куски. Индустрия выбрала первый вариант плюс масштаб. А энкодеры никуда не делись: модели эмбеддингов и реранкерыреранкерEN rerankerВторая ступень поиска — точная, но медленная модель (обычно кросс-энкодер) пересортировывает десятки кандидатов от быстрого ретривера. (модели, которые заново упорядочивают найденные документы по полезности) для поиска (например, BGE от Пекинской академии ИИ, BAAI) — это BERT-подобные энкодеры. Они понадобятся в RAG на ступени 8.

Проверь себя6 вопросов
1 Какая формула описывает Pre-Norm подслой?
2 Сколько параметров в FFN без смещений при d=1024d = 1024 и скрытом слое 4d4d?
3 Какие утверждения верны?Можно выбрать несколько ответов
4 Почему в диалоге с decoder-only моделью можно переиспользовать KV-кэш прошлых реплик?
5 На тексте из 2000 токенов BERT маскирует 15%. Во сколько раз больше обучающих предсказаний получит decoder-only модель (считайте T−1T - 1)? Округлите до одного знака.
6 Как называется вектор, который течёт сквозь всю модель и в который каждый подслой дописывает свою поправку? (англ. или рус.)
  • Блок трансформера = внимание (токены общаются) + FFN (каждый токен думает сам) + остаточные связи + нормализация. Форма (T,d)(T, d) на входе и выходе одинакова, поэтому блоки складываются в стопку.
  • FFN расширяет вектор в 4 раза и сжимает обратно; в нём две трети параметров блока и, судя по исследованиям, значительная часть «знаний» модели. Блок стоит ≈12d2\approx 12d^2 параметров, GPT-2 small — 124 439 808.
  • Pre-Norm x+F(LN(x))x + F(\mathrm{LN}(x)) сохраняет чистый остаточный путь и обучается стабильно; Post-Norm из 2017 года требует разогрева и ломается на глубине. Цена Pre-Norm — растущая норма потока.
  • Encoder-only (BERT), encoder-decoder (T5) и decoder-only (GPT) различаются маской и целью. Decoder-only победил: сигнал с каждой позиции, один интерфейс для всех задач, простота масштабирования и KV-кэш.

Дальше: в блоке нет ни слова о порядке токенов — внимание видит текст как мешок слов. Как дать модели позиции и почему весь мир перешёл на китайское изобретение RoPE — Позиционное кодирование и RoPE.