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

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

Почему внимание не видит порядок слов, как устроено синусоидальное кодирование 2017 года и как RoPE Су Цзяньлиня превращает позицию в поворот, после которого оценка внимания зависит от позиций только через расстояние между токенами.

УРОКstage-4/4-5-transformer/02-positional-encoding.mdx2 чУрок 2 из 4
После урока вы сможете
  • Показать в коде, что self-attention без позиций не отличает «мать любит дочь» от «дочь любит мать»
  • Реализовать синусоидальное кодирование и RoPE на NumPy и проверить свойство относительности
  • Вывести, почему после поворота RoPE оценка q⋅kq \cdot k зависит от позиций только через m−nm - n
  • Объяснить, почему RoPE стал стандартом и что меняет база θ\theta (10 000, 500 000, 1 000 000)

В китайском языке нет падежей, и смысл фразы держится на порядке слов. «Собака кусает человека» — обычная новость, «человек кусает собаку» — сенсация, а по-китайски обе фразы записываются одними и теми же тремя иероглифами, только в разном порядке. В русском падежи часто выручают («собаку кусает человек» понятно в любом порядке), но не всегда: в «мать любит дочь» и «дочь любит мать» слова и даже их формы одни и те же, и кто кого любит, решает только порядок.

А теперь неприятный факт: блок трансформера из прошлого урока не видит порядок. Внимание сравнивает каждый токен с каждым через q⋅kq \cdot k, и в этой формуле нет номеров позиций. FFN и нормализация работают с каждым токеном отдельно. Проверим:

import numpy as np
rng = np.random.default_rng(0)
d = 8
emb = {w: rng.normal(size=d) for w in ["мать", "любит", "дочь"]} # случайный эмбеддинг каждого слова
W_q, W_k, W_v = (rng.normal(size=(d, d)) / np.sqrt(d) for _ in range(3))
def attention(X):
"""Self-attention без маски и без позиций."""
Q, K, V = X @ W_q, X @ W_k, X @ W_v
S = Q @ K.T / np.sqrt(d)
A = np.exp(S - S.max(axis=1, keepdims=True))
return (A / A.sum(axis=1, keepdims=True)) @ V
def sinusoidal(T, d, base=10000.0):
"""Синусоидальные коды позиций (разберём ниже)."""
ang = np.arange(T)[:, None] * base ** (-np.arange(0, d, 2) / d)
pe = np.zeros((T, d))
pe[:, 0::2], pe[:, 1::2] = np.sin(ang), np.cos(ang)
return pe
X1 = np.stack([emb[w] for w in ["мать", "любит", "дочь"]]) # мать любит дочь
X2 = np.stack([emb[w] for w in ["дочь", "любит", "мать"]]) # дочь любит мать
y1, y2 = attention(X1), attention(X2)
print("без позиций: выход «любит» одинаков:", np.allclose(y1[1], y2[1]))
print("без позиций: выход «мать» одинаков:", np.allclose(y1[0], y2[2]))
pe = sinusoidal(3, d)
z1, z2 = attention(X1 + pe), attention(X2 + pe)
print("с позициями: выход «любит» одинаков:", np.allclose(z1[1], z2[1]))
без позиций: выход «любит» одинаков: True
без позиций: выход «мать» одинаков: True
с позициями: выход «любит» одинаков: False

Без позиций глагол «любит» получает один и тот же вектор в обеих фразах, а «мать» — тот же вектор, где бы она ни стояла. Математически это называется эквивариантностью к перестановкамэквивариантность к перестановкамEN permutation equivarianceПереставили входные токены — выходы переставились так же: Attn(PX) = P·Attn(X). Без позиций внимание видит текст как мешок токенов («собака кусает человека» = «человек кусает собаку»).: если переставить строки входа матрицей перестановки PP, выход переставится так же, Attn(PX)=P Attn(X)\mathrm{Attn}(PX) = P\,\mathrm{Attn}(X). Для внимания текст — мешок токенов. Нужно позиционное кодированиепозиционное кодированиеEN positional encoding.

Хотим к каждому токену приложить «метку» его места. Требования:

  • разные позиции — разные коды, одинаковые для любого текста;
  • значения ограничены, чтобы не заглушать эмбеддинги (их компоненты порядка единицы);
  • расстояние легко «вычислить»: модели важнее «на 3 токена назад», чем «на позиции 1027»;
  • работает на длинах, которых не было при обучении (хотя бы немного).

Наивные варианты проваливаются:

ИдеяПримерПроблема
номер как число0, 1, …, 4095числа в тысячи раз больше компонент эмбеддинга; длины длиннее обучающих — незнакомые числа
доля длиныpos/Tpos / Tкод 0.5 означает позицию 5 в тексте из 10 токенов и позицию 500 в тексте из 1000
обучаемая таблицаобучаемые позиционные эмбеддингиEN learned (absolute) position embeddingsТаблица векторов по одному на позицию, обучается вместе с моделью (BERT — 512 позиций, GPT-2 — 1024). За пределами таблицы позиций просто нет.GPT-2: 1024×7681024 \times 768позиции 1025 просто нет; 786 432 параметра на то, что можно задать формулой

Обучаемая таблица — это абсолютное кодированиеабсолютное позиционное кодированиеEN absolute positional encodingКаждый токен получает метку своего номера в тексте (синусоиды, обучаемая таблица). Расстояние между токенами модель должна вывести сама., как и синусоиды ниже: токен получает метку своего номера. Ею пользовались BERT (512 позиций) и GPT-2 (1024).

Авторы трансформера предложили код из синусов и косинусов разной частоты:

PE(pos,2i)=sin⁡(pos⋅θi),PE(pos,2i+1)=cos⁡(pos⋅θi),θi=10000−2i/d\begin{gathered} PE(pos, 2i) = \sin(pos \cdot \theta_i), \\ PE(pos, 2i+1) = \cos(pos \cdot \theta_i), \\ \theta_i = 10000^{-2i/d} \end{gathered}

Что здесь написано: вектор позиции разбит на d/2d/2 пар. Пара ii — это точка (sin⁡,cos⁡)(\sin, \cos) на единичной окружности, которая с каждой позицией сдвигается на угол θi\theta_i. Первая пара бежит быстрее всех (θ0=1\theta_0 = 1 радиан за токен), последняя почти стоит. Это синусоидальное кодированиесинусоидальное позиционное кодированиеEN sinusoidal positional encodingИз статьи 2017 года: PE(pos, 2i) = sin(pos·θ_i), PE(pos, 2i+1) = cos(pos·θ_i), θ_i = 10000^(−2i/d). Прибавляется к эмбеддингам один раз на входе.; его прибавляют к эмбеддингам токенов на входе модели.

Пример для d=8d = 8 (частоты θ=1, 0.1, 0.01, 0.001\theta = 1,\ 0.1,\ 0.01,\ 0.001):

posпара 0пара 1пара 2пара 3
0(0,1)(0, 1)(0,1)(0, 1)(0,1)(0, 1)(0,1)(0, 1)
1(0.841,0.540)(0.841, 0.540)(0.100,0.995)(0.100, 0.995)(0.010,1.000)(0.010, 1.000)(0.001,1.000)(0.001, 1.000)
2(0.909,−0.416)(0.909, -0.416)(0.199,0.980)(0.199, 0.980)(0.020,1.000)(0.020, 1.000)(0.002,1.000)(0.002, 1.000)
50(−0.262,0.965)(-0.262, 0.965)(−0.959,0.284)(-0.959, 0.284)(0.479,0.878)(0.479, 0.878)(0.050,0.999)(0.050, 0.999)

Интуиция — часы с d/2d/2 стрелками: секундная крутится быстро и различает соседние позиции, часовая медленно и различает далёкие. Похоже на двоичный счётчик, где младший бит меняется каждый шаг, а старший — редко. Длина волны пары ii равна 2π/θi2\pi / \theta_i: от 6.28 токена до 2π⋅10000(d−2)/d2\pi \cdot 10000^{(d-2)/d}, при d=512d = 512 — около 60 600 токенов.

Главное свойство видно из школьных формул sin⁡(a+b)\sin(a + b) и cos⁡(a+b)\cos(a + b): пара на позиции pos+kpos + k получается из пары на позиции pospos поворотом на угол kθik\theta_i, и этот поворот не зависит от pospos. Отсюда скалярное произведение кодов двух позиций зависит только от расстояния между ними (в каждой паре срабатывает формула cos⁡(a−b)=cos⁡acos⁡b+sin⁡asin⁡b\cos(a - b) = \cos a \cos b + \sin a \sin b из напоминания выше):

PE(m)⋅PE(n)=∑i(sin⁡mθisin⁡nθi+cos⁡mθicos⁡nθi)=∑icos⁡((m−n)θi)\begin{aligned} PE(m) \cdot PE(n) &= \sum_{i} \big(\sin m\theta_i \sin n\theta_i + \cos m\theta_i \cos n\theta_i\big) \\ &= \sum_{i} \cos\big((m - n)\theta_i\big) \end{aligned}

Проверка при d=64d = 64: PE(10)⋅PE(13)=PE(100)⋅PE(103)=25.587PE(10) \cdot PE(13) = PE(100) \cdot PE(103) = 25.587, а PE(m)⋅PE(m)=d/2=32PE(m) \cdot PE(m) = d/2 = 32. Вы проверите это в задании.

Код позиции прибавлен к эмбеддингу, а внимание сравнивает уже проекции:

qm⊤kn=(xm+pm)⊤WQWK⊤(xn+pn)q_m^\top k_n = (x_m + p_m)^\top W_Q W_K^\top (x_n + p_n)

Раскрыв скобки, получим четыре слагаемых: «содержание–содержание», два смешанных и «позиция–позиция». Красивое свойство «зависит только от m−nm - n» было у pm⋅pnp_m \cdot p_n, но матрицы WQWK⊤W_Q W_K^\top его портят, а смешанные слагаемые добавляют шум. К тому же позиция и смысл живут в одних и тех же координатах и мешают друг другу. Хочется, чтобы относительность была точной именно в оценке внимания.

Относительное кодированиеотносительное позиционное кодированиеEN relative positional encodingОценка внимания зависит от расстояния между токенами, а не от их номеров (Shaw и др. 2018, смещения T5, ALiBi, RoPE). вмешивается прямо в оценки внимания:

  • Shaw и соавторы (2018): к ключам добавляют обучаемый вектор расстояния (расстояния больше порога обрезаются).
  • T5 (2019): к оценке qm⋅knq_m \cdot k_n прибавляют обучаемое число, общее для «корзины» расстояний, своё у каждой головы.
  • ALiBiALiBi (линейные смещения внимания)EN Attention with Linear Biases (ALiBi)К оценке внимания прибавляется штраф, линейно растущий с расстоянием, свой наклон у каждой головы (Press и др., 2021). Использовался в BLOOM и MPT; позже уступил RoPE. (2021): штраф −sh⋅∣m−n∣-s_h \cdot |m - n|, линейно растущий с расстоянием, с фиксированным наклоном shs_h у каждой головы. Использовался в BLOOM и MPT.

У всех трёх общий недостаток: к матрице оценок T×TT \times T добавляется отдельное слагаемое для каждой пары позиций. Формула внимания перестаёт быть «чистым» softmax(QK⊤/d)V\mathrm{softmax}(QK^\top/\sqrt{d})V, и каждую такую схему приходится отдельно встраивать в оптимизированные ядра внимания. Хотелось бы получить относительность, не трогая саму формулу.

В марте 2021 года Су Цзяньлинь поставил задачу так: найти преобразование f(x,m)f(x, m), которое применяется к запросу и ключу по отдельности, каждому со своей абсолютной позицией, но после которого их скалярное произведение зависит только от разности позиций:

f(q,m)⊤f(k,n)=g(q,k,m−n)f(q, m)^\top f(k, n) = g(q, k, m - n)

Абсолютные позиции дёшевы: каждый токен обрабатывается сам по себе. Относительный результат — то, что нужно вниманию. Ответ — ротационное позиционное кодированиеротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM. (RoPE, Rotary Position Embedding).

Пусть qq и kk — векторы на плоскости. Матрица поворотаматрица поворотаEN rotation matrixR(α) = [[cos α, −sin α], [sin α, cos α]] поворачивает вектор на плоскости на угол α, не меняя длины. Главное свойство для RoPE: R(a)ᵀR(b) = R(b − a). на угол α\alpha:

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

Повернём запрос на позиции mm на угол mθm\theta, а ключ на позиции nn — на nθn\theta. Нам понадобятся два свойства поворотов. Транспонирование даёт поворот в обратную сторону: R(α)⊤=R(−α)R(\alpha)^\top = R(-\alpha) (транспонируйте матрицу выше — у синусов поменяются знаки). А два поворота подряд складываются: R(a)R(b)=R(a+b)R(a)R(b) = R(a + b). Значит,

(R(mθ) q)⊤(R(nθ) k)=q⊤R(−mθ)R(nθ) k=q⊤R((n−m)θ) k\big(R(m\theta)\, q\big)^\top \big(R(n\theta)\, k\big) = q^\top R(-m\theta) R(n\theta)\, k = q^\top R\big((n - m)\theta\big)\, k

Абсолютные позиции mm и nn исчезли, осталась только разность. Числа: q=k=(1,0)q = k = (1, 0), θ=1\theta = 1.

  • m=3m = 3, n=1n = 1: R(3)q=(−0.990, 0.141)R(3)q = (-0.990,\ 0.141), R(1)k=(0.540, 0.841)R(1)k = (0.540,\ 0.841), произведение =−0.416=cos⁡2= -0.416 = \cos 2.
  • m=10m = 10, n=8n = 8: R(10)q=(−0.839, −0.544)R(10)q = (-0.839,\ -0.544), R(8)k=(−0.146, 0.989)R(8)k = (-0.146,\ 0.989), произведение снова −0.416-0.416.

Векторы совсем другие, а оценка та же: расстояние в обоих случаях 2.

Вектор головы размера dd режем на d/2d/2 пар, и каждую пару вращаем на своей частоте, ровно как в синусоидах:

θi=b−2i/d,i=0,1,…,d/2−1,b=10 000\theta_i = b^{-2i/d}, \qquad i = 0, 1, \dots, d/2 - 1, \qquad b = 10\,000

Число bb называют базой RoPEбаза RoPEEN RoPE base (rope_theta)Число b в частотах RoPE θ_i = b^(−2i/d). Классическое значение 10 000; в длинноконтекстных моделях его увеличивают (Llama 3 — 500 000, Qwen2.5 — 1 000 000). (rope_theta в конфигах). Токен на позиции mm:

x2i′=x2icos⁡(mθi)−x2i+1sin⁡(mθi),x2i+1′=x2isin⁡(mθi)+x2i+1cos⁡(mθi)\begin{gathered} x'_{2i} = x_{2i}\cos(m\theta_i) - x_{2i+1}\sin(m\theta_i), \\ x'_{2i+1} = x_{2i}\sin(m\theta_i) + x_{2i+1}\cos(m\theta_i) \end{gathered}

Формально это блочно-диагональная матрица RmR_m (на диагонали стоят d/2d/2 блоков-поворотов 2×22 \times 2, все остальные элементы — нули, поэтому каждая пара координат вращается независимо от других), и для неё то же тождество: (Rmq)⊤(Rnk)=q⊤Rn−m k(R_m q)^\top (R_n k) = q^\top R_{n-m}\, k. Но матрицу d×dd \times d никто не строит: достаточно заранее посчитать таблицы cos⁡(mθi)\cos(m\theta_i) и sin⁡(mθi)\sin(m\theta_i) и сделать несколько поэлементных умножений.

flowchart LR
X["x токена на позиции m"] --> Q["W_Q → q"] --> RQ["RoPE: поворот на m·θ"]
X --> K["W_K → k"] --> RK["RoPE: поворот на m·θ"]
X --> V["W_V → v (без RoPE)"]
RQ --> S["оценки q·k / √d → softmax"]
RK --> S
RK --> C["KV-кэш хранит уже повёрнутые ключи"]
S --> O["взвешенная сумма v"]
V --> O
RoPE поворачивает только запросы и ключи, в каждом слое и в каждой голове

Три отличия от синусоид. RoPE ничего не прибавляет к эмбеддингам. Его применяют в каждом слое и в каждой голове, сразу после проекций WQW_Q и WKW_K. А значения vv не трогают: позиция нужна, чтобы решить, куда смотреть, а не что брать.

  1. Точная относительность. Оценка qm⋅knq_m \cdot k_n зависит от содержания и от m−nm - n, и больше ни от чего.
  2. Длина не меняется. Поворот сохраняет нормы, поэтому анализ масштаба d\sqrt{d} из урока 4.4.2 остаётся в силе.
  3. Ноль параметров. Только формула.
  4. Удобство для системы. Ключ поворачивается на угол своей абсолютной позиции один раз, кладётся в KV-кэш и больше не меняется. Ядро внимания (в том числе FlashAttention) получает обычные qq и kk — никаких добавочных слагаемых.
  5. Затухание с расстоянием. Су Цзяньлинь показал, что при RoPE верхняя граница оценки убывает с ростом ∣m−n∣|m - n|. Для иллюстрации возьмём q=k=(1,…,1)q = k = (1, \dots, 1), d=128d = 128, b=10 000b = 10\,000 и посчитаем нормированную оценку 164∑icos⁡((m−n)θi)\frac{1}{64}\sum_i \cos\big((m - n)\theta_i\big):
Расстояние m−nm - n01010010005000
Оценка1.000.670.480.16≈ 0

Убывание не строго монотонное (на больших расстояниях оценка колеблется около нуля), но тенденция ясна. Далёкие токены по умолчанию влияют слабее — разумное предположение «по умолчанию» (априорное) для текста. Модель может его преодолеть, если содержание qq и kk совпадает сильно.

Частоты RoPE разбивают голову на «специализации». При d=128d = 128 и b=10 000b = 10\,000 пара 0 делает полный оборот за 6.3 токена: она чувствительна к локальному порядку («предыдущее слово»). Самая медленная пара имеет длину волны около 54 тыс. токенов: на контексте 4096 токенов она поворачивается всего на 27°, то есть почти не несёт информации о позиции и служит скорее «смысловым» каналом.

Чем длиннее контекст, на котором учат модель, тем больше берут базу, чтобы медленные пары не успевали сделать полный оборот и далёкие позиции не путались:

МодельБаза bbРазмер вращаемой части головыСамая длинная волна, токенов
LLaMA 1/210 000128≈ 54 тыс.
Llama 3500 000128≈ 2.56 млн
Qwen2.5, Qwen31 000 000128≈ 5.06 млн
MiniMind (minimind-3)1 000 00096≈ 4.71 млн
DeepSeek-V310 000 + YaRN64 (развязанная часть)≈ 47 тыс. до растяжения

Сам по себе RoPE не экстраполирует: модель, обученная на 4K токенов, на 16K обычно ломается, потому что видит углы, которых не было при обучении. Лечат это управлением частотами — Position Interpolation, NTK-aware, YaRN. Это тема модуля 5.5: в китайском сообществе её называют экстраполяция по длинеэкстраполяция по длинеEN length extrapolationСпособность модели работать на последовательностях длиннее тех, на которых её обучали. В китайском сообществе так называют всю тему расширения контекста.. А почему у DeepSeek RoPE применяется только к 64 измерениям головы, объясняет урок о MLA.

Какие координаты считать парой? В статье RoFormer и исходном коде Llama пары соседние — (0,1),(2,3),…(0, 1), (2, 3), \dots (interleaved). В Hugging Face transformers (Llama, Qwen), GPT-NeoX и MiniMind пары — (i,i+d/2)(i, i + d/2), первая половина вектора с второй, через функцию rotate_half (half). Математически это одно и то же с точностью до перестановки координат, а перестановку можно «спрятать» в сами веса: переставить выходы WQW_Q и WKW_K (в PyTorch это строки weight у q_proj и k_proj). Именно это делает скрипт конвертации весов Llama из формата Meta в формат Hugging Face. Проверим:

import torch
torch.manual_seed(0)
def rope_cache(T, d, base=10000.0):
"""cos и sin формы (T, d/2): угол pos · θ_i для каждой позиции и каждой пары."""
theta = base ** (-torch.arange(0, d, 2).float() / d) # (d/2,) частоты θ_i
# outer — внешнее произведение: angles[pos, i] = pos * θ_i
angles = torch.outer(torch.arange(T).float(), theta) # (T, d/2)
return angles.cos(), angles.sin()
def rope_interleaved(x, cos, sin):
"""Пары (0,1), (2,3), … — как в статье RoFormer. x: (..., T, d)."""
# «...» в индексе — «все ведущие оси как есть» (B, H); 0::2 — чётные координаты, 1::2 — нечётные
x1, x2 = x[..., 0::2], x[..., 1::2]
# формулы поворота пары из текста: x' = x1·cos − x2·sin, y' = x1·sin + x2·cos
out = torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1) # (..., T, d/2, 2)
return out.flatten(-2) # склеить две последние оси: пары снова встают рядом, (..., T, d)
def rope_half(x, cos, sin):
"""Пары (i, i + d/2) — как в Hugging Face (rotate_half), Qwen, MiniMind."""
x1, x2 = x.chunk(2, dim=-1) # первая и вторая половины
return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
B, H, T, d = 1, 2, 6, 8
cos, sin = rope_cache(T, d)
q, k = torch.randn(B, H, T, d), torch.randn(B, H, T, d)
# 1) Относительность: оценки при сдвиге всех позиций на 100 не меняются
cos100, sin100 = rope_cache(T + 100, d) # таблица на 106 позиций; срез [100:] — позиции 100…105
s = rope_interleaved(q, cos, sin) @ rope_interleaved(k, cos, sin).transpose(-1, -2)
s_shift = rope_interleaved(q, cos100[100:], sin100[100:]) @ rope_interleaved(k, cos100[100:], sin100[100:]).transpose(-1, -2)
print("сдвиг позиций на 100 не меняет оценки:", torch.allclose(s, s_shift, atol=1e-4))
# 2) Две раскладки — одно и то же с точностью до перестановки координат
perm = torch.cat([torch.arange(0, d, 2), torch.arange(1, d, 2)]) # [0, 2, 4, 6, 1, 3, 5, 7]
a = rope_interleaved(q, cos, sin)[..., perm] # повернули «по-статейному», потом переставили
b = rope_half(q[..., perm], cos, sin) # переставили, потом повернули «по-HF»
print("interleaved и half совпадают после перестановки:", torch.allclose(a, b, atol=1e-6))
# 3) А без перестановки — нет (типичный баг при конвертации весов)
print("без перестановки совпадают:", torch.allclose(rope_interleaved(q, cos, sin), rope_half(q, cos, sin), atol=1e-6))
сдвиг позиций на 100 не меняет оценки: True
interleaved и half совпадают после перестановки: True
без перестановки совпадают: False

Если вы загрузили чужие веса, а модель генерирует бессвязный текст, хотя loss при обучении был нормальным, — проверьте раскладку RoPE одной из первых.

ГодКтоКак кодировали позицию
2017Transformerсинусоиды, прибавленные к эмбеддингам
2018BERT, GPTобучаемая таблица позиций
2018–2019Shaw и соавт., T5относительные смещения в оценках внимания
март 2021Су Цзяньлинь, блог kexue.fmRoPE
апрель 2021статья RoFormer (arXiv:2104.09864)RoPE для англоязычного мира
2021GPT-J (EleutherAI)одна из первых крупных открытых моделей с RoPE
2021–2022ALiBi; BLOOMлинейный штраф за расстояние
2022GPT-NeoX-20B, PaLM, GLM-130BRoPE
2023LLaMA, Qwen, MistralRoPE становится стандартом открытых LLM
2024–2026DeepSeek-V2/V3, Qwen2-VL, Qwen3, Kimi K2, MiniMindRoPE, в том числе развязанный (MLA у DeepSeek и Kimi) и многомерный (M-RoPE у Qwen2-VL)

Почему победил именно он:

  • Относительность без параметров и без изменения ядра внимания. Всё, что нужно, — повернуть qq и kk перед обычным scaled dot-product attention. FlashAttention, KV-кэш, тензорный параллелизм (модуль 5.4) работают без изменений.
  • Проверка масштабом. Открытые GPT-J и GPT-NeoX, закрытая PaLM от Google, затем LLaMA выбрали RoPE и хорошо обучились; дальше выбор закрепился сам, как это бывает со стандартами.
  • Управляемые частоты. Контекст можно растягивать, меняя частоты, без переобучения с нуля (модуль 5.5). С обучаемой таблицей это невозможно: позиций за её краем просто нет.
  • Обобщается на другие оси. Пары можно вращать по высоте и ширине картинки или по времени видео. Су Цзяньлинь описал двумерный RoPE в 2021 году, а Qwen2-VL использует M-RoPE для текста, картинок и видео (ступень 9).

Реализуйте rope_frequencies, sinusoidal_encoding и apply_rope на NumPy. Тесты проверят известные значения, сохранение длины, работу с формой (B,H,T,d)(B, H, T, d) и главное — что оценка после RoPE зависит только от расстояния.

Синусоиды и RoPE на NumPynumpy · нужен интернет8 тестов
import numpy as np


def rope_frequencies(d, base=10000.0):
    """Частоты вращения θ_i = base^(-2i/d) для i = 0, 1, …, d/2 - 1.

    Возвращает массив формы (d/2,). Для d = 8 и base = 10000: [1, 0.1, 0.01, 0.001].
    """
    # TODO: np.arange(d // 2) и возведение в степень
    ...


def sinusoidal_encoding(T, d, base=10000.0):
    """Синусоидальное позиционное кодирование формы (T, d).

    Строка pos — код позиции pos. Столбец 2i = sin(pos · θ_i), столбец 2i+1 = cos(pos · θ_i),
    где θ_i — частоты из rope_frequencies(d, base).
    """
    # TODO: матрица углов (T, d/2) = позиции[:, None] * частоты[None, :],
    #       затем заполнить чётные столбцы синусами, нечётные — косинусами (срезы 0::2 и 1::2)
    ...


def apply_rope(x, positions, base=10000.0):
    """Применяет RoPE к x формы (..., T, d) — например (T, d) или (B, H, T, d).

    positions — список или массив из T номеров позиций (приведите его через np.asarray).
    Пара координат (x[..., 2i], x[..., 2i+1]) токена на позиции pos поворачивается на угол pos · θ_i:
        x'[2i]   = x[2i] · cos − x[2i+1] · sin
        x'[2i+1] = x[2i] · sin + x[2i+1] · cos
    Исходный x менять нельзя — верните новый массив.
    """
    # TODO
    ...


# Проверьте себя кнопкой «Запустить»:
print(rope_frequencies(8))                          # ожидается [1.    0.1   0.01  0.001]
pe = sinusoidal_encoding(3, 4)
print(None if pe is None else pe.round(3))          # строка 0: [0, 1, 0, 1]
r = apply_rope(np.array([[1.0, 0.0]]), [1])
print(r)                                            # поворот на 1 радиан: [[0.5403 0.8415]]
if r is not None:
    rng = np.random.default_rng(0)
    q, k = rng.normal(size=(1, 8)), rng.normal(size=(1, 8))
    s1 = apply_rope(q, [5]) @ apply_rope(k, [2]).T      # позиции 5 и 2: расстояние 3
    s2 = apply_rope(q, [105]) @ apply_rope(k, [102]).T  # позиции 105 и 102: то же расстояние
    print(s1.item(), s2.item())                         # должны совпасть

После задания посмотрите, как то же самое написано в настоящем проекте: в MiniMind (model/model_minimind.py) функция precompute_freqs_cis строит таблицы cos и sin для 32 768 позиций с базой 10610^6, а apply_rotary_pos_emb поворачивает qq и kk через rotate_half — раскладка half из раздела выше.

Проверь себя6 вопросов
1 Self-attention без позиционного кодирования и без маски обработал «мать любит дочь» и «дочь любит мать». Что верно?
2 Синусоидальное кодирование, d=8d = 8, база 10 000. Чему равна компонента PE(pos=2,2i=2)PE(pos = 2, 2i = 2), то есть sin⁡(2⋅θ1)\sin(2 \cdot \theta_1)? Округлите до трёх знаков.
3 RoPE при d=2d = 2, θ=1\theta = 1: запрос q=(1,0)q = (1, 0) на позиции m=5m = 5, ключ k=(1,0)k = (1, 0) на позиции n=2n = 2. Чему равна оценка (Rmq)⋅(Rnk)(R_m q) \cdot (R_n k)? Округлите до двух знаков.
4 Какие утверждения о RoPE верны?Можно выбрать несколько ответов
5 Вы загрузили веса, обученные с RoPE в раскладке interleaved, в код с rotate_half, не переставив строки WQW_Q и WKW_K. Что произойдёт?
6 Частота пары RoPE: θi=b−2i/d\theta_i = b^{-2i/d}. Чему равна θ1\theta_1 при d=4d = 4 и базе b=10 000b = 10\,000?
  • Внимание эквивариантно к перестановкам: без позиций «мать любит дочь» и «дочь любит мать» неразличимы. Порядок нужно сообщить явно.
  • Синусоидальное кодирование (2017) — часы с d/2d/2 стрелками разной скорости; сдвиг позиции — это поворот каждой пары, поэтому PE(m)⋅PE(n)PE(m) \cdot PE(n) зависит от m−nm - n. Но после сложения с эмбеддингом и проекций это свойство размывается.
  • RoPE поворачивает каждую пару координат qq и kk на угол mθim\theta_i. Из Rm⊤Rn=Rn−mR_m^\top R_n = R_{n-m} следует точная относительность оценки внимания — без параметров и без изменения ядра внимания.
  • Стандарт индустрии: GPT-J, PaLM, LLaMA, Qwen, DeepSeek, Kimi. База bb растёт вместе с длиной контекста (10 000 → 500 000 → 1 000 000), а растяжение за пределы обучающей длины — тема модуля 5.5.
  • Две раскладки (interleaved и half) эквивалентны только с переставленными весами — частый источник багов.

Дальше: соберём все современные детали вместе — RMSNorm, SwiGLU, RoPE, GQA, отсутствие смещений — и сравним блок GPT-2 с блоками Llama 3, Qwen3 и DeepSeek-V3: Современный блок LLM.