Расширение контекста — PI, NTK, YaRN
Почему модель, обученная на 4K токенов, ломается на 16K, и как правкой частот RoPE (PI, NTK-aware, YaRN) DeepSeek и Qwen растягивают контекст до 128K и дальше за доли процента бюджета претрейна.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Длинный контекстдлинный контекстEN long context стал обязательной частью современных LLM. Qwen3 «из коробки» работает с 32K токенов и растягивается до 128K, DeepSeek-V3 — со 128K, Qwen2.5-1M и MiniMax-01 — с миллионом. Агенту для работы с кодом нужно держать в голове весь репозиторий, юристу — договор на 300 страниц.
Но обучать модель на 128K токенов с первого шага — разорительно. Внимание стоит (квадратичная сложность вниманияквадратичная сложность вниманияEN quadratic complexity (O(n²))Каждый из n токенов сравнивается с каждым — n² оценок на голову. При длинном контексте это главная стоимость внимания; отсюда FlashAttention, разреженное и линейное внимание., урок 4.4.3), а большинство документов в корпусе всё равно короче нескольких тысяч токенов. Поэтому все делают одинаково: претрейн на коротком контексте (4K–32K), затем короткое дообучение на длинном. DeepSeek-V3 прошла 14.8 трлн токенов на длине 4K, а расширение до 128K стоило около 126 млрд токенов — 0.85% бюджета.
Чтобы такое дообучение вообще сработало, нужно правильно «растянуть» позиционное кодирование. Этим и занимается урок: почему наивное удлинение ломает модель и как три приёма — PI, NTK-aware и YaRN — это исправляют. В китайском сообществе вся тема называется «экстраполяцией по длине»экстраполяция по длинеEN length extrapolationСпособность модели работать на последовательностях длиннее тех, на которых её обучали. В китайском сообществе так называют всю тему расширения контекста..
Напоминание: RoPE как часы с 64 стрелками
Заголовок раздела «Напоминание: RoPE как часы с 64 стрелками»В уроке 4.5.2 мы разобрали ротационные позиционные эмбеддингиротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM.. Вектор запроса (и ключа) размера делится на пар координат. Пару номер на позиции поворачивают на угол , где
Здесь — база 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 и любят.
Удобно представить часы с 64 стрелками (для ). Пара 0 поворачивается на 1 радиан за токен: полный оборот за токена, как секундная стрелка. Последняя пара — очень медленная «часовая». Время полного оборота называют длиной волны (частота и период волны — урок 1.0.4):
| Пара | Частота | Длина волны , токенов | Оборотов за 4096 токенов |
|---|---|---|---|
| 0 | 1 | 6.3 | 652 |
| 16 | 0.1 | 63 | 65 |
| 32 | 0.01 | 628 | 6.5 |
| 46 | 0.0013 | 4 712 | 0.87 |
| 63 | 0.000115 | 54 410 | 0.075 |
Посмотрите на последний столбец. Быстрые пары за обучающие 4096 токенов провернулись десятки и сотни раз и видели все возможные углы. А пары с 46-й по 63-ю (18 пар из 64) не сделали ни одного полного оборота.
Почему модель ломается за границей обучающей длины
Заголовок раздела «Почему модель ломается за границей обучающей длины»Возьмите модель, обученную на 4K, и подайте ей 16K токенов. Perplexity ровная до 4096, а сразу за границей резко взлетает, и модель начинает выдавать бессмыслицу. Причин две.
1. Невиданные углы. Пара 63 за всё обучение повернулась максимум на радиана. Её веса выучили «мир», в котором угол этой пары лежит между 0 и 0.47. На позиции 16 000 угол будет 1.85 — почти в четыре раза больше всего, что модель видела. Это как человек, который видел часы только между часом и половиной второго: покажите ему девять вечера — и он не поймёт, что происходит. Для быстрых пар всё в порядке: позиция 16 000 даёт угол, который уже встречался тысячи раз (углы повторяются через ).
2. Размывание внимания. Чем больше ключей, тем сильнее softmax «размазывает» веса: энтропия внимания растёт примерно как . Модель, привыкшая выбирать среди 4 тысяч токенов, на 16 тысячах смотрит «расфокусированно». Мы обсуждали это в уроке 4.4.2 вместе со статьёй Су Цзяньлиня об энтропии и множителе .
Значит, рецепт расширения должен (а) не выводить медленные пары за обученный диапазон углов и (б) вернуть вниманию прежнюю «резкость». Первую задачу решают PI и NTK, обе — YaRN.
Метод 1: Position Interpolation — сжать линейку
Заголовок раздела «Метод 1: Position Interpolation — сжать линейку»Летом 2023 года энтузиаст kaiokendev (проект SuperHOT) и группа Meta (Chen et al.) независимо предложили одно и то же. Если модель не умеет экстраполировать на невиданные позиции, давайте интерполировать: позицию заменим на , где — во сколько раз растягиваем контекст. Позиции от 0 до превращаются в числа от 0 до , и все углы остаются в обученном диапазоне. Для частот это то же самое, что поделить каждую на :
интерполяция позицийинтерполяция позиций (PI)EN position interpolation (PI)Позиции делятся на коэффициент растяжения s, и все углы RoPE остаются в обученном диапазоне. В китайских текстах — дословный перевод того же термина. работает поразительно хорошо: Meta растянула LLaMA до 32 768 токенов, дообучив её не более чем за 1000 шагов. Но у метода есть цена — разрешение.
Пара 0 теперь поворачивается не на 1, а на 0.25 радиана за токен (при ). Соседние токены стали в 4 раза «ближе» друг к другу по самой точной стрелке. Представьте линейку, которую сжали вчетверо: миллиметровые деления превратились в четвертинки миллиметра, и их трудно различить. Ниже в практике мы посчитаем: разница между оценкой внимания соседнего токена и оценкой токена «с самим собой» у PI в 16 раз меньше, чем у исходного RoPE. Без дообучения модель путает порядок близких слов, с дообучением — заново учится различать мелкие углы.
Метод 2: NTK-aware — поменять систему счисления
Заголовок раздела «Метод 2: NTK-aware — поменять систему счисления»Через несколько дней после PI пользователь Reddit bloc97 заметил: быстрым парам помощь не нужна, они и так видели все углы. Страдают только медленные. Значит, надо экстраполировать быстрые частоты и интерполировать медленные. Сделать это можно одной строкой — увеличить базу RoPE:
Проверим края. При множитель — самая быстрая пара не изменилась. При показатель равен , и самая медленная частота делится ровно на , как в PI. Между ними множитель плавно убывает. Для , новая база .
Метод назвали NTK-awareNTK-aware масштабирование RoPEEN NTK-aware scaled RoPEВместо позиций увеличивают базу RoPE в s^(d/(d−2)) раз — быстрые частоты почти не меняются, медленные интерполируются. — это отсылка к теории нейронного касательного ядра (NTK): сетям трудно выучивать высокочастотные зависимости, поэтому сжимать быстрые частоты, как PI, вредно. По словам автора, LLaMA 7B с такой правкой работала на 8K+ токенах без всякого дообучения.
Интуиция Су Цзяньлиня: RoPE — это система счисления
Заголовок раздела «Интуиция Су Цзяньлиня: RoPE — это система счисления»Лучшее объяснение NTK-aware написал Су Цзяньлинь в статье «RoPE — это кодирование в β-ичной системе». Позицию можно записать в системе счисления с основанием : младшая цифра меняется на каждом шаге, старшие — всё реже. RoPE устроен так же: быстрые пары — «младшие цифры», медленные — «старшие», и .
Теперь аналогия. Трёхзначный десятичный счётчик считает до 999. Как досчитать до 3999, не добавляя цифр?
- PI: сжать шкалу — каждый реальный шаг сдвигает счётчик на 0.25. Числа до 3999 уложатся в диапазон 0–999.75, но младший разряд теперь меняется на четверть деления за шаг: соседние числа становятся трудно различимы.
- NTK-aware: перейти в шестнадцатеричную систему. Три цифры дают значений, а младший разряд по-прежнему меняется на 1 за шаг. Локальная точность не пострадала, старшие разряды просто «растянулись». Цена — «цифры» 10–15, которых счётчик раньше не видел. В младших разрядах это не страшно, они и так перебрали все положения, а в старших это и есть те «невиданные углы», которые NTK-aware прячет не до конца.
Цена и варианты
Заголовок раздела «Цена и варианты»NTK-aware не идеален. В практике ниже вы увидите, что при у 17 медленных пар углы всё же выходят за виденный диапазон — до 1.45 раза. Поэтому без дообучения коэффициент обычно берут с запасом. Есть и Dynamic NTK: коэффициент пересчитывают под текущую длину, , и короткие тексты вообще не затрагиваются. Так в 2023 году растягивала контекст первая Qwen (подробнее — в «Китайском угле»).
Ещё проще поступают при обучении: сразу берут большую базу и дообучают на длинных текстах. Этот приём называют ABF (adjusted base frequency). Code Llama взяла , Llama 3 — , Qwen2.5 и Qwen3 поднимают базу с до на этапе с длиной 32K, Qwen2.5-1M — до . У Су Цзяньлиня есть отдельная статья о выборе базы («Принципы выбора базы RoPE»): чем длиннее нужный контекст, тем больше должна быть база.
Метод 3: YaRN — каждой частоте свой режим
Заголовок раздела «Метод 3: YaRN — каждой частоте свой режим»YaRN (Yet another RoPE extensioN; Peng, Quesnelle, Fan, Shippole, 2023; среди авторов и сам bloc97) соединяет обе идеи аккуратнее. Каждая пара сама решает, интерполироваться ей или нет, по числу оборотов за обучающую длину :
Функция «обрезает» число до отрезка от 0 до 1: всё меньше нуля становится 0, всё больше
единицы — 1 (np.clip в NumPy). Что здесь написано:
- (у LLaMA и DeepSeek ): пара сделала больше 32 оборотов, она видела все углы. , частота не меняется.
- (): пара не сделала ни одного оборота. , частота делится на , как в PI.
- Между ними — плавный переход по линейной «рампе».
Пример для пары 32 из таблицы выше (, длина волны 628 токенов) при : , , новая частота — пара замедлилась, но не вчетверо, как в PI, а примерно в 2.6 раза.
Эта часть называется NTK-by-parts. Для , , получается 21 нетронутая пара (длина волны меньше 128 токенов), 25 пар в переходной зоне и 18 интерполированных — ровно тех медленных, что не успели сделать оборот.
flowchart LR
P["Пара i: оборотов за L токенов<br/>r = L·θ/2π"] --> Q{"r ?"}
Q -->|"r > 32"| A["быстрая: θ без изменений"]
Q -->|"1 ≤ r ≤ 32"| B["средняя: смесь θ и θ/s"]
Q -->|"r меньше 1"| C["медленная: θ/s, как в PI"]Вторая часть YaRN — температура внимания. Против размывания внимания логиты делят на , где
На практике таблицы и RoPE умножают на . Этот множитель попадает и в , и в , поэтому логиты растут в раз, а на инференсе это ничего не стоит (в коде DeepSeek-V3 тот же квадрат множителя просто вшит в масштаб softmax). При множитель логитов 1.30, при — 1.87. Коэффициент 0.1 подобран авторами эмпирически.
По данным статьи, YaRN требует в 10 раз меньше токенов и в 2.5 раза меньше шагов дообучения, чем предыдущие методы,
и растягивает Llama 2 до 128K. Похожую схему по частям использует Llama 3.1 (тип llama3 в rope_scaling).
| Метод | Что меняет | Быстрые пары | Медленные пары | Дообучение | Где встречается |
|---|---|---|---|---|---|
| Без изменений | ничего | как были | невиданные углы | не помогает | — |
| PI | позиции | сжаты в раз | в диапазоне | ~1000 шагов | ранние расширения LLaMA |
| NTK-aware | базу | почти как были | частично в диапазоне | можно без него | llama.cpp, Qwen (2023, динамический вариант) |
| ABF | базу (сильно), с обучением | как были | в диапазоне после обучения | обязательно | Code Llama, Qwen, Llama 3 |
| YaRN | частоты по частям + температура | как были | в диапазоне | сотни шагов | DeepSeek-V2/V3, Qwen |
Как расширяют контекст DeepSeek и Qwen
Заголовок раздела «Как расширяют контекст DeepSeek и Qwen»Обе лаборатории растягивают контекст поэтапно: короткий претрейн, затем несколько ступенек длины.
flowchart TB subgraph D["DeepSeek-V3"] direction LR d1["Претрейн<br/>14.8T токенов, 4K"] --> d2["YaRN s=40<br/>1000 шагов, 32K"] --> d3["1000 шагов<br/>128K"] end subgraph Q["Qwen2.5 / Qwen3"] direction LR q1["Претрейн, 4K<br/>база 10^4"] --> q2["Длинный этап, 32K<br/>база 10^6 (ABF)"] --> q3["Инференс: YaRN x4 + DCA<br/>до 128K"] end
DeepSeek-V3 после претрейна на 4K делает два этапа по 1000 шагов. Первый — длина 32K, батч 1920 последовательностей. Второй — длина 128K, батч 480. Каждый этап — около 62.9 млрд токенов ( и ). Параметры YaRN одинаковы: , , , . В опубликованном конфиге:
"rope_scaling": {"type": "yarn", "factor": 40, "original_max_position_embeddings": 4096, "beta_fast": 32, "beta_slow": 1, "mscale": 1.0, "mscale_all_dim": 1.0}— формальный потолок с запасом над 128K. Тонкость, связанная с уроком 5.2.2: в MLA позиции несёт только развязанная RoPE-часть ключа (64 измерения из 192), поэтому YaRN применяется только к ней.
Qwen идёт двумя дорогами одновременно. При обучении — ABF: Qwen2.5 и Qwen3 проходят основной претрейн на 4K, а на
завершающем этапе удлиняют последовательности до 32K и поднимают базу RoPE с до . На инференсе — YaRN и
Dual Chunk Attention (DCA) дают ещё четырёхкратный запас, до 128K. В карточках моделей Qwen предлагают дописать в config.json:
"rope_scaling": {"type": "yarn", "factor": 4.0, "original_max_position_embeddings": 32768}и сразу предупреждают: многие движки поддерживают только статический YaRN, где коэффициент действует при любой длине, и на коротких текстах качество может слегка просесть. Поэтому включать его стоит, только когда длинный контекст действительно нужен.
Qwen2.5-1M доводит идею до предела: пять ступенек длины при обучении (4K → 32K → 65K → 131K → 262K) с базой , а до миллиона токенов модель дотягивается на инференсе через DCA. Идея DCA (статья HKU и Alibaba, 2024): длинную последовательность режут на куски и пересчитывают относительные расстояния так, чтобы ни одно не превышало обученного. Ближние токены сохраняют точные расстояния, дальние получают «сжатые». Для ускорения prefill на таких длинах добавлено разреженное вниманиеразреженное вниманиеEN sparse attention — о нём следующий урок.
Практика
Заголовок раздела «Практика»Задание в браузере
Заголовок раздела «Задание в браузере»Реализуйте частоты RoPE и три способа их растянуть. Тесты проверят края NTK-aware, три режима YaRN и главное свойство: на длине у PI и YaRN нет ни одной медленной пары с «невиданным» углом.
import math
import numpy as np
def rope_freqs(d, base=10000.0):
"""Частоты RoPE для головы размера d (d чётное).
Возвращает массив из d // 2 чисел: theta_i = base ** (-2 * i / d), i = 0, 1, ..., d/2 - 1.
Пара координат i поворачивается на угол m * theta_i, где m — позиция токена.
Пример: rope_freqs(8) -> [1.0, 0.1, 0.01, 0.001].
"""
# TODO: np.arange(0, d, 2) даёт 0, 2, 4, ... — это и есть 2i
...
def pi_freqs(freqs, s):
"""Position Interpolation: позиции сжимаются в s раз, m -> m / s.
Для частот это то же самое, что поделить каждую частоту на s.
Входной массив не изменяйте — верните новый.
"""
# TODO
...
def ntk_freqs(d, base, s):
"""NTK-aware: позиции не трогаем, увеличиваем базу RoPE.
Новая база: base * s ** (d / (d - 2)). Самая высокая частота остаётся 1.0,
самая низкая уменьшается ровно в s раз.
"""
# TODO: одна строка через rope_freqs
...
def yarn_freqs(freqs, s, L, alpha=1.0, beta=32.0):
"""YaRN (NTK-by-parts): каждая пара решает сама, интерполироваться ей или нет.
freqs — исходные частоты, s — во сколько раз растягиваем контекст,
L — обучающая длина контекста (в токенах).
1) r_i = L * theta_i / (2 * pi) — сколько полных оборотов пара успела сделать за L токенов;
2) gamma_i = clip((r_i - alpha) / (beta - alpha), 0, 1);
3) новая частота = (1 - gamma_i) * theta_i / s + gamma_i * theta_i.
Быстрые пары (r > beta) не меняются, медленные (r < alpha) делятся на s, средние — плавный переход.
"""
# TODO
...
def yarn_attention_factor(s):
"""Множитель YaRN для q и k: 0.1 * ln(s) + 1 при s > 1, иначе 1.0.
Он умножает и q, и k, поэтому логиты внимания растут в (0.1 ln s + 1)^2 раз —
распределение внимания становится чуть острее.
"""
# TODO
...
# Проверьте себя кнопкой «Запустить»:
np.set_printoptions(precision=6, suppress=True)
f = rope_freqs(8)
print("RoPE:", f) # ожидается [1. 0.1 0.01 0.001]
if f is not None:
print("PI x4:", pi_freqs(f, 4)) # ожидается [0.25 0.025 0.0025 0.00025]
print("NTK x4:", ntk_freqs(8, 10000.0, 4)) # [1. 0.062996 0.003969 0.00025]: первая как у RoPE, последняя как у PI
print("YaRN x4:", yarn_freqs(f, 4, L=1024)) # [1. 0.06201 0.002652 0.00025]: быстрая пара не тронута, медленная /4
print("множитель YaRN при s=40:", yarn_attention_factor(40)) # ≈ 1.3689
Сравнение методов на NumPy
Заголовок раздела «Сравнение методов на NumPy»Скрипт ниже сравнивает четыре варианта для типичной головы (, ), обученной на 4K, при растяжении до 16K. Для каждого метода он считает, сколько медленных пар получают невиданные углы, во сколько раз худший угол больше виденного и насколько оценка внимания соседнего токена отличается от оценки токена «с самим собой» (чем больше, тем легче отличить соседа).
import numpy as np
def rope_freqs(d, base=10000.0): return base ** (-np.arange(0, d, 2) / d)
def yarn_freqs(f, s, L, alpha=1.0, beta=32.0): r = L * f / (2 * np.pi) # оборотов за обучающую длину gamma = np.clip((r - alpha) / (beta - alpha), 0, 1) return (1 - gamma) * f / s + gamma * f
d, base, L, s = 128, 10000.0, 4096, 4 # голова 128, обучали на 4K, хотим 16Kf = rope_freqs(d, base)methods = { "без изменений": f, "PI": f / s, "NTK-aware": rope_freqs(d, base * s ** (d / (d - 2))), "YaRN": yarn_freqs(f, s, L),}
seen = L * f # максимальный угол каждой пары в обученииslow = seen < 2 * np.pi # пары, не сделавшие за 4K токенов ни одного оборотаprint(f"медленных пар: {slow.sum()} из {d // 2}")print(f"{'метод':<14}{'новых углов':>12}{'превышение':>12}{'различие соседей':>18}")for name, g in methods.items(): new = s * L * g # максимальный угол на длине 16K n_bad = int(np.sum(slow & (new > seen + 1e-9))) excess = np.max(new[slow] / seen[slow]) # во сколько раз худший угол больше виденного near = 1 - np.mean(np.cos(g)) # насколько оценка соседа (Δ = 1) ниже оценки «самого себя» print(f"{name:<14}{n_bad:>12}{excess:>11.2f}x{near:>18.4f}")Ожидаемый вывод:
медленных пар: 18 из 64метод новых углов превышение различие соседейбез изменений 18 4.00x 0.0298PI 0 1.00x 0.0019NTK-aware 17 1.45x 0.0264YaRN 0 1.00x 0.0298Таблица — весь урок в четырёх строках. Без изменений 18 пар уходят в неизвестность с четырёхкратным превышением. PI убирает проблему, но различие соседей падает с 0.0298 до 0.0019, в 16 раз (: косинус малого угла , урок 1.0.4). NTK-aware сохраняет разрешение, но не до конца прячет медленные пары. YaRN получает лучшее от обоих.
Зачем температура: энтропия внимания
Заголовок раздела «Зачем температура: энтропия внимания»Второй скрипт показывает размывание внимания. Логиты здесь — случайные числа с разбросом ; у настоящей модели распределение другое, но тенденция та же.
import numpy as np
rng = np.random.default_rng(0)
def attn_entropy(n, mult=1.0, sigma=2.0, trials=50): """Средняя энтропия softmax по n ключам; логиты ~ N(0, sigma²), умноженные на mult.""" out = [] for _ in range(trials): z = rng.normal(0, sigma, size=n) * mult p = np.exp(z - z.max()) p /= p.sum() out.append(-(p * np.log(p)).sum()) return np.mean(out)
m = (0.1 * np.log(4) + 1) ** 2 # YaRN, s = 4: логиты умножаются на ≈ 1.2965print(f"4K ключей: энтропия {attn_entropy(4096):.2f}")print(f"16K ключей: энтропия {attn_entropy(16384):.2f}")print(f"16K ключей + YaRN x{m:.4f}: энтропия {attn_entropy(16384, m):.2f}")Ожидаемый вывод:
4K ключей: энтропия 6.3316K ключей: энтропия 7.7216K ключей + YaRN x1.2965: энтропия 6.47Энтропия в натах: — внимание «эффективно» распределено по нескольким сотням ключей. На 16K таких ключей уже , внимание расфокусировано. Множитель YaRN возвращает его почти к исходной резкости.
Проверь себя
Заголовок раздела «Проверь себя»- RoPE-модель ломается за обучающей длиной из-за медленных пар частот, которые не сделали ни одного оборота, и из-за размывания внимания на большем числе ключей.
- PI делит позиции на : углы в диапазоне, но падает разрешение на быстрых частотах. NTK-aware увеличивает базу: быстрые частоты почти не трогаются, медленные интерполируются. ABF — то же с большой базой и дообучением.
- YaRN решает за каждую пару отдельно (по числу оборотов ) и добавляет температуру .
- DeepSeek-V3 растянула контекст с 4K до 128K двумя этапами YaRN за 0.85% бюджета. Qwen сочетает ABF при обучении с YaRN и DCA на инференсе, Qwen2.5-1M доходит до миллиона токенов.
Дальше: растянуть позиции — полдела. Внимание на 128K всё ещё стоит . В следующем уроке разберём, как DeepSeek научила модель смотреть не на все токены, — скользящее окно, NSA и DSA: Разреженное внимание.