Позиционное кодирование и RoPE
Почему внимание не видит порядок слов, как устроено синусоидальное кодирование 2017 года и как RoPE Су Цзяньлиня превращает позицию в поворот, после которого оценка внимания зависит от позиций только через расстояние между токенами.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В китайском языке нет падежей, и смысл фразы держится на порядке слов. «Собака кусает человека» — обычная новость, «человек кусает собаку» — сенсация, а по-китайски обе фразы записываются одними и теми же тремя иероглифами, только в разном порядке. В русском падежи часто выручают («собаку кусает человек» понятно в любом порядке), но не всегда: в «мать любит дочь» и «дочь любит мать» слова и даже их формы одни и те же, и кто кого любит, решает только порядок.
А теперь неприятный факт: блок трансформера из прошлого урока не видит порядок. Внимание сравнивает каждый токен с каждым через , и в этой формуле нет номеров позиций. FFN и нормализация работают с каждым токеном отдельно. Проверим:
import numpy as np
rng = np.random.default_rng(0)d = 8emb = {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). Без позиций внимание видит текст как мешок токенов («собака кусает человека» = «человек кусает собаку»).: если переставить строки входа матрицей перестановки , выход переставится так же, . Для внимания текст — мешок токенов. Нужно позиционное кодированиепозиционное кодированиеEN positional encoding.
Каким должен быть код позиции
Заголовок раздела «Каким должен быть код позиции»Хотим к каждому токену приложить «метку» его места. Требования:
- разные позиции — разные коды, одинаковые для любого текста;
- значения ограничены, чтобы не заглушать эмбеддинги (их компоненты порядка единицы);
- расстояние легко «вычислить»: модели важнее «на 3 токена назад», чем «на позиции 1027»;
- работает на длинах, которых не было при обучении (хотя бы немного).
Наивные варианты проваливаются:
| Идея | Пример | Проблема |
|---|---|---|
| номер как число | 0, 1, …, 4095 | числа в тысячи раз больше компонент эмбеддинга; длины длиннее обучающих — незнакомые числа |
| доля длины | код 0.5 означает позицию 5 в тексте из 10 токенов и позицию 500 в тексте из 1000 | |
| обучаемая таблицаобучаемые позиционные эмбеддингиEN learned (absolute) position embeddingsТаблица векторов по одному на позицию, обучается вместе с моделью (BERT — 512 позиций, GPT-2 — 1024). За пределами таблицы позиций просто нет. | GPT-2: | позиции 1025 просто нет; 786 432 параметра на то, что можно задать формулой |
Обучаемая таблица — это абсолютное кодированиеабсолютное позиционное кодированиеEN absolute positional encodingКаждый токен получает метку своего номера в тексте (синусоиды, обучаемая таблица). Расстояние между токенами модель должна вывести сама., как и синусоиды ниже: токен получает метку своего номера. Ею пользовались BERT (512 позиций) и GPT-2 (1024).
Синусоидальное кодирование (2017)
Заголовок раздела «Синусоидальное кодирование (2017)»Авторы трансформера предложили код из синусов и косинусов разной частоты:
Что здесь написано: вектор позиции разбит на пар. Пара — это точка на единичной окружности, которая с каждой позицией сдвигается на угол . Первая пара бежит быстрее всех ( радиан за токен), последняя почти стоит. Это синусоидальное кодированиесинусоидальное позиционное кодированиеEN sinusoidal positional encodingИз статьи 2017 года: PE(pos, 2i) = sin(pos·θ_i), PE(pos, 2i+1) = cos(pos·θ_i), θ_i = 10000^(−2i/d). Прибавляется к эмбеддингам один раз на входе.; его прибавляют к эмбеддингам токенов на входе модели.
Пример для (частоты ):
| pos | пара 0 | пара 1 | пара 2 | пара 3 |
|---|---|---|---|---|
| 0 | ||||
| 1 | ||||
| 2 | ||||
| 50 |
Интуиция — часы с стрелками: секундная крутится быстро и различает соседние позиции, часовая медленно и различает далёкие. Похоже на двоичный счётчик, где младший бит меняется каждый шаг, а старший — редко. Длина волны пары равна : от 6.28 токена до , при — около 60 600 токенов.
Сдвиг — это поворот
Заголовок раздела «Сдвиг — это поворот»Главное свойство видно из школьных формул и : пара на позиции получается из пары на позиции поворотом на угол , и этот поворот не зависит от . Отсюда скалярное произведение кодов двух позиций зависит только от расстояния между ними (в каждой паре срабатывает формула из напоминания выше):
Проверка при : , а . Вы проверите это в задании.
Где синусоиды не дотягивают
Заголовок раздела «Где синусоиды не дотягивают»Код позиции прибавлен к эмбеддингу, а внимание сравнивает уже проекции:
Раскрыв скобки, получим четыре слагаемых: «содержание–содержание», два смешанных и «позиция–позиция». Красивое свойство «зависит только от » было у , но матрицы его портят, а смешанные слагаемые добавляют шум. К тому же позиция и смысл живут в одних и тех же координатах и мешают друг другу. Хочется, чтобы относительность была точной именно в оценке внимания.
Относительные позиции
Заголовок раздела «Относительные позиции»Относительное кодированиеотносительное позиционное кодированиеEN relative positional encodingОценка внимания зависит от расстояния между токенами, а не от их номеров (Shaw и др. 2018, смещения T5, ALiBi, RoPE). вмешивается прямо в оценки внимания:
- Shaw и соавторы (2018): к ключам добавляют обучаемый вектор расстояния (расстояния больше порога обрезаются).
- T5 (2019): к оценке прибавляют обучаемое число, общее для «корзины» расстояний, своё у каждой головы.
- ALiBiALiBi (линейные смещения внимания)EN Attention with Linear Biases (ALiBi)К оценке внимания прибавляется штраф, линейно растущий с расстоянием, свой наклон у каждой головы (Press и др., 2021). Использовался в BLOOM и MPT; позже уступил RoPE. (2021): штраф , линейно растущий с расстоянием, с фиксированным наклоном у каждой головы. Использовался в BLOOM и MPT.
У всех трёх общий недостаток: к матрице оценок добавляется отдельное слагаемое для каждой пары позиций. Формула внимания перестаёт быть «чистым» , и каждую такую схему приходится отдельно встраивать в оптимизированные ядра внимания. Хотелось бы получить относительность, не трогая саму формулу.
RoPE: позиция как поворот
Заголовок раздела «RoPE: позиция как поворот»В марте 2021 года Су Цзяньлинь поставил задачу так: найти преобразование , которое применяется к запросу и ключу по отдельности, каждому со своей абсолютной позицией, но после которого их скалярное произведение зависит только от разности позиций:
Абсолютные позиции дёшевы: каждый токен обрабатывается сам по себе. Относительный результат — то, что нужно вниманию. Ответ — ротационное позиционное кодированиеротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM. (RoPE, Rotary Position Embedding).
Двумерный случай
Заголовок раздела «Двумерный случай»Пусть и — векторы на плоскости. Матрица поворотаматрица поворотаEN rotation matrixR(α) = [[cos α, −sin α], [sin α, cos α]] поворачивает вектор на плоскости на угол α, не меняя длины. Главное свойство для RoPE: R(a)ᵀR(b) = R(b − a). на угол :
Повернём запрос на позиции на угол , а ключ на позиции — на . Нам понадобятся два свойства поворотов. Транспонирование даёт поворот в обратную сторону: (транспонируйте матрицу выше — у синусов поменяются знаки). А два поворота подряд складываются: . Значит,
Абсолютные позиции и исчезли, осталась только разность. Числа: , .
- , : , , произведение .
- , : , , произведение снова .
Векторы совсем другие, а оценка та же: расстояние в обоих случаях 2.
Много измерений: d/2 плоскостей
Заголовок раздела «Много измерений: d/2 плоскостей»Вектор головы размера режем на пар, и каждую пару вращаем на своей частоте, ровно как в синусоидах:
Число называют базой 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 в конфигах). Токен на позиции :
Формально это блочно-диагональная матрица (на диагонали стоят блоков-поворотов , все остальные элементы — нули, поэтому каждая пара координат вращается независимо от других), и для неё то же тождество: . Но матрицу никто не строит: достаточно заранее посчитать таблицы и и сделать несколько поэлементных умножений.
Где именно применяется RoPE
Заголовок раздела «Где именно применяется RoPE»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 стандартом
Заголовок раздела «Свойства, которые сделали RoPE стандартом»- Точная относительность. Оценка зависит от содержания и от , и больше ни от чего.
- Длина не меняется. Поворот сохраняет нормы, поэтому анализ масштаба из урока 4.4.2 остаётся в силе.
- Ноль параметров. Только формула.
- Удобство для системы. Ключ поворачивается на угол своей абсолютной позиции один раз, кладётся в KV-кэш и больше не меняется. Ядро внимания (в том числе FlashAttention) получает обычные и — никаких добавочных слагаемых.
- Затухание с расстоянием. Су Цзяньлинь показал, что при RoPE верхняя граница оценки убывает с ростом . Для иллюстрации возьмём , , и посчитаем нормированную оценку :
| Расстояние | 0 | 10 | 100 | 1000 | 5000 |
|---|---|---|---|---|---|
| Оценка | 1.00 | 0.67 | 0.48 | 0.16 | ≈ 0 |
Убывание не строго монотонное (на больших расстояниях оценка колеблется около нуля), но тенденция ясна. Далёкие токены по умолчанию влияют слабее — разумное предположение «по умолчанию» (априорное) для текста. Модель может его преодолеть, если содержание и совпадает сильно.
Быстрые и медленные пары
Заголовок раздела «Быстрые и медленные пары»Частоты RoPE разбивают голову на «специализации». При и пара 0 делает полный оборот за 6.3 токена: она чувствительна к локальному порядку («предыдущее слово»). Самая медленная пара имеет длину волны около 54 тыс. токенов: на контексте 4096 токенов она поворачивается всего на 27°, то есть почти не несёт информации о позиции и служит скорее «смысловым» каналом.
Чем длиннее контекст, на котором учат модель, тем больше берут базу, чтобы медленные пары не успевали сделать полный оборот и далёкие позиции не путались:
| Модель | База | Размер вращаемой части головы | Самая длинная волна, токенов |
|---|---|---|---|
| LLaMA 1/2 | 10 000 | 128 | ≈ 54 тыс. |
| Llama 3 | 500 000 | 128 | ≈ 2.56 млн |
| Qwen2.5, Qwen3 | 1 000 000 | 128 | ≈ 5.06 млн |
| MiniMind (minimind-3) | 1 000 000 | 96 | ≈ 4.71 млн |
| DeepSeek-V3 | 10 000 + YaRN | 64 (развязанная часть) | ≈ 47 тыс. до растяжения |
Сам по себе RoPE не экстраполирует: модель, обученная на 4K токенов, на 16K обычно ломается, потому что видит углы, которых не было при обучении. Лечат это управлением частотами — Position Interpolation, NTK-aware, YaRN. Это тема модуля 5.5: в китайском сообществе её называют экстраполяция по длинеэкстраполяция по длинеEN length extrapolationСпособность модели работать на последовательностях длиннее тех, на которых её обучали. В китайском сообществе так называют всю тему расширения контекста.. А почему у DeepSeek RoPE применяется только к 64 измерениям головы, объясняет урок о MLA.
Две раскладки — один капкан
Заголовок раздела «Две раскладки — один капкан»Какие координаты считать парой? В статье RoFormer и исходном коде Llama пары соседние —
(interleaved). В Hugging Face transformers (Llama, Qwen), GPT-NeoX и MiniMind пары — , первая
половина вектора с второй, через функцию rotate_half (half). Математически это одно и то же с точностью
до перестановки координат, а перестановку можно «спрятать» в сами веса: переставить выходы и
(в 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, 8cos, 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…105s = 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 не меняет оценки: Trueinterleaved и half совпадают после перестановки: Trueбез перестановки совпадают: FalseЕсли вы загрузили чужие веса, а модель генерирует бессвязный текст, хотя loss при обучении был нормальным, — проверьте раскладку RoPE одной из первых.
Почему RoPE стал стандартом
Заголовок раздела «Почему RoPE стал стандартом»| Год | Кто | Как кодировали позицию |
|---|---|---|
| 2017 | Transformer | синусоиды, прибавленные к эмбеддингам |
| 2018 | BERT, GPT | обучаемая таблица позиций |
| 2018–2019 | Shaw и соавт., T5 | относительные смещения в оценках внимания |
| март 2021 | Су Цзяньлинь, блог kexue.fm | RoPE |
| апрель 2021 | статья RoFormer (arXiv:2104.09864) | RoPE для англоязычного мира |
| 2021 | GPT-J (EleutherAI) | одна из первых крупных открытых моделей с RoPE |
| 2021–2022 | ALiBi; BLOOM | линейный штраф за расстояние |
| 2022 | GPT-NeoX-20B, PaLM, GLM-130B | RoPE |
| 2023 | LLaMA, Qwen, Mistral | RoPE становится стандартом открытых LLM |
| 2024–2026 | DeepSeek-V2/V3, Qwen2-VL, Qwen3, Kimi K2, MiniMind | RoPE, в том числе развязанный (MLA у DeepSeek и Kimi) и многомерный (M-RoPE у Qwen2-VL) |
Почему победил именно он:
- Относительность без параметров и без изменения ядра внимания. Всё, что нужно, — повернуть и перед обычным 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. Тесты проверят известные значения,
сохранение длины, работу с формой и главное — что оценка после RoPE зависит только от расстояния.
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 позиций с базой
, а apply_rotary_pos_emb поворачивает и через rotate_half — раскладка half из раздела выше.
Проверь себя
Заголовок раздела «Проверь себя»- Внимание эквивариантно к перестановкам: без позиций «мать любит дочь» и «дочь любит мать» неразличимы. Порядок нужно сообщить явно.
- Синусоидальное кодирование (2017) — часы с стрелками разной скорости; сдвиг позиции — это поворот каждой пары, поэтому зависит от . Но после сложения с эмбеддингом и проекций это свойство размывается.
- RoPE поворачивает каждую пару координат и на угол . Из следует точная относительность оценки внимания — без параметров и без изменения ядра внимания.
- Стандарт индустрии: GPT-J, PaLM, LLaMA, Qwen, DeepSeek, Kimi. База растёт вместе с длиной контекста (10 000 → 500 000 → 1 000 000), а растяжение за пределы обучающей длины — тема модуля 5.5.
- Две раскладки (interleaved и half) эквивалентны только с переставленными весами — частый источник багов.
Дальше: соберём все современные детали вместе — RMSNorm, SwiGLU, RoPE, GQA, отсутствие смещений — и сравним блок GPT-2 с блоками Llama 3, Qwen3 и DeepSeek-V3: Современный блок LLM.