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

Позиционное кодирование

Реализация: core/positional_embeddings.py · класс PositionalEmbeddings (GPT-1, GPT-2); core/rope.py · класс RoPE (LLaMA, Mistral, Mixtral, Gemma)

  • Почему механизм внимания сам по себе не знает порядка токенов и как это строго доказать.
  • Как устроены обучаемые абсолютные позиционные эмбеддинги GPT-1 и GPT-2 и чем ограничена их длина.
  • Как синусоидальное кодирование из «Attention Is All You Need» превращает сдвиг позиции в поворот — и почему это прямой путь к RoPE.
  • Полный вывод RoPE: матрицы поворота, частоты θi\theta_i, доказательство того, что оценка внимания зависит только от расстояния между токенами, эффективная реализация и перенос весов из HuggingFace.
  • Как база rope_theta связана с длиной контекста и как RoPE работает вместе с KV-кэшем.
  • Авторегрессия и общая схема decoder-only трансформера — language-modeling.md.
  • Эмбеддинги токенов и матрица EE — embeddings.md.
  • Формулу attention мы кратко напомним ниже; подробный разбор — в следующей главе, attention.md.
  • Из математики: скалярное произведение, умножение матриц, транспонирование, sin⁡\sin и cos⁡\cos суммы углов. Для одного необязательного вывода — комплексные числа в форме eiφ=cos⁡φ+isin⁡φe^{i\varphi} = \cos\varphi + i\sin\varphi.

Обозначения — как во всём пособии (notation.md): TT — длина последовательности, dd — размерность модели, dhd_h — размер головы, Tmax⁡T_{\max} — максимальная длина контекста (max_position_embeddings), позиции нумеруются с 0.

Напомним формулу одной головы внимания без маски (подробно — в attention.md):

Attn⁡(X)=softmax⁡ ⁣(QK⊤dh)V,Q=XWq,K=XWk,V=XWv\operatorname{Attn}(X) = \operatorname{softmax}\!\left(\frac{Q K^\top}{\sqrt{d_h}}\right) V, \qquad Q = X W_q,\quad K = X W_k,\quad V = X W_v

где:

  • X∈RT×dX \in \mathbb{R}^{T \times d} — матрица входных состояний, строка tt — вектор токена на позиции tt;
  • Wq,Wk,Wv∈Rd×dhW_q, W_k, W_v \in \mathbb{R}^{d \times d_h} — матрицы проекций;
  • Q,K,V∈RT×dhQ, K, V \in \mathbb{R}^{T \times d_h} — запросы (queries), ключи (keys) и значения (values);
  • softmax⁡\operatorname{softmax} применяется к каждой строке матрицы T×TT \times T отдельно;
  • dh\sqrt{d_h} — нормирующий множитель.

В этой формуле нет ни одного места, где использовался бы номер строки. Вес, с которым токен ii смотрит на токен jj, зависит только от содержимого векторов xix_i и xjx_j. Отсюда следует свойство, которое называется переставочной эквивариантностью (permutation equivariance): если переставить токены на входе, выход переставится точно так же и больше никак не изменится.

Формально: пусть P∈{0,1}T×TP \in \{0,1\}^{T \times T} — матрица перестановки (в каждой строке и каждом столбце ровно одна единица; в следующем разделе буквой PP будет обозначена другая матрица — позиционных эмбеддингов). Умножение PXPX переставляет строки XX. Утверждение:

Attn⁡(PX)=P Attn⁡(X)\operatorname{Attn}(PX) = P \, \operatorname{Attn}(X)
Доказательство

Шаг 1. Проекции переставляются вместе со входом, потому что умножение справа на WW действует на каждую строку отдельно:

Q′=(PX)Wq=P(XWq)=PQ,K′=PK,V′=PVQ' = (PX) W_q = P (X W_q) = P Q,\qquad K' = P K,\qquad V' = P V

Шаг 2. Матрица оценок:

S′=Q′K′⊤dh=PQ(PK)⊤dh=PQK⊤P⊤dh=PSP⊤S' = \frac{Q' K'^\top}{\sqrt{d_h}} = \frac{P Q (P K)^\top}{\sqrt{d_h}} = \frac{P Q K^\top P^\top}{\sqrt{d_h}} = P S P^\top

Умножение слева на PP переставляет строки SS, справа на P⊤P^\top — столбцы, той же перестановкой.

Шаг 3. Softmax по строкам коммутирует с такой двойной перестановкой: перестановка строк просто меняет порядок, в котором строки обрабатываются, а перестановка столбцов внутри строки переставляет слагаемые знаменателя ∑jesij\sum_j e^{s_{ij}} (сумма от этого не меняется) и числители. Поэтому

softmax⁡(PSP⊤)=P softmax⁡(S) P⊤\operatorname{softmax}(P S P^\top) = P \, \operatorname{softmax}(S) \, P^\top

Шаг 4. Для матрицы перестановки P⊤P=IP^\top P = I (это ортогональная матрица). Собираем:

Attn⁡(PX)=P softmax⁡(S) P⊤⋅PV=P softmax⁡(S) (P⊤P) V=P softmax⁡(S) V=P Attn⁡(X)\operatorname{Attn}(PX) = P\,\operatorname{softmax}(S)\,P^\top \cdot P V = P\,\operatorname{softmax}(S)\,(P^\top P)\, V = P\,\operatorname{softmax}(S)\, V = P\,\operatorname{Attn}(X)

Шаг 5. Остальные части блока трансформера — FFN, LayerNorm/RMSNorm, residual-сложение — применяются к каждой строке независимо, а значит, тоже эквивариантны. Композиция эквивариантных отображений эквивариантна, поэтому эквивариантен и весь стек слоёв.

Что это значит на практике. Фразы «кот ест рыбу» и «рыбу ест кот» состоят из одних и тех же токенов. Без позиционной информации вектор слова «кот» на выходе будет одинаковым в обеих фразах: модель видит мешок токенов, а не последовательность. Для языка это неприемлемо: смысл определяется порядком. Нужно как-то сообщить модели, где стоит каждый токен.

Проверить эквивариантность можно численно:

import torch
torch.manual_seed(0)
T, d, d_h = 6, 16, 8
X = torch.randn(T, d)
W_q, W_k, W_v = torch.randn(3, d, d_h)
def attn(X):
S = (X @ W_q) @ (X @ W_k).T / d_h ** 0.5
return torch.softmax(S, dim=-1) @ (X @ W_v)
perm = torch.randperm(T)
print(torch.allclose(attn(X[perm]), attn(X)[perm], atol=1e-5)) # True

В decoder-only моделях к оценкам добавляется causal-маска (masks.md): токен ii видит только j≤ij \le i. Маска привязана к номерам строк, поэтому она сама нарушает эквивариантность: в коде выше с маской allclose даёт False. Косвенно модель может извлечь из маски позицию — например, первый токен смотрит только на себя, а сотый усредняет сто векторов. Haviv et al. (2022) показали, что языковые модели с causal-маской и вовсе без позиционного кодирования учатся почти так же хорошо. Но это неявная и грубая информация; все модели этого пособия кодируют позицию явно.

Позицию можно сообщить модели двумя способами:

  • абсолютная позиция (absolute position) — каждому токену сообщается его номер tt: «я пятый»;
  • относительная позиция (relative position) — паре токенов сообщается расстояние между ними i−ji - j: «ты на три токена левее меня».

Для языка важнее относительная: смысл «прилагательное перед существительным» не зависит от того, стоит пара в начале документа или на тысячной позиции. Дальше мы увидим, как методы постепенно шли от абсолютной позиции к относительной — и как RoPE получает относительную позицию, поворачивая векторы по абсолютной.

Самый простой способ: завести для каждой позиции свой обучаемый вектор и прибавить его к эмбеддингу токена. Так сделано в GPT-1 (Radford et al., 2018, раздел 4.1: авторы явно выбирают обучаемые позиционные эмбеддинги вместо синусоидальных) и GPT-2 (Radford et al., 2019):

Xt(0)=E[xt]+P[t],t=0,1,…,T−1X^{(0)}_{t} = E[x_t] + P[t], \qquad t = 0, 1, \dots, T-1

где:

  • xtx_t — индекс токена на позиции tt;
  • E∈RV×dE \in \mathbb{R}^{V \times d} — матрица эмбеддингов токенов, E[xt]∈RdE[x_t] \in \mathbb{R}^{d} — её строка (embeddings.md);
  • P∈RTmax⁡×dP \in \mathbb{R}^{T_{\max} \times d} — матрица позиционных эмбеддингов, обучаемый параметр (не матрица перестановки из предыдущего раздела); P[t]∈RdP[t] \in \mathbb{R}^{d} — строка для позиции tt;
  • Xt(0)∈RdX^{(0)}_{t} \in \mathbb{R}^{d} — вход первого блока декодера для позиции tt (строка tt матрицы X(0)∈RT×dX^{(0)} \in \mathbb{R}^{T \times d}, как в embeddings.md). Для всей последовательности X(0)=E[x0:T]+P[0:T]X^{(0)} = E[x_{0:T}] + P[0:T], где E[x0:T]E[x_{0:T}] и P[0:T]P[0:T] — строки с номерами x0,…,xT−1x_0, \dots, x_{T-1} и 0,…,T−10, \dots, T-1 соответственно.

Интуиция. Позиционный вектор — такой же «словарный» вектор, только словарь состоит из номеров позиций 0,1,…,Tmax⁡−10, 1, \dots, T_{\max}-1. После сложения вектор строки tt несёт одновременно «что за токен» и «где он стоит», и эквивариантность нарушается: одинаковые токены на разных позициях дают разные входы. Сложение, а не конкатенация, не увеличивает размерность; модель сама учится разносить информацию о токене и позиции по разным направлениям dd-мерного пространства.

Численный пример. Пусть d=2d = 2, E[«кот»]=(0.5,−1)E[\text{«кот»}] = (0.5, -1), P[0]=(0.1,0.2)P[0] = (0.1, 0.2), P[3]=(−0.3,0.4)P[3] = (-0.3, 0.4). Тогда «кот» на позиции 0 даёт (0.6,−0.8)(0.6, -0.8), а на позиции 3 — (0.2,−0.6)(0.2, -0.6): attention увидит два разных вектора.

Параметров Tmax⁡⋅dT_{\max} \cdot d: для GPT-1 (Tmax⁡=512T_{\max} = 512, d=768d = 768) — 393 216, для GPT-2 small (Tmax⁡=1024T_{\max} = 1024, d=768d = 768) — 786 432, около 0,6 % от 124,4M параметров модели (gpt2.md). Немного.

Главный недостаток — жёсткий предел длины. Строк P[t]P[t] для t≥Tmax⁡t \ge T_{\max} нет вовсе, и модель не может обработать больше Tmax⁡T_{\max} токенов за раз. Даже если добавить строки, их нечем обучить. Кроме того, строки для редких больших позиций обучаются хуже: длинных примеров в данных меньше. И каждая позиция кодируется независимо, поэтому связь «позиции 17 и 18 соседние» модель должна выучить сама.

Класс PositionalEmbeddings — обёртка над nn.Embedding(max_seq_len, emb_size): его матрица весов и есть PP. Метод forward(seq_len, start_pos=0, positions=None) возвращает строки матрицы PP с номерами start_pos … start_pos + seq_len − 1 формы [seq_len, emb_size], а с positions формы [batch, seq_len] — строки с этими номерами, [batch, seq_len, emb_size]:

if positions is not None: # паддинг: своя позиция у каждой строки
return self.embedding(positions) # [batch, seq_len, emb_size]
if seq_len < 1 or start_pos + seq_len > self.max_seq_len:
raise IndexError(...)
...
positions = torch.arange(start=start_pos, end=start_pos + seq_len, device=...)
return self.embedding(positions)

В моделях GPT и GPT2 сложение выглядит так (forward):

tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
if padding is None:
pos_out = self._position_embeddings(seq_len, start_pos=start_pos).unsqueeze(0) # [1, seq_len, emb_size]
else:
pos_out = self._position_embeddings(seq_len, positions=padding.positions) # [batch, seq_len, emb_size]
out = self._dropout(tok_out + pos_out) # broadcast по батчу без паддинга

Без паддинга unsqueeze(0) превращает [seq_len, emb_size] в [1, seq_len, emb_size], и одна и та же матрица позиций прибавляется ко всем примерам батча. С паддингом в attention_mask позиции у строк разные: позиция токена — его номер среди настоящих токенов строки, cumsum(mask) − 1 (у строки [pad, pad, a, b] токен a — на позиции 0). Их считает padding_from_attention_mask из core/padding.py и передаёт в positions; подробно — в masks.md. Веса PP инициализируются вместе с остальными Embedding/Linear нормальным распределением (N(0,0.022)\mathcal{N}(0, 0.02^2) по умолчанию, init_normal_ в core/weight_init.py). При загрузке весов OpenAI/HF матрица приходит из ключа wpe.weight (GPT-2) или positions_embed.weight (GPT-1), см. models/gpt/hf_weights.py.

Докстринг PositionalEmbeddings упоминает и синусоидальный вариант, но реализован только обучаемый: синусоидального кодирования в репозитории нет.

При генерации с KV-кэшем (generation.md) модель на каждом шаге получает только новый токен, seq_len = 1. Его позиция — не 0, а число уже обработанных токенов. Эту позицию считает функция cache_start_pos в core/generation.py: для кэша слоёв MHA она равна длине закэшированной последовательности K. Модель передаёт её в PositionalEmbeddings как start_pos и сначала проверяет check_sequence_length: если start_pos + seq_len > max_position_embeddings, выбрасывается ValueError.

Что будет, если забыть start_pos? Каждый новый токен получит P[0]P[0] — модель будет «думать», что каждое следующее слово стоит в начале текста.

Когда генерация выходит за Tmax⁡T_{\max}, generate (функция next_generation_input) берёт последние Tmax⁡T_{\max} токенов и пересчитывает их без кэша: окно сдвинулось, позиции всех токенов уменьшились на единицу, и старые K/V, посчитанные со старыми позициями, больше не годятся.

В исходном трансформере (Vaswani et al., 2017, раздел 3.5) позиционный вектор не обучается, а вычисляется по формуле и так же прибавляется к эмбеддингу токена:

PE(t,2i)=sin⁡ ⁣(t100002i/d),PE(t,2i+1)=cos⁡ ⁣(t100002i/d)\mathrm{PE}(t, 2i) = \sin\!\left(\frac{t}{10000^{2i/d}}\right), \qquad \mathrm{PE}(t, 2i+1) = \cos\!\left(\frac{t}{10000^{2i/d}}\right)

где:

  • tt — позиция токена;
  • i=0,1,…,d/2−1i = 0, 1, \dots, d/2 - 1 — номер пары координат; координаты 2i2i и 2i+12i+1 используют одну частоту;
  • dd — размерность модели (в статье dmodeld_{\text{model}});
  • PE(t)∈Rd\mathrm{PE}(t) \in \mathbb{R}^{d} — позиционный вектор, PE∈RTmax⁡×d\mathrm{PE} \in \mathbb{R}^{T_{\max} \times d} — таблица для всех позиций; параметров нет.

Обозначим частоту пары ωi=10000−2i/d\omega_i = 10000^{-2i/d}. Тогда пара ii — это точка (sin⁡ωit,cos⁡ωit)(\sin \omega_i t, \cos \omega_i t) на единичной окружности, которая с ростом tt равномерно вращается с угловой скоростью ωi\omega_i радиан на позицию.

Частоты образуют геометрическую прогрессию: ωi+1/ωi=10000−2/d\omega_{i+1} / \omega_i = 10000^{-2/d} — постоянное отношение. Периоды (длины волн) λi=2π/ωi\lambda_i = 2\pi / \omega_i растут от 2π2\pi (пара 0) до почти 10000⋅2π10000 \cdot 2\pi (последняя пара) — так и сказано в статье.

Интуиция: двоичный счётчик. Запишите числа 0, 1, 2, … в двоичной системе: младший разряд меняется каждый шаг, следующий — каждые два шага, дальше — каждые четыре. Синусоидальное кодирование — «гладкий» аналог такого счётчика: быстрые пары различают соседние позиции, медленные — далёкие. Геометрическая прогрессия частот даёт шкалу, одинаково подробную на всех масштабах (как разряды числа).

Численный пример. d=4d = 4: частоты ω0=1\omega_0 = 1, ω1=10000−1/2=0.01\omega_1 = 10000^{-1/2} = 0.01. Для t=1t = 1:

PE(1)=(sin⁡1, cos⁡1, sin⁡0.01, cos⁡0.01)≈(0.841, 0.540, 0.010, 1.000)\mathrm{PE}(1) = (\sin 1,\ \cos 1,\ \sin 0.01,\ \cos 0.01) \approx (0.841,\ 0.540,\ 0.010,\ 1.000)

Первая пара заметно повернулась, вторая — почти нет: её «стрелка» пройдёт полный круг только за 2π/0.01≈6282\pi / 0.01 \approx 628 позиций.

Авторы выбрали синусоиды, потому что, по их гипотезе, модели будет легко научиться обращать внимание по относительным позициям: для любого фиксированного сдвига kk вектор PE(t+k)\mathrm{PE}(t+k) — линейная функция PE(t)\mathrm{PE}(t), причём матрица не зависит от tt.

Выведем это для одной пары. По формулам синуса и косинуса суммы:

sin⁡(ω(t+k))=sin⁡(ωt)cos⁡(ωk)+cos⁡(ωt)sin⁡(ωk)cos⁡(ω(t+k))=cos⁡(ωt)cos⁡(ωk)−sin⁡(ωt)sin⁡(ωk)\begin{aligned} \sin(\omega(t+k)) &= \sin(\omega t)\cos(\omega k) + \cos(\omega t)\sin(\omega k) \\ \cos(\omega(t+k)) &= \cos(\omega t)\cos(\omega k) - \sin(\omega t)\sin(\omega k) \end{aligned}

В матричном виде:

(sin⁡(ω(t+k))cos⁡(ω(t+k)))=(cos⁡ωksin⁡ωk−sin⁡ωkcos⁡ωk)⏟Mk(sin⁡ωtcos⁡ωt)\begin{pmatrix} \sin(\omega(t+k)) \\ \cos(\omega(t+k)) \end{pmatrix} = \underbrace{\begin{pmatrix} \cos \omega k & \sin \omega k \\ -\sin \omega k & \cos \omega k \end{pmatrix}}_{M_k} \begin{pmatrix} \sin \omega t \\ \cos \omega t \end{pmatrix}

где:

  • ω\omega — частота пары;
  • kk — сдвиг позиции;
  • Mk∈R2×2M_k \in \mathbb{R}^{2 \times 2} — матрица поворота на угол ωk\omega k (здесь по часовой стрелке, потому что порядок координат «синус, косинус»). Она зависит от kk, но не от tt.

Для всего вектора матрица сдвига блочно-диагональная: на диагонали стоят блоки 2×22 \times 2, по одному на каждую пару со своей частотой ωi\omega_i. Итак, сдвиг позиции на kk — это поворот каждой пары на угол ωik\omega_i k.

Второе следствие: скалярное произведение двух позиционных векторов зависит только от расстояния:

PE(t)⋅PE(t+k)=∑i=0d/2−1[sin⁡(ωit)sin⁡(ωi(t+k))+cos⁡(ωit)cos⁡(ωi(t+k))]=∑i=0d/2−1cos⁡(ωik)\mathrm{PE}(t) \cdot \mathrm{PE}(t+k) = \sum_{i=0}^{d/2-1} \left[\sin(\omega_i t)\sin(\omega_i (t+k)) + \cos(\omega_i t)\cos(\omega_i (t+k))\right] = \sum_{i=0}^{d/2-1} \cos(\omega_i k)

(использована формула cos⁡(a−b)=cos⁡acos⁡b+sin⁡asin⁡b\cos(a - b) = \cos a \cos b + \sin a \sin b). Например, при d=8d = 8 произведения PE(3)⋅PE(10)\mathrm{PE}(3)\cdot\mathrm{PE}(10) и PE(20)⋅PE(27)\mathrm{PE}(20)\cdot\mathrm{PE}(27) оба равны ≈3.516\approx 3.516.

Почему этого недостаточно. Хорошее свойство есть у самих векторов PE\mathrm{PE}, но в модель они попадают через сложение с эмбеддингом токена, а затем через проекции WqW_q, WkW_k. Оценка внимания (ei+PE(i))WqWk⊤(ej+PE(j))⊤(e_i + \mathrm{PE}(i)) W_q W_k^\top (e_j + \mathrm{PE}(j))^\top раскладывается на четыре слагаемых, и в трёх из них позиция смешана с содержимым произвольными матрицами. Относительность не гарантирована — модель должна её выучить. RoPE берёт ту же идею «сдвиг = поворот» и применяет поворот прямо к qq и kk, где он работает точно.

История. Vaswani et al. сравнили синусоиды с обучаемыми эмбеддингами и получили почти одинаковое качество (таблица 3, строка E); синусоиды выбрали в надежде на экстраполяцию на длины, большие, чем при обучении. GPT-1 и GPT-2 вернулись к обучаемым эмбеддингам.

В этом репозитории синусоидальное кодирование не реализовано.

Эти методы в репозитории не реализованы; они нужны как контекст, чтобы увидеть, какое место занимает RoPE.

Shaw et al. (2018) первыми встроили относительную позицию прямо в attention. Для каждой пары (i,j)(i, j) берётся обучаемый вектор aijKa_{ij}^K, зависящий только от обрезанного расстояния clip⁡(j−i,−k,k)\operatorname{clip}(j - i, -k, k), и прибавляется к ключу:

eij=xiWq (xjWk+aijK)⊤dhe_{ij} = \frac{x_i W_q \,(x_j W_k + a_{ij}^K)^\top}{\sqrt{d_h}}

где kk — максимальное учитываемое расстояние (обозначение статьи; здесь это число, а не вектор ключа), дальше все расстояния считаются одинаковыми; аналогичный вектор aijVa_{ij}^V прибавляется к значениям. Цена — дополнительные параметры и тензор оценок, зависящий от пары позиций, что усложняет эффективную реализацию.

T5 (Raffel et al., 2019) упростил идею: к оценке eije_{ij} прибавляется обучаемое число (скаляр), зависящее от расстояния j−ij - i, разбитого на корзины (buckets), своё для каждой головы.

ALiBi (Press et al., 2022) обходится без обучаемых параметров: к оценке прибавляется штраф, линейно растущий с расстоянием,

eij=qikj⊤dh−mh (i−j),j≤ie_{ij} = \frac{q_i k_j^\top}{\sqrt{d_h}} - m_h \,(i - j), \qquad j \le i

где mh>0m_h > 0 — фиксированный наклон головы hh; наклоны образуют геометрическую прогрессию (для 8 голов — 1/2,1/4,…,1/2561/2, 1/4, \dots, 1/256). Далёкие токены получают меньший вес, «крутизна» у каждой головы своя. Позиционных эмбеддингов на входе нет. Статья называется «Train Short, Test Long»: главный аргумент — модель, обученная на коротких последовательностях, продолжает работать на более длинных (экстраполяция).

RoPE (Rotary Position Embedding, «ротационные позиционные эмбеддинги»; Su et al., 2021) ставит задачу так: найти преобразование f(x,m)f(x, m) вектора xx, стоящего на позиции mm, такое, что скалярное произведение преобразованных запроса и ключа зависит от позиций только через разность:

⟨f(q,m), f(k,n)⟩=g(q,k,m−n)\langle f(q, m),\, f(k, n) \rangle = g(q, k, m - n)

где:

  • q,k∈Rdhq, k \in \mathbb{R}^{d_h} — запрос токена на позиции mm и ключ токена на позиции nn (одной головы, уже после проекций WqW_q, WkW_k);
  • ⟨⋅,⋅⟩\langle \cdot, \cdot \rangle — скалярное произведение;
  • gg — какая-то функция, в которую позиции входят только как m−nm - n.

Ответ RoFormer (раздел 3.2): f(x,m)=Rmxf(x, m) = R_m x, где RmR_m — поворот на угол, пропорциональный mm. Разберём по шагам.

В этом разделе удобнее считать qq и kk векторами-столбцами и записывать скалярное произведение как q⊤kq^\top k; в коде векторы — строки, но скалярное произведение от этого не меняется.

Матрица поворота на угол α\alpha против часовой стрелки:

R(α)=(cos⁡α−sin⁡αsin⁡αcos⁡α)R(\alpha) = \begin{pmatrix} \cos\alpha & -\sin\alpha \\ \sin\alpha & \cos\alpha \end{pmatrix}

где α\alpha — угол в радианах; R(α)∈R2×2R(\alpha) \in \mathbb{R}^{2 \times 2} действует на столбец (x0,x1)⊤(x_0, x_1)^\top.

Пример: R(90°)R(90°) переводит (1,0)⊤(1, 0)^\top в (0,1)⊤(0, 1)^\top, а (0,1)⊤(0, 1)^\top — в (−1,0)⊤(-1, 0)^\top.

Нам понадобятся три свойства.

  1. Повороты складываются: R(α)R(β)=R(α+β)R(\alpha) R(\beta) = R(\alpha + \beta).
  2. Обратный поворот — транспонирование: R(α)⊤=R(−α)R(\alpha)^\top = R(-\alpha), и R(α)⊤R(α)=IR(\alpha)^\top R(\alpha) = I (матрица ортогональна).
  3. Как следствие: R(α)⊤R(β)=R(β−α)R(\alpha)^\top R(\beta) = R(\beta - \alpha).
Вывод свойств

Свойство 1. Перемножаем матрицы:

R(α)R(β)=(cos⁡αcos⁡β−sin⁡αsin⁡β−cos⁡αsin⁡β−sin⁡αcos⁡βsin⁡αcos⁡β+cos⁡αsin⁡β−sin⁡αsin⁡β+cos⁡αcos⁡β)R(\alpha)R(\beta) = \begin{pmatrix} \cos\alpha\cos\beta - \sin\alpha\sin\beta & -\cos\alpha\sin\beta - \sin\alpha\cos\beta \\ \sin\alpha\cos\beta + \cos\alpha\sin\beta & -\sin\alpha\sin\beta + \cos\alpha\cos\beta \end{pmatrix}

По формулам cos⁡(α+β)=cos⁡αcos⁡β−sin⁡αsin⁡β\cos(\alpha+\beta) = \cos\alpha\cos\beta - \sin\alpha\sin\beta и sin⁡(α+β)=sin⁡αcos⁡β+cos⁡αsin⁡β\sin(\alpha+\beta) = \sin\alpha\cos\beta + \cos\alpha\sin\beta это ровно

(cos⁡(α+β)−sin⁡(α+β)sin⁡(α+β)cos⁡(α+β))=R(α+β)\begin{pmatrix} \cos(\alpha+\beta) & -\sin(\alpha+\beta) \\ \sin(\alpha+\beta) & \cos(\alpha+\beta) \end{pmatrix} = R(\alpha+\beta)

Свойство 2. Транспонирование меняет местами внедиагональные элементы:

R(α)⊤=(cos⁡αsin⁡α−sin⁡αcos⁡α)=(cos⁡(−α)−sin⁡(−α)sin⁡(−α)cos⁡(−α))=R(−α)R(\alpha)^\top = \begin{pmatrix} \cos\alpha & \sin\alpha \\ -\sin\alpha & \cos\alpha \end{pmatrix} = \begin{pmatrix} \cos(-\alpha) & -\sin(-\alpha) \\ \sin(-\alpha) & \cos(-\alpha) \end{pmatrix} = R(-\alpha)

(косинус чётный, синус нечётный). Тогда по свойству 1: R(α)⊤R(α)=R(−α)R(α)=R(0)=IR(\alpha)^\top R(\alpha) = R(-\alpha)R(\alpha) = R(0) = I.

Свойство 3. R(α)⊤R(β)=R(−α)R(β)=R(β−α)R(\alpha)^\top R(\beta) = R(-\alpha) R(\beta) = R(\beta - \alpha).

Блочно-диагональная матрица для вектора головы

Заголовок раздела «Блочно-диагональная матрица для вектора головы»

Вектор головы имеет размерность dhd_h (чётную). Разобьём его на dh/2d_h/2 пар соседних координат (x0,x1),(x2,x3),…(x_0, x_1), (x_2, x_3), \dots и повернём каждую пару ii на свой угол mθim\theta_i:

Rm=(R(mθ0)0⋯00R(mθ1)⋯0⋮⋮⋱⋮00⋯R(mθdh/2−1))∈Rdh×dhR_m = \begin{pmatrix} R(m\theta_0) & 0 & \cdots & 0 \\ 0 & R(m\theta_1) & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & R(m\theta_{d_h/2-1}) \end{pmatrix} \in \mathbb{R}^{d_h \times d_h}

где:

  • mm — позиция токена (целое, 0≤m<Tmax⁡0 \le m < T_{\max});
  • θi\theta_i — частота пары ii (в радианах на позицию); здесь θ\theta — не параметры модели, а фиксированные числа;
  • R(mθi)R(m\theta_i) — блок 2×22 \times 2 из предыдущего пункта; вне диагональных блоков — нули.

Запрос и ключ преобразуются так:

q~m=Rm qm,k~n=Rn kn\tilde q_m = R_m\, q_m, \qquad \tilde k_n = R_n\, k_n

где qm=Wq⊤xmq_m = W_q^\top x_m и kn=Wk⊤xnk_n = W_k^\top x_n — обычные проекции (в столбцовой записи). Для пары ii это покоординатно:

x~2i=x2icos⁡(mθi)−x2i+1sin⁡(mθi)x~2i+1=x2isin⁡(mθi)+x2i+1cos⁡(mθi)\begin{aligned} \tilde x_{2i} &= x_{2i}\cos(m\theta_i) - x_{2i+1}\sin(m\theta_i) \\ \tilde x_{2i+1} &= x_{2i}\sin(m\theta_i) + x_{2i+1}\cos(m\theta_i) \end{aligned}

Именно эти две строки записаны в докстринге класса RoPE и реализованы в forward.

Все свойства поворотов переносятся на RmR_m поблочно: блоки не взаимодействуют, поэтому RaRb=Ra+bR_a R_b = R_{a+b}, Rm⊤=R−mR_m^\top = R_{-m}, Rm⊤Rm=IR_m^\top R_m = I и Ra⊤Rb=Rb−aR_a^\top R_b = R_{b-a}.

Частоты берутся как в синусоидальном кодировании:

θi=base−2i/dh,i=0,1,…,dh/2−1\theta_i = \text{base}^{-2i/d_h}, \qquad i = 0, 1, \dots, d_h/2 - 1

где:

  • base\text{base} — база (в конфигах rope_theta), обычно 10410^4; должна быть больше 1;
  • dhd_h — размер головы (не dd модели: поворачиваются векторы голов);
  • θ0=1\theta_0 = 1 радиан на позицию, последняя частота θdh/2−1=base−(dh−2)/dh≈1/base\theta_{d_h/2-1} = \text{base}^{-(d_h-2)/d_h} \approx 1/\text{base}.

Как и у Vaswani, частоты убывают в геометрической прогрессии: у пары 0 угол растёт на радиан за токен, у последней — на десятитысячную долю радиана. Подробнее о выборе базы — в разделе «Скорости вращения и база».

Пример. dh=4d_h = 4, base=100\text{base} = 100 (маленькая база для наглядности): θ0=1000=1\theta_0 = 100^{0} = 1, θ1=100−2/4=0.1\theta_1 = 100^{-2/4} = 0.1.

Главное свойство: оценка зависит только от m − n

Заголовок раздела «Главное свойство: оценка зависит только от m − n»

Утверждение. Для любых q,k∈Rdhq, k \in \mathbb{R}^{d_h} и позиций m,nm, n:

(Rmq)⊤(Rnk)=q⊤Rn−m k(R_m q)^\top (R_n k) = q^\top R_{n-m}\, k

Правая часть зависит от позиций только через разность n−mn - m.

Доказательство.

Шаг 1. Транспонирование произведения: (Rmq)⊤=q⊤Rm⊤(R_m q)^\top = q^\top R_m^\top. Значит,

(Rmq)⊤(Rnk)=q⊤Rm⊤Rn k(R_m q)^\top (R_n k) = q^\top R_m^\top R_n\, k

Шаг 2. Rm⊤=R−mR_m^\top = R_{-m} (ортогональность поворота, свойство 2 поблочно).

Шаг 3. R−mRn=Rn−mR_{-m} R_n = R_{n-m} (повороты складываются, свойство 1 поблочно: блок ii равен R(−mθi)R(nθi)=R((n−m)θi)R(-m\theta_i)R(n\theta_i) = R((n-m)\theta_i)).

Шаг 4. Подставляем: (Rmq)⊤(Rnk)=q⊤Rn−m k(R_m q)^\top (R_n k) = q^\top R_{n-m}\, k. ∎

Интуиция. Каждый вектор поворачивается на угол, зависящий от своей абсолютной позиции. Но скалярное произведение двух векторов зависит только от их длин и угла между ними. Если оба вектора повернуть ещё на один и тот же угол cθic\theta_i (сдвинуть обе позиции на cc), угол между ними не изменится. Поэтому (m,n)(m, n) и (m+c,n+c)(m + c, n + c) дают одну и ту же оценку внимания.

Явная формула. Распишем вклад одной пары. Пусть δ=m−n\delta = m - n, а (qe,qo)(q_e, q_o) и (ke,ko)(k_e, k_o) — пары ii векторов qq и kk (чётная и нечётная координаты). Тогда

(Rmq)⊤(Rnk)=∑i=0dh/2−1[aicos⁡(δθi)+bisin⁡(δθi)],ai=qeke+qoko,bi=qeko−qoke(R_m q)^\top (R_n k) = \sum_{i=0}^{d_h/2-1} \Big[ a_i \cos(\delta\theta_i) + b_i \sin(\delta\theta_i) \Big], \qquad a_i = q_e k_e + q_o k_o,\quad b_i = q_e k_o - q_o k_e

где aia_i — скалярное произведение пар, bib_i — их «косое» произведение (площадь параллелограмма со знаком). Оценка — это сумма синусоид по расстоянию δ\delta с частотами θi\theta_i и амплитудами, которые задаёт содержимое qq и kk. Модель через WqW_q и WkW_k управляет амплитудами и может, например, сделать голову, которая смотрит на соседний токен.

Вывод явной формулы

По доказанному, вклад пары ii равен q⊤R(−δθi)kq^\top R(-\delta\theta_i) k (с учётом n−m=−δn - m = -\delta). Обозначим φ=δθi\varphi = \delta\theta_i. Поворот на −φ-\varphi:

R(−φ)(keko)=(kecos⁡φ+kosin⁡φ−kesin⁡φ+kocos⁡φ)R(-\varphi)\begin{pmatrix} k_e \\ k_o \end{pmatrix} = \begin{pmatrix} k_e\cos\varphi + k_o\sin\varphi \\ -k_e\sin\varphi + k_o\cos\varphi \end{pmatrix}

Скалярно умножаем на (qe,qo)(q_e, q_o):

qekecos⁡φ+qekosin⁡φ−qokesin⁡φ+qokocos⁡φ=(qeke+qoko)cos⁡φ+(qeko−qoke)sin⁡φq_e k_e\cos\varphi + q_e k_o\sin\varphi - q_o k_e \sin\varphi + q_o k_o\cos\varphi = (q_e k_e + q_o k_o)\cos\varphi + (q_e k_o - q_o k_e)\sin\varphi

Суммируем по парам.

Поворот на плоскости — это умножение на комплексное число единичной длины. Сопоставим паре ii вектора xx комплексное число:

zi=x2i+i x2i+1∈Cz_i = x_{2i} + \mathrm{i}\, x_{2i+1} \in \mathbb{C}

где i\mathrm{i} — мнимая единица. Тогда поворот пары на угол mθim\theta_i — это умножение на eimθi=cos⁡(mθi)+isin⁡(mθi)e^{\mathrm{i} m\theta_i} = \cos(m\theta_i) + \mathrm{i}\sin(m\theta_i):

zi eimθi=(x2icos⁡mθi−x2i+1sin⁡mθi)+i (x2isin⁡mθi+x2i+1cos⁡mθi)z_i\, e^{\mathrm{i} m\theta_i} = \big(x_{2i}\cos m\theta_i - x_{2i+1}\sin m\theta_i\big) + \mathrm{i}\,\big(x_{2i}\sin m\theta_i + x_{2i+1}\cos m\theta_i\big)

— те же формулы, что для x~2i\tilde x_{2i}, x~2i+1\tilde x_{2i+1} выше. Весь RoPE — это dh/2d_h/2 комплексных умножений (RoFormer, раздел 3.4.1; так же устроен эталонный код Meta, который переводит пары в комплексные числа).

Скалярное произведение через комплексные числа. Для пар z=a+ibz = a + \mathrm{i}b и w=c+ifw = c + \mathrm{i}f (вещественные a,b,c,fa, b, c, f — координаты двух пар): zwˉ=(a+ib)(c−if)=(ac+bf)+i(bc−af)z\bar w = (a + \mathrm{i}b)(c - \mathrm{i}f) = (ac + bf) + \mathrm{i}(bc - af), так что Re⁡(zwˉ)=ac+bf\operatorname{Re}(z \bar w) = ac + bf — скалярное произведение пар (a,b)(a, b) и (c,f)(c, f). Поэтому

q⊤k=Re⁡∑i=0dh/2−1zi(q) zi(k)‾q^\top k = \operatorname{Re} \sum_{i=0}^{d_h/2-1} z_i^{(q)}\, \overline{z_i^{(k)}}

где zi(q)z_i^{(q)}, zi(k)z_i^{(k)} — комплексные числа пар qq и kk, черта — комплексное сопряжение.

Относительное свойство. Сопряжение произведения — произведение сопряжений, и eiφ‾=e−iφ\overline{e^{\mathrm{i}\varphi}} = e^{-\mathrm{i}\varphi}. Тогда

(Rmq)⊤(Rnk)=Re⁡∑izi(q)eimθi  zi(k)‾ e−inθi=Re⁡∑izi(q) zi(k)‾  ei(m−n)θi(R_m q)^\top (R_n k) = \operatorname{Re} \sum_i z_i^{(q)} e^{\mathrm{i} m\theta_i}\; \overline{z_i^{(k)}}\, e^{-\mathrm{i} n\theta_i} = \operatorname{Re} \sum_i z_i^{(q)}\, \overline{z_i^{(k)}}\; e^{\mathrm{i}(m-n)\theta_i}

Абсолютные позиции сократились в показателе экспоненты: осталась только разность m−nm - n. Это одна строчка вместо четырёх шагов матричного доказательства — показатели экспонент складываются так же, как углы поворотов.

Поворот не меняет длину вектора:

∥Rmx∥2=(Rmx)⊤(Rmx)=x⊤Rm⊤Rm x=x⊤x=∥x∥2\lVert R_m x \rVert^2 = (R_m x)^\top (R_m x) = x^\top R_m^\top R_m\, x = x^\top x = \lVert x \rVert^2

где ∥⋅∥\lVert \cdot \rVert — евклидова норма. Следствия: масштаб оценок q⊤k/dhq^\top k / \sqrt{d_h} после RoPE остаётся тем же, что без него, и нормирование на dh\sqrt{d_h} по-прежнему корректно (attention.md). Позиция не «раздувает» и не «гасит» векторы, как это может делать прибавленный позиционный вектор. И при m=nm = n (токен смотрит на себя) R0=IR_0 = I в формуле q⊤Rn−mkq^\top R_{n-m} k — оценка та же, что без RoPE.

RoPE применяется только к QQ и KK. Причины:

  1. Позиция нужна, чтобы решить, куда смотреть. Относительная позиция должна влиять на веса внимания softmax⁡(QK⊤)\operatorname{softmax}(QK^\top) — а они вычисляются только из QQ и KK. Значения VV — это то, что забирается с выбранных позиций.
  2. Поворот V сломал бы относительность. Выход головы — взвешенная сумма ∑jwijvj\sum_j w_{ij} v_j. Если бы vjv_j были повёрнуты на угол своей абсолютной позиции jj, в выход попала бы абсолютная позиция каждого источника: одинаковый контекст, сдвинутый на cc позиций, давал бы другой выход. В скалярном произведении q⊤kq^\top k повороты сокращаются, а во взвешенной сумме сокращаться нечему.

Итог: RoPE не добавляет позицию к содержимому, которое передаётся дальше по residual-потоку, а только управляет маршрутизацией внимания.

Возьмём dh=4d_h = 4, base=100\text{base} = 100, то есть θ0=1\theta_0 = 1, θ1=0.1\theta_1 = 0.1, и векторы

q=(1, 2, 0, 1),k=(0, 1, 1, 1)q = (1,\ 2,\ 0,\ 1), \qquad k = (0,\ 1,\ 1,\ 1)

Без RoPE q⊤k=0+2+0+1=3q^\top k = 0 + 2 + 0 + 1 = 3.

Поворот qq на позиции m=3m = 3. Пара 0, угол 3⋅1=33 \cdot 1 = 3: cos⁡3=−0.9900\cos 3 = -0.9900, sin⁡3=0.1411\sin 3 = 0.1411.

x̃₀ = 1·(−0.9900) − 2·0.1411 = −1.2722
x̃₁ = 1·0.1411 + 2·(−0.9900) = −1.8389

Пара 1, угол 3⋅0.1=0.33 \cdot 0.1 = 0.3: cos⁡0.3=0.9553\cos 0.3 = 0.9553, sin⁡0.3=0.2955\sin 0.3 = 0.2955.

x̃₂ = 0·0.9553 − 1·0.2955 = −0.2955
x̃₃ = 0·0.2955 + 1·0.9553 = 0.9553

Поворот kk на позиции n=1n = 1. Пара 0, угол 1: cos⁡1=0.5403\cos 1 = 0.5403, sin⁡1=0.8415\sin 1 = 0.8415 → (0−0.8415, 0+0.5403)=(−0.8415, 0.5403)(0 - 0.8415,\ 0 + 0.5403) = (-0.8415,\ 0.5403). Пара 1, угол 0.1: cos⁡0.1=0.9950\cos 0.1 = 0.9950, sin⁡0.1=0.0998\sin 0.1 = 0.0998 → (0.9950−0.0998, 0.0998+0.9950)=(0.8952, 1.0948)(0.9950 - 0.0998,\ 0.0998 + 0.9950) = (0.8952,\ 1.0948).

Скалярное произведение:

пара 0: (−1.2722)(−0.8415) + (−1.8389)(0.5403) = 1.0706 − 0.9936 = 0.0770
пара 1: (−0.2955)(0.8952) + (0.9553)(1.0948) = −0.2645 + 1.0459 = 0.7814
итого: 0.8584

Проверка по явной формуле. δ=m−n=2\delta = m - n = 2. Пара 0: a0=1⋅0+2⋅1=2a_0 = 1\cdot 0 + 2 \cdot 1 = 2, b0=1⋅1−2⋅0=1b_0 = 1\cdot 1 - 2\cdot 0 = 1, вклад 2cos⁡2+sin⁡2=−0.8323+0.9093=0.07702\cos 2 + \sin 2 = -0.8323 + 0.9093 = 0.0770. Пара 1: a1=0+1=1a_1 = 0 + 1 = 1, b1=0⋅1−1⋅1=−1b_1 = 0\cdot 1 - 1\cdot 1 = -1, вклад cos⁡0.2−sin⁡0.2=0.9801−0.1987=0.7814\cos 0.2 - \sin 0.2 = 0.9801 - 0.1987 = 0.7814. Совпадает.

Сдвиг обеих позиций. Те же векторы на других позициях:

mmnnq~m\tilde q_mk~n\tilde k_nq~m⊤k~n\tilde q_m^\top \tilde k_n
31(−1.2722, −1.8389, −0.2955, 0.9553)(−0.8415, 0.5403, 0.8952, 1.0948)0.8584
53(2.2015, −0.3916, −0.4794, 0.8776)(−0.1411, −0.9900, 0.6598, 1.2509)0.8584
108(0.2490, −2.2222, −0.8415, 0.5403)(−0.9894, −0.1455, −0.0206, 1.4141)0.8584
13(−1.1426, 1.9221, −0.0998, 0.9950)(−0.1411, −0.9900, 0.6598, 1.2509)−0.5629
02(1, 2, 0, 1)(−0.9093, −0.4161, 0.7814, 1.1787)−0.5629

Повёрнутые векторы в каждой строке разные, а оценки при одинаковой разности m−nm - n совпадают. При m−n=−2m - n = -2 оценка другая: gg зависит от знака разности, то есть RoPE различает «ключ слева» и «ключ справа» (в causal-модели нужен только случай n≤mn \le m).

Тот же результат даёт класс из репозитория:

import torch
from llm.core.rope import RoPE
rope = RoPE(head_size=4, max_seq_len=16, base=100)
q = torch.tensor([1., 2., 0., 1.]).expand(1, 1, 16, 4) # один и тот же q на всех 16 позициях
k = torch.tensor([0., 1., 1., 1.]).expand(1, 1, 16, 4)
q_rot, k_rot = rope(q)[0, 0], rope(k)[0, 0] # [16, 4]
print(q_rot[3] @ k_rot[1], q_rot[5] @ k_rot[3]) # tensor(0.8584) tensor(0.8584)
print(torch.allclose(q_rot.norm(dim=-1), torch.tensor(6.0).sqrt())) # True: норма √6 сохранилась

Хранить и умножать матрицу Rm∈Rdh×dhR_m \in \mathbb{R}^{d_h \times d_h} расточительно: она почти вся из нулей, а умножение стоило бы O(dh2)O(d_h^2) операций на вектор. Раскроем формулы поворота так, чтобы остались только поэлементные операции (RoFormer, раздел 3.4.2):

x~=x⊙cos⁡m+rotate⁡(x)⊙sin⁡m\tilde x = x \odot \cos_m + \operatorname{rotate}(x) \odot \sin_m

где:

  • x∈Rdhx \in \mathbb{R}^{d_h} — вектор головы на позиции mm;
  • ⊙\odot — поэлементное умножение;
  • cos⁡m=(cos⁡mθ0, cos⁡mθ0, cos⁡mθ1, cos⁡mθ1, … )\cos_m = (\cos m\theta_0,\ \cos m\theta_0,\ \cos m\theta_1,\ \cos m\theta_1,\ \dots) — каждая частота повторена дважды, по разу на координату пары; sin⁡m\sin_m — аналогично;
  • rotate⁡(x)=(−x1, x0, −x3, x2, … )\operatorname{rotate}(x) = (-x_1,\ x_0,\ -x_3,\ x_2,\ \dots) — в каждой паре координаты меняются местами, у первой меняется знак (поворот пары на 90°).

Проверим для пары 0: x~0=x0cos⁡mθ0+(−x1)sin⁡mθ0\tilde x_0 = x_0\cos m\theta_0 + (-x_1)\sin m\theta_0, x~1=x1cos⁡mθ0+x0sin⁡mθ0\tilde x_1 = x_1\cos m\theta_0 + x_0\sin m\theta_0 — ровно формулы поворота. Стоимость — O(dh)O(d_h) на вектор, а таблицы cos⁡\cos и sin⁡\sin для всех позиций можно посчитать заранее.

В RoPE.forward то же записано чуть иначе — через раздельные чётные и нечётные координаты, без повторения частот:

x_even = x[..., 0::2] # x₀, x₂, x₄, … [B, H, T, d_h/2]
x_odd = x[..., 1::2] # x₁, x₃, x₅, …
x_rotated_even = x_even * cos - x_odd * sin # x̃₂ᵢ
x_rotated_odd = x_even * sin + x_odd * cos # x̃₂ᵢ₊₁
x_rotated = torch.stack([x_rotated_even, x_rotated_odd], dim=-1).flatten(-2) # чередуем обратно

stack(..., dim=-1) даёт тензор [..., d_h/2, 2] с парами (x~2i,x~2i+1)(\tilde x_{2i}, \tilde x_{2i+1}), а flatten(-2) раскладывает его обратно в порядок x~0,x~1,x~2,…\tilde x_0, \tilde x_1, \tilde x_2, \dots.

Два способа выбрать пары: соседние координаты и половины вектора

Заголовок раздела «Два способа выбрать пары: соседние координаты и половины вектора»

Какие координаты объединять в пару — вопрос соглашения. Существуют два варианта:

Пара iiФункция поворотаГде
чередующиеся пары (interleaved)(x2i, x2i+1)(x_{2i},\ x_{2i+1})(−x1,x0,−x3,x2,… )(-x_1, x_0, -x_3, x_2, \dots)статья RoFormer, эталонный код Meta (LLaMA), этот репозиторий
половины вектора(xi, xi+dh/2)(x_i,\ x_{i + d_h/2})rotate_half: (−xdh/2,…,−xdh−1, x0,…,xdh/2−1)(-x_{d_h/2}, \dots, -x_{d_h-1},\ x_0, \dots, x_{d_h/2-1})HuggingFace transformers

В HF-варианте cos⁡m=(cos⁡mθ0,…,cos⁡mθdh/2−1, cos⁡mθ0,…,cos⁡mθdh/2−1)\cos_m = (\cos m\theta_0, \dots, \cos m\theta_{d_h/2-1},\ \cos m\theta_0, \dots, \cos m\theta_{d_h/2-1}) — таблица частот повторена целиком, а не поэлементно; сами частоты те же.

Математически это одна и та же операция в разных системах координат: переставьте координаты вектора так, чтобы xix_i и xi+dh/2x_{i+d_h/2} оказались соседями, — и половинный вариант превратится в чередующийся. Обозначим эту перестановку матрицей Π\Pi. Для скалярного произведения (Πq)⊤(Πk)=q⊤Π⊤Πk=q⊤k(\Pi q)^\top (\Pi k) = q^\top \Pi^\top \Pi k = q^\top k — перестановка, применённая к qq и kk одинаково, оценок не меняет.

Следствие для весов. Модель, обученная с одним вариантом, не работает с другим: её WqW_q, WkW_k выучены так, что пары частот лежат в определённых координатах. Но перестановку можно «впечатать» в веса: переставить выходы (столбцы WqW_q, WkW_k в нашей записи xWxW) — тогда проекция сразу выдаёт векторы в нужном порядке.

Функция _hf_to_meta_rows в models/llama/hf_weights.py делает именно это. В PyTorch nn.Linear хранит вес формы [out, in], то есть W⊤W^\top, поэтому переставляются строки тензора веса. Внутри каждой головы hh для i=0,…,dh/2−1i = 0, \dots, d_h/2 - 1 и s∈{0,1}s \in \{0, 1\}:

Wздесь[h dh+2i+s, :]=WHF[h dh+s⋅dh2+i, :]W^{\text{здесь}}\big[h\,d_h + 2i + s,\ :\big] = W^{\text{HF}}\big[h\,d_h + s \cdot \tfrac{d_h}{2} + i,\ :\big]

где:

  • WHF,Wздесь∈R(Hdh)×dW^{\text{HF}}, W^{\text{здесь}} \in \mathbb{R}^{(H d_h) \times d} — веса q_proj.weight из HF и _q.weight в этом репозитории (для k_proj — GG голов вместо HH);
  • s=0s = 0 — первая координата пары, s=1s = 1 — вторая;
  • hh — номер головы; перестановка не выходит за пределы головы.

Словами: строки головы в HF идут как [x0…xdh/2−1∣y0…ydh/2−1][x_0 \dots x_{d_h/2-1} \mid y_0 \dots y_{d_h/2-1}] (первые и вторые координаты пар), здесь — [x0,y0,x1,y1,… ][x_0, y_0, x_1, y_1, \dots]. Для dh=8d_h = 8 строка jj здесь берётся из строки HF

j здесь: 0 1 2 3 4 5 6 7
строка HF: 0 4 1 5 2 6 3 7

В коде это три операции с формой:

value.reshape(num_heads, 2, head_size // 2, *rest) # [голова, s, i, …]
.transpose(1, 2) # [голова, i, s, …]
.reshape(value.shape) # строка h·d_h + 2i + s

Применяется к q_proj с num_heads и к k_proj с num_kv_heads (у GQA голов K меньше). v_proj и o_proj не переставляются: V не поворачивается, и порядок его координат ни с чем не должен совпадать. Скрипт конвертации HF (convert_llama_weights_to_hf.py) при переходе от весов Meta к HF делает прямую перестановку; здесь — обратная. Подробнее о загрузке весов — в llama.md.

Проверить эквивалентность можно так: посчитать оценки QK⊤QK^\top HF-способом (rotate_half) на исходных весах и способом этого репозитория на переставленных — они совпадут; без перестановки — нет.

RoFormer (раздел 3.4.3) отмечает ещё одно свойство — затухание на больших расстояниях (long-term decay). Запишем оценку в комплексной форме: ∑ihi eiδθi\sum_i h_i\, e^{\mathrm{i}\delta\theta_i}, где hi=zi(q)zi(k)‾h_i = z_i^{(q)} \overline{z_i^{(k)}}, δ=m−n\delta = m - n. Суммирование по частям (преобразование Абеля) даёт оценку сверху:

∣∑i=0dh/2−1hi eiδθi∣≤(max⁡i∣hi+1−hi∣)∑j=1dh/2∣Sj∣,Sj=∑i=0j−1eiδθi\Big| \sum_{i=0}^{d_h/2-1} h_i\, e^{\mathrm{i}\delta\theta_i} \Big| \le \Big( \max_i |h_{i+1} - h_i| \Big) \sum_{j=1}^{d_h/2} |S_j|, \qquad S_j = \sum_{i=0}^{j-1} e^{\mathrm{i}\delta\theta_i}

где SjS_j — частичные суммы единичных векторов с углами δθi\delta\theta_i (считаем hdh/2=0h_{d_h/2} = 0). При δ=0\delta = 0 все слагаемые SjS_j смотрят в одну сторону, и ∣Sj∣=j|S_j| = j. С ростом δ\delta углы расходятся, единичные векторы начинают гасить друг друга, и суммы уменьшаются. Среднее 2dh∑j∣Sj∣\frac{2}{d_h}\sum_j |S_j| при dh=128d_h = 128, base=104\text{base} = 10^4:

δ\delta01510501002001000
среднее ∣Sj∣\lvert S_j \rvert32.531.520.818.012.610.27.24.5

Спад идёт с колебаниями (например, при δ=300\delta = 300 — 7.2, больше, чем при 250 — 6.5). Это верхняя граница, а не сама оценка: она не запрещает модели смотреть далеко, но при прочих равных далёкие пары токенов получают меньше «максимально возможного» веса. Такое смещение в сторону близких токенов считается желательным для языка.

Пары вращаются с разной скоростью: θi=base−2i/dh\theta_i = \text{base}^{-2i/d_h} убывает от θ0=1\theta_0 = 1 радиана на позицию у первой пары до ≈1/base\approx 1/\text{base} у последней. Это похоже на часы с dh/2d_h/2 стрелками: быстрые делают оборот за несколько токенов, медленные — за тысячи. Период пары — через сколько токенов её угол повторяется — равен 2π/θi2\pi / \theta_i. Для dh=64d_h = 64:

Пара iiθi\theta_i при base = 10⁴периодθi\theta_i при base = 10⁶период
016 токенов16 токенов
80.1630.032199
160.016280.0016 283
240.0016 2833.2·10⁻⁵~199 000
311.3·10⁻⁴~47 0001.5·10⁻⁶~4 000 000

Каждая пара различает расстояния на своём масштабе. Быстрые пары точно кодируют соседство, но на большом расстоянии их угол успевает много раз провернуться, и расстояния 1000 и 1006 для них почти неразличимы (6≈2π6 \approx 2\pi). Дальние расстояния однозначно кодируют только медленные пары — пока их угол в пределах контекста не делает полного оборота.

База задаёт, насколько медленной будет последняя пара, то есть под какую длину контекста рассчитана «шкала». Угол самой медленной пары (dh=64d_h = 64, i=31i = 31) на позиции mm:

Позиция mm5124 0968 19232 768
base = 10⁴3.9°31°63°250°
base = 10⁶0.04°0.4°0.7°2.9°

При базе 10410^4 для контекста 32k запаса почти нет: медленная пара проходит больше двух третей оборота. При 10610^6 она поворачивается лишь на 3°. Цена большой базы — все пары, кроме первой, вращаются медленнее, и ближние расстояния кодируются грубее (сравните строки 8 и 16 первой таблицы).

Модельrope_thetaКонтекст
RoFormer, LLaMA-1, Mistral 7B v0.1, Gemma10 000до 8k
Mixtral 8x7B1 000 00032k

В репозитории база задаётся ключом конфига rope_theta (по умолчанию 10000) у LLaMA, Mistral, Mixtral и Gemma и передаётся в RoPE(head_size, max_seq_len, base=...). Для учебных конфигов (Tmax⁡≤512T_{\max} \le 512) разница между 10410^4 и 10610^6 почти не видна: самая медленная пара при 10410^4 к позиции 512 поворачивается примерно на 4°.

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

Что будет, если подать модели позицию больше той, на которой её обучали? Быстрые пары при этом не видят ничего нового — их углы давно прошли все значения на окружности. А медленные пары получают углы, которых модель никогда не видела, и оценки внимания становятся непредсказуемыми. Chen et al. (2023) показали, что прямая экстраполяция LLaMA за длину обучения быстро разрушает качество. Известные способы расширить контекст готовой модели (в репозитории их нет: конфига rope_scaling не существует, и hf_weights.py рассчитан на модели без него):

  • Position Interpolation (Chen et al., 2023): позиции сжимаются, m→m⋅L/L′m \to m \cdot L / L', где LL — длина обучения, L′L' — новая длина. Все углы остаются в знакомом диапазоне, но становятся дробными. После короткого дообучения (порядка 1000 шагов) LLaMA работает с контекстом до 32 768.
  • Увеличение базы («NTK-aware»-масштабирование, описано в YaRN, Peng et al., 2023): вместо равномерного сжатия увеличивается база. Быстрые пары почти не меняются (ближние расстояния кодируются как раньше), медленные замедляются сильнее.
  • Llama 2 Long (Xiong et al., 2023): база увеличена с 10 000 до 500 000 и модель дообучена на длинных последовательностях (контекст 32k).
  • Code Llama (Rozière et al., 2023): база 1 000 000, дообучение на последовательностях 16k; модель устойчиво работает на ещё более длинных входах.

Mixtral 8x7B сразу обучен с базой 10610^6 под контекст 32k.

При генерации с KV-кэшем (generation.md) ключи прошлых токенов не пересчитываются, а берутся из кэша. Для RoPE это означает:

  • Кэш хранит K уже повёрнутыми. Поворот RnR_n зависит только от позиции nn ключа, а она не меняется, пока токен в окне. Поэтому ключ поворачивают один раз, когда токен появился, и кладут в кэш. V кэшируется как есть.
  • Новый токен поворачивается по своей абсолютной позиции. Это start_pos — позиция первого нового токена. RoPE.forward(x, start_pos) берёт строки таблиц cos/sin[start_pos : start_pos + seq_len].
  • Откуда берётся start_pos. В MultiHeadAttention (LLaMA) — длина кэша: start_pos = cache[0].size(2). В GroupedQueryAttention (Mistral, Mixtral, Gemma) кэш со скользящим окном обрезается до window_size позиций, и его длина перестаёт совпадать с позицией токена; поэтому слой хранит кэш как тройку (K, V, next_pos) и берёт start_pos = cache[2], а после шага записывает next_pos = start_pos + seq_len.

Если start_pos передать неправильно (например, 0 на каждом шаге), новый запрос будет повёрнут так, будто он стоит в начале текста, и все расстояния до ключей из кэша окажутся неверными.

Когда генерация выходит за Tmax⁡T_{\max}, generate пересчитывает последние Tmax⁡T_{\max} токенов без кэша — как и у GPT: при сдвиге окна позиции всех токенов меняются, и повёрнутые K из кэша больше не годятся. Заметим, что для RoPE оценки зависят только от разностей позиций, поэтому математически старые K можно было бы оставить; однако в таблицах cos/sin нет строк за Tmax⁡T_{\max}, и реализация выбирает простой путь.

У GPT позиция добавляется один раз, на входе, и дальше путешествует по residual-потоку вместе с содержимым. У RoPE-моделей позиции на входе нет вовсе: она вносится в каждом слое, в каждой голове, и только в QQ и KK.

%%{init: {"flowchart": {"rankSpacing": 28, "nodeSpacing": 28}}}%%
flowchart TB
    subgraph GPT["GPT-1, GPT-2: сложение на входе"]
        direction TB
        I1(["token ids"]):::io --> TE1["Token Embedding"]:::blue
        I1 --> PE1["Position Embedding<br/>(обучаемая таблица)"]:::purple
        TE1 --> S1(("+")):::add
        PE1 --> S1
        S1 --> D1["Decoder × L<br/>attention без позиции"]:::gray
        D1 --> O1(["logits"]):::io
    end
    subgraph ROPE["LLaMA, Mistral, Mixtral, Gemma: поворот внутри attention"]
        direction TB
        I2(["token ids"]):::io --> TE2["Token Embedding"]:::blue
        TE2 --> X2(["x слоя"]):::io
        X2 --> Q2["W_q → Q"]:::gray
        X2 --> K2["W_k → K"]:::gray
        X2 --> V2["W_v → V"]:::gray
        Tab["RoPE: таблицы cos/sin<br/>один модуль на все слои"]:::rope
        Q2 --> RQ["поворот Q"]:::rope
        K2 --> RK["поворот K"]:::rope
        Tab -.-> RQ
        Tab -.-> RK
        RQ --> SC["оценки Q·Kᵀ → softmax"]:::gray
        RK --> SC
        SC --> AV["веса · V"]:::gray
        V2 -- "V не поворачивается" --> AV
        AV --> Rep(["повторяется в каждом из L слоёв"]):::io
    end
    style GPT fill:transparent,stroke:#9673a6,stroke-width:2px
    style ROPE fill:transparent,stroke:#3a9e8f,stroke-width:2px

    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 информация о позиции должна «дожить» до глубоких слоёв через residual-поток и смешивается с содержимым. У RoPE каждый слой получает точную относительную позицию заново, а в residual-поток она не попадает.

Класс RoPE — модуль без обучаемых параметров. Один экземпляр создаётся в __init__ модели (Llama, Mistral, Mixtral, Gemma) и под именем _position_embeddings передаётся во все слои attention:

self._position_embeddings = RoPE(
head_size=head_size,
max_seq_len=config["max_position_embeddings"],
base=config.get("rope_theta", 10_000),
)
def __init__(self, head_size: int, max_seq_len: int, base: float = 10_000):
super().__init__()
if base <= 1:
raise ValueError(f"base должна быть > 1, получено {base}")
if head_size % 2 != 0:
raise ValueError(f"head_size={head_size} должен быть чётным: RoPE поворачивает пары координат")
  • База больше 1. При base=1\text{base} = 1 все частоты равны 1 — все пары вращаются одинаково быстро, и медленных «стрелок» нет. При base<1\text{base} < 1 частоты растут, а не убывают. Оба случая — почти наверняка ошибка в конфиге.
  • Чётный head_size. Поворачиваются пары координат, одна координата осталась бы без пары. Модели проверяют это ещё раньше, при разборе конфига: resolve_head_size(config, ..., rope=True) в core/config_checks.py.
freqs = 1.0 / (base ** (2 * torch.arange(head_size // 2).float() / head_size)) # θᵢ, [d_h/2]
positions = torch.arange(max_seq_len).float() # m, [T_max]
freq_matrix = positions.unsqueeze(1) * freqs.unsqueeze(0) # m·θᵢ, [T_max, d_h/2]
  • freqs — формула θi=base−2i/dh\theta_i = \text{base}^{-2i/d_h} для i=0,…,dh/2−1i = 0, \dots, d_h/2 - 1 (arange(head_size // 2) даёт ii, умножение на 2 — 2i2i).
  • freq_matrix — внешнее произведение векторов позиций и частот: [T_max, 1] * [1, d_h/2] → [T_max, d_h/2], элемент (m,i)(m, i) равен углу mθim\theta_i.
self.register_buffer("cos_matrix", torch.cos(freq_matrix), persistent=False)
self.register_buffer("sin_matrix", torch.sin(freq_matrix), persistent=False)
  • Таблицы вычисляются один раз и хранятся как буферы: они переезжают на GPU вместе с моделью (model.to(device)), но не являются параметрами и не обучаются.
  • persistent=False — буферы не попадают в state_dict. Их незачем хранить: они полностью определяются head_size, max_seq_len и base. К тому же один объект RoPE зарегистрирован и в модели, и в каждом слое attention, и с persistent=True чекпоинт содержал бы одни и те же таблицы num_layers + 1 раз.
  • _load_from_state_dict выбрасывает ключи cos_matrix/sin_matrix, если они есть в старом чекпоинте (сохранённом до перехода на persistent=False), поэтому такие чекпоинты загружаются и с strict=True.

Размер таблиц: 2⋅Tmax⁡⋅dh/2=Tmax⁡⋅dh2 \cdot T_{\max} \cdot d_h / 2 = T_{\max} \cdot d_h чисел — не зависит ни от числа слоёв, ни от числа голов.

assert x.ndim == 4, "RoPE поддерживает только 4D-вход [batch, num_heads, seq_len, head_size]"
batch_size, num_heads, seq_len, head_size = x.shape
if positions is None:
cos = self.cos_matrix[start_pos:start_pos+seq_len].to(x.dtype) # [seq_len, head_size//2]
sin = self.sin_matrix[start_pos:start_pos+seq_len].to(x.dtype)
cos = cos.reshape(1, 1, seq_len, head_size // 2)
sin = sin.reshape(1, 1, seq_len, head_size // 2)
else:
cos = self.cos_matrix[positions].to(x.dtype).unsqueeze(1) # [batch, 1, seq_len, head_size//2]
sin = self.sin_matrix[positions].to(x.dtype).unsqueeze(1)
  1. Вход — QQ или KK уже после разбиения на головы: [B, H, T, d_h] (для K в GQA — [B, G, T, d_h]).
  2. Срез [start_pos : start_pos + seq_len] выбирает углы для абсолютных позиций новых токенов. Без кэша start_pos = 0.
  3. .to(x.dtype) приводит таблицы (они во float32) к типу входа, например bfloat16.
  4. reshape(1, 1, seq_len, d_h/2) готовит таблицы к broadcasting: одни и те же углы для всех примеров батча и всех голов — позиция токена от головы не зависит.
  5. При паддинге в attention_mask вместо start_pos передаются positions формы [B, T] — позиции токенов среди настоящих токенов своей строки (cumsum(mask) − 1, см. masks.md). Индексация cos_matrix[positions] берёт для каждой строки свои углы, unsqueeze(1) добавляет ось голов: углы по-прежнему общие для всех голов, но уже свои у каждого примера батча.

Далее — разделение на чётные/нечётные координаты, поворот и обратная склейка, разобранные в разделе «Эффективная реализация без матриц». Результат — новый тензор той же формы и типа, вход не изменяется.

В MultiHeadAttention (LLaMA) и GroupedQueryAttention (Mistral, Mixtral, Gemma) — после разбиения на головы и до склейки с кэшем:

positions = padding.positions if padding is not None else None
if self._rope is not None:
q = self._rope(q, start_pos=start_pos, positions=positions)
k = self._rope(k, start_pos=start_pos, positions=positions)
# затем: k = torch.cat([k_cache, k], dim=2) — в кэше K уже повёрнуты

Перед этим слой проверяет, что start_pos + seq_len не превышает max_seq_len, и выбрасывает ValueError. Сам RoPE.forward границу не проверяет: при выходе за таблицу срез окажется короче seq_len, и reshape упадёт с RuntimeError. При прямом использовании класса следите за длиной сами.

Для GPT-1 и GPT-2 параметр rope у MultiHeadAttention равен None, и поворот не применяется.

МетодГде входит позицияОбучаемых параметровОтносительная позицияЗа пределами длины обученияГде используетсяВ репозитории
Обучаемые абсолютныесумма с эмбеддингом на входеTmax⁡⋅dT_{\max} \cdot dнет, только если модель выучитневозможно: строк P[t]P[t] нетGPT-1, GPT-2PositionalEmbeddings
Синусоидальные (Vaswani)сумма с эмбеддингом на входе0сдвиг = линейное отображение PE\mathrm{PE}, но после проекций не гарантированаформула определена для любых tt, качество не гарантированоисходный Transformerнет
Shaw et al.векторы aijK,aijVa_{ij}^K, a_{ij}^V в attention2(2k+1)dh2(2k+1) d_h на слойда, до расстояния kkдальше kk расстояния неразличимы—нет
T5 biasскаляр к оценкечисло корзин × число головдадальние корзины общиеT5нет
ALiBiлинейный штраф к оценке0даглавное преимущество: работает на длинах больше обучения—нет
RoPEповорот Q и K в каждом слое0да, точно: q⊤Rn−mkq^\top R_{n-m} kплохо без дообучения; расширяется интерполяцией или увеличением базыLLaMA, Mistral, Mixtral, GemmaRoPE
  • Забытый start_pos при генерации с кэшем. И для PositionalEmbeddings, и для RoPE новый токен получит позицию 0. В репозитории start_pos вычисляется из кэша автоматически; ошибка возможна при самостоятельной сборке модели из модулей core.
  • Несовпадение соглашения о парах при переносе весов. Веса HF нельзя загрузить «как есть»: без перестановки строк q_proj/k_proj модель выдаёт мусор, хотя все формы тензоров совпадают. Ошибка не видна по формам — только по качеству.
  • Поворот V. Лишний поворот VV не вызывает ошибок формы, но ломает относительность и расходится с эталонными весами.
  • Смена rope_theta у обученной модели. Меняет все углы; без дообучения качество падает. При загрузке весов HF rope_theta в конфиге должен совпадать с HF (для Mixtral 8x7B — 10610^6).
  • Нечётный head_size. RoPE неприменим; конфиг отклоняется с ValueError.
  • d_h, а не d. Частоты считаются по размеру головы. Если перепутать, углы будут другими, и веса HF не подойдут.
  • Точность. Углы mθim\theta_i считаются во float32 один раз при создании модуля. Таблицы приводятся к типу входа (.to(x.dtype)); в bfloat16 (8 бит мантиссы) это даёт ошибку до ≈2⋅10−3\approx 2 \cdot 10^{-3} в значениях cos⁡\cos/sin⁡\sin — так же поступает и HF.
  • Causal-маска тоже несёт позицию. Модель без позиционного кодирования, но с causal-маской — не «мешок токенов» (см. выше). Поэтому «позиция не нужна» — неверный вывод из того, что модель без неё как-то обучилась.
  • Attention без маски переставочно-эквивариантен: Attn⁡(PX)=P Attn⁡(X)\operatorname{Attn}(PX) = P\,\operatorname{Attn}(X). Без позиционной информации модель видит мешок токенов.
  • GPT-1/GPT-2 прибавляют к эмбеддингу токена обучаемый вектор позиции P[t]P[t]: просто, Tmax⁡⋅dT_{\max} \cdot d параметров, жёсткий предел длины Tmax⁡T_{\max}.
  • Синусоидальное кодирование: частоты в геометрической прогрессии, сдвиг позиции — поворот пар координат. Эта идея ведёт к RoPE.
  • RoPE поворачивает qq и kk на углы mθim\theta_i: (Rmq)⊤(Rnk)=q⊤Rn−m k(R_m q)^\top (R_n k) = q^\top R_{n-m}\,k — оценка зависит только от расстояния, норма сохраняется, параметров нет, V не поворачивается.
  • Реализация — поэлементно через таблицы cos⁡\cos/sin⁡\sin за O(dh)O(d_h); соседние пары (Meta, этот репозиторий) и половины вектора (HF) эквивалентны с точностью до перестановки строк WqW_q, WkW_k.
  • База rope_theta задаёт самую медленную частоту и тем самым длину контекста, под которую рассчитана шкала; 10410^4 — классика, 10610^6 — Mixtral 32k.
  • С KV-кэшем K хранятся повёрнутыми, а новые токены поворачиваются по абсолютной позиции start_pos (в GQA — next_pos из кэша).
  1. Докажите, что без маски attention переставочно-эквивариантен, а с causal-маской — нет. Приведите пример из двух токенов, где перестановка меняет выход первого токена.

    Ответ

    Доказательство — в разделе «Attention не знает порядка». Пример с маской: токены a,ba, b. В порядке (a,b)(a, b) первый токен aa видит только себя, его выход — vav_a. В порядке (b,a)(b, a) токен aa стоит вторым, видит bb и себя, его выход — взвешенная сумма vav_a и vbv_b, в общем случае не равная vav_a. Значит, выход токена aa зависит от его места.

  2. Сколько параметров занимают позиционные эмбеддинги GPT-2 small (Tmax⁡=1024T_{\max} = 1024, d=768d = 768)? Какую долю это составляет от 124,4M?

    Ответ

    1024⋅768=786 4321024 \cdot 768 = 786\,432 параметров, около 0,63 %0{,}63\,\%.

  3. Выведите из формул суммы углов, что R(α)R(β)=R(α+β)R(\alpha)R(\beta) = R(\alpha + \beta), и объясните, почему отсюда следует Rm⊤Rn=Rn−mR_m^\top R_n = R_{n-m} для блочно-диагональных матриц RoPE.

    Ответ

    Вывод для 2×22 \times 2 — в разделе «Поворот на плоскости». Блочно-диагональные матрицы перемножаются поблочно: ii-й блок произведения равен произведению ii-х блоков. Поэтому Rm⊤RnR_m^\top R_n имеет блоки R(mθi)⊤R(nθi)=R(−mθi)R(nθi)=R((n−m)θi)R(m\theta_i)^\top R(n\theta_i) = R(-m\theta_i)R(n\theta_i) = R((n-m)\theta_i), то есть это Rn−mR_{n-m}.

  4. Посчитайте частоты θi\theta_i и периоды для dh=8d_h = 8, base=104\text{base} = 10^4.

    Ответ

    θi=104⋅(−2i/8)=10−i\theta_i = 10^{4 \cdot (-2i/8)} = 10^{-i}: θ=(1, 0.1, 0.01, 0.001)\theta = (1,\ 0.1,\ 0.01,\ 0.001), периоды 2π/θi≈(6.3, 63, 628, 6283)2\pi/\theta_i \approx (6.3,\ 63,\ 628,\ 6283) токенов.

  5. Почему одной частоты мало? Пусть dh=2d_h = 2, θ0=1\theta_0 = 1, q=k=(1,0)q = k = (1, 0). Найдите оценку q~m⊤k~n\tilde q_m^\top \tilde k_n как функцию δ=m−n\delta = m - n и сравните δ=0\delta = 0 и δ=44\delta = 44.

    Ответ

    По явной формуле a=1a = 1, b=0b = 0: оценка cos⁡δ\cos\delta. При δ=0\delta = 0 — 1, при δ=44\delta = 44 — cos⁡44≈0.9998\cos 44 \approx 0.9998 (44≈7⋅2π44 \approx 7 \cdot 2\pi). Одна быстрая частота не отличает «тот же токен» от «44 токена назад». Медленные частоты разрешают эту неоднозначность — поэтому их набор в геометрической прогрессии.

  6. Почему RoPE применяют к Q и K, но не к V? Что сломается, если повернуть и V?

    Ответ

    См. раздел «Почему V не поворачивается»: позиция должна влиять на веса внимания, а они вычисляются из QK⊤QK^\top, где повороты сокращаются до Rn−mR_{n-m}. В выходе ∑jwijvj\sum_j w_{ij} v_j повороты RjvjR_j v_j не сокращаются, и выход начнёт зависеть от абсолютных позиций источников; кроме того, веса, обученные без поворота V, перестанут подходить.

  7. Для dh=4d_h = 4 запишите, из каких строк HF-матрицы q_proj.weight одной головы берутся строки 0, 1, 2, 3 в этом репозитории. Проверьте ответ вызовом _hf_to_meta_rows.

    Ответ

    По формуле 2i+s←s⋅dh/2+i2i + s \leftarrow s \cdot d_h/2 + i с dh/2=2d_h/2 = 2: строка 0 ← 0, 1 ← 2, 2 ← 1, 3 ← 3.

    import torch
    from llm.models.llama.hf_weights import _hf_to_meta_rows
    rows = torch.arange(4.).unsqueeze(1) # «вес» [4, 1]: номер строки HF
    print(_hf_to_meta_rows(rows, num_heads=1).squeeze(1)) # tensor([0., 2., 1., 3.])
  8. Самая медленная пара RoPE при dh=64d_h = 64: через сколько токенов она делает полный оборот при базе 10410^4 и при базе 5⋅1055 \cdot 10^5 (Llama 2 Long)? Почему для контекста 32k удобнее вторая?

    Ответ

    θ31=base−62/64\theta_{31} = \text{base}^{-62/64}. При 10410^4: θ31≈1.33⋅10−4\theta_{31} \approx 1.33 \cdot 10^{-4}, период ≈47 000\approx 47\,000. При 5⋅1055 \cdot 10^5: θ31≈3.0⋅10−6\theta_{31} \approx 3.0 \cdot 10^{-6}, период ≈2 085 000\approx 2\,085\,000. При базе 10410^4 на контексте 32k медленная пара проходит 250°250° — больше двух третей оборота, и её угол становится неоднозначным ближе к краю контекста; при 5⋅1055 \cdot 10^5 она поворачивается на несколько градусов и остаётся однозначной мерой дальних расстояний.

  • Vaswani et al. Attention Is All You Need. 2017. arXiv:1706.03762 — раздел 3.5, синусоидальное кодирование
  • Radford, Narasimhan, Salimans, Sutskever. Improving Language Understanding by Generative Pre-Training. OpenAI, 2018. PDF — обучаемые позиционные эмбеддинги GPT-1
  • Radford, Wu, Child, Luan, Amodei, Sutskever. Language Models are Unsupervised Multitask Learners. OpenAI, 2019. PDF — GPT-2
  • Shaw, Uszkoreit, Vaswani. Self-Attention with Relative Position Representations. 2018. arXiv:1803.02155
  • Raffel et al. Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer. 2019. arXiv:1910.10683 — T5, относительный bias
  • Press, Smith, Lewis. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. 2022. arXiv:2108.12409 — ALiBi
  • Su et al. RoFormer: Enhanced Transformer with Rotary Position Embedding. 2021. arXiv:2104.09864 — RoPE; разделы 3.2, 3.4.1–3.4.3
  • Haviv, Ram, Press, Izsak, Levy. Transformer Language Models without Positional Encodings Still Learn Positional Information. 2022. arXiv:2203.16634
  • Chen, Wong, Chen, Tian. Extending Context Window of Large Language Models via Positional Interpolation. 2023. arXiv:2306.15595
  • Peng, Quesnelle, Fan, Shippole. YaRN: Efficient Context Window Extension of Large Language Models. 2023. arXiv:2309.00071 — в том числе обзор «NTK-aware»-масштабирования
  • Xiong et al. Effective Long-Context Scaling of Foundation Models. 2023. arXiv:2309.16039 — Llama 2 Long
  • Rozière et al. Code Llama: Open Foundation Models for Code. 2023. arXiv:2308.12950
  • Jiang et al. Mixtral of Experts. 2024. arXiv:2401.04088