Почти все современные LLM кодируют позицию токена поворотом векторов в слое внимания. Данный подход называется RoPE (Rotary Position Embedding) и хорошо описан в статьях [1], [2]. Я коротко опишу только то, что необходимо для понимания механизма его работы.
Вектор запроса и вектор ключа состоят из пар координат, и каждую такую пару RoPE поворачивает на угол, зависящий от позиции токена в тексте. Разные пары координат крутятся с разной скоростью: одни поворачиваются быстро и успевают сделать полный оборот уже через несколько токенов, поэтому они хорошо различают соседние слова.
Другие пары крутятся медленно, делая оборот только через тысячи токенов, они отвечают за дальние связи в тексте. Вместе такие пары образуют что‑то вроде набора часов с разным шагом: от секундной стрелки, которая различает позиции 1 и 2, до часовой, которая различает позиции за тысячи токенов друг от друга.
Главное свойство RoPE в том, что при сравнении запроса и ключа имеет значение не то, на какой именно позиции стоит каждый из них, а только то, на каком расстоянии они друг от друга. Иными словами, модель не запоминает «токен на позиции N», она учится узнавать паттерн вида «что‑то похожее встречалось примерно N токенов назад».
Когда на инференсе мы подаем последовательность длиннее того окна, на котором модель обучалась, ломается сразу несколько вещей.
Во‑первых, модель начинает встречать углы поворота, которых она никогда не видела на обучении. Позиции за пределами обученного максимума дают такие комбинации поворотов пар координат, к которым модель просто не строила внимания, она с ними не тренировалась и не знает, как их интерпретировать.
Во‑вторых, на больших расстояниях начинают слипаться медленные компоненты. Быстрые пары координат совершают много оборотов, и на длинной дистанции их сигнал усредняется, поэтому различать далекие друг от друга позиции модели приходится в основном по медленным компонентам, а они на таких масштабах почти не успевают измениться и дают модели мало информации [3].
Помимо этого, с ростом длины последовательности растет и энтропия внимания: softmax размазывается по все большему числу позиций, и модели становится сложнее сфокусироваться на действительно важных токенах.
Независимо от RoPE, у языковых моделей есть особенность, похожая на человеческую память. Исследование показало, что качество работы с данными сильно зависит от их места в тексте [4].
Модели отлично воспринимают информацию в самом начале и в самом конце документа, но часто теряют то, что находится посередине. При этом чем больше становится объем текста, тем сильнее проседает качество обработки информации в центре. Думаю, многие замечали отдельный «now read in the middle» шаг в рассуждениях моделей, это попытка обойти данную проблему.
Борьба с ограничением окна представлена двумя подходами: дообучение на длинных последовательностях (дорого и долго) и модификация частот RoPE на лету (дешево и сердито).
Нас интересует второй подход, который представлен следующими методами:
Position Interpolation (PI)
Идея здесь предельно простая: если модель училась на позициях от нуля до какого‑то максимума, а нам нужно окно в несколько раз больше, то можно просто пересчитать все входящие позиции так, чтобы они снова уместились в знакомый модели диапазон [5].
Для этого каждую позицию токена домножают на дробь, где исходный обученный максимум стоит в числителе, а новая, увеличенная длина окна стоит в знаменателе. Получается, что позиции как бы сжимаются пропорционально во столько раз, во сколько мы расширяем контекст, и все углы поворота остаются в том диапазоне, который модель видела на обучении.
Проблема в том, что такое сжатие в лоб, без дообучения, давит и на быстрые, и на медленные частоты одинаково. А ведь именно быстрые осцилляции отвечают за то, чтобы модель различала соседние токены друг от друга.
Когда их тоже сжимают, локальное разрешение по позиции падает, соседние токены начинают выглядеть для модели почти одинаково, и она начинает путаться в порядке слов внутри локального окна, даже если с дальними связями все стало в порядке.
NTK‑aware scaling
Вместо того чтобы одинаково сжимать все позиции одним коэффициентом, меняется сама база, из которой считаются частоты поворота. Причем эта база не просто домножается на коэффициент растяжения контекста напрямую, а возводится в степень, зависящую от размерности головы внимания, так что чем больше нужный коэффициент расширения, тем сильнее растет итоговая база [6].
За счет такого пересчета быстрые компоненты, которые отвечают за различение соседних токенов, меняются совсем незначительно, и локальное разрешение почти не страдает. А вот медленные компоненты, наоборот, сжимаются гораздо сильнее, и именно за счет этого дальние расстояния, которые раньше выходили за пределы обученного диапазона, теперь в него влезают.
Получается, что углы поворота как бы перераспределяются между разными измерениями: одним достается почти вся нагрузка по сохранению точности на коротких дистанциях, другим достается нагрузка по растяжению на длинные дистанции. Название метода как раз отсылает к этой идее перераспределения и проведено по аналогии с NTK‑ядром из теории нейросетей.
Dynamic NTK является дальнейшим развитием идеи: масштабирование применяется только когда последовательность действительно превышает нативное окно, а коэффициент растет вместе с фактической длиной. Короткие запросы идут через нетронутые частоты и не теряют в точности.
YaRN
Этот подход объединяет идеи PI и NTK‑aware, добавляя к ним два уточнения.
Первое уточнение касается того, что для разных измерений RoPE выбирается свой режим обработки в зависимости от длины волны. Коротковолновые измерения, которые крутятся быстро, оставляют почти без изменений, чтобы не терять локальное разрешение.
Длинноволновые, медленные измерения сжимают линейно, как в PI, потому что именно на них лежит нагрузка по расширению дальнего контекста.
А промежуточные измерения плавно смешивают оба подхода, примерно как это делает NTK‑aware. В результате модель не теряет способность различать соседние токены и одновременно получает расширенный интервал для дальних связей.
Второе уточнение относится к масштабированию логитов внимания перед применением softmax с помощью специального множителя, называемого температурой (не путать с температурой сэмплирования). Он компенсирует рост энтропии распределения внимания, который возникает на длинных последовательностях, независимо от позиции токена в тексте.
Благодаря такому смешиванию YaRN дообучается заметно дешевле своих предшественников, дословно из аннотации статьи, описывающей метод: requiring 10x less tokens and 2.5x less training steps than previous methods, а в приложении отдельно разбирается динамический режим на моделях вообще без какого‑либо дообучения («Dynamic scaling on models without any fine‑tuning») [7]. Именно этот сценарий мы и будем использовать в практической части.
LongRoPE и LongRoPE2
Вместо того чтобы задавать масштаб частот по единому правилу, метод подбирает для каждой частоты RoPE свой собственный коэффициент с помощью эволюционного алгоритма (по сути это подбор удачных комбинаций перебором) [8].
Само дообучение проводится постепенно: сначала модель адаптируют к окну в 131 тысячу токенов, затем к 512 тысячам, и только потом к двум миллионам, увеличивая длину контекста ступенчато. В итоге получается модель, способная работать с контекстом до двух миллионов токенов при относительно небольшом объеме дообучения.
LongRoPE2 продолжает развитие этой идеи [9]. Здесь поиск масштабов упрощают до одномерного и придерживаются принципа «малых потерь»: качество на коротких контекстах не проседает вообще, а длинное окно расширяется поверх него. Именно техники такого рода и стоят за тем самым нативным миллионом токенов, который предлагают современные флагманские модели.
Для наших целей увеличения размера контекста на лету, без регистрации и СМС, подходят методы: YaRN и Dynamic NTK. Для сокращения объема статьи я приведу практические примеры с использованием YaRN.
За чей счет банкет?
Само RoPE‑масштабирование почти бесплатно по вычислениям, ведь это фактически просто изменение таблиц синусов/косинусов в начале инференса, но изменение масштаба не проходит бесследно.
Дело в том, что KV‑кэш растет линейно вместе с длиной последовательности. Каждый токен добавляет в кэш определенное количество чисел (ключ и значение) на каждый полный слой внимания, с учетом числа KV‑голов и размерности головы. На контексте в миллион токенов такой кэш легко разрастается до нескольких десятков гигабайт.
Но это еще не все: само внимание в режиме prefill, если оно полное, растет квадратично относительно длины последовательности. Из‑за этого обработка миллионного промпта без flash‑attention и разреженных схем внимания может занимать часы, так как вычислительная нагрузка растет непропорционально быстро с ростом длины входа.
В гибридных архитектурах все немного лучше: основную часть слоев занимают линейные механизмы вроде сверток или линейного внимания с постоянным по размеру состоянием, а полное внимание вставлено лишь изредка, раз в несколько слоев.
Благодаря этому KV‑кэш приходится держать только для этих немногочисленных полных слоев, а остальные слои никакой истории токенов в явном виде не накапливают, и их состояние не растет вместе с длиной контекста.
Я составил таблицу размеров KV‑кэша (без квантования, в FP16) для нескольких часто используемых (и хайповых) моделей:
| Модель | Полных attn‑слоев | KV‑голов × dim | 262K токенов | 1M токенов | |‑|—|‑|—|‑|—| | Qwen3.8–27B | 16 из 64 | 4 × 256 | ~17.2 GB | ~68.7 GB | | Qwen3.8-Flash‑Next (MoE) | 12 из 48 | 2 × 256 | ~6.4 GB | ~25.8 GB | | Qwen3.5–4B | 8 из 32 | 4 × 256 | ~8.6 GB | ~34.4 GB | | Gemma 4 E2B | 7 из 35 (+28 sliding) | 1 × 256 | — | ~7.5 GB | | Gemma 4 E4B | 7 из 42 (+35 sliding) | 2 × 256 | — | ~15.0 GB | | LFM2.5–2.6B | 8 из 30 (+22 conv) | 8 × 64 | — | ~17.2 GB | | MiniCPM5-1B | 24 из 24 (Llama) | 2 × 128 | ~3.2 GB (131K) | ~12.8 GB (0.5M) |
Тут выделяется архитектура Qwen3.8–27B: в ней всего 16 полных слоев внимания из 64, а остальные 48 это Gated DeltaNet с постоянным состоянием, поэтому миллионный контекст в FP16 потребует всего ~68.7 GB, а не вчетверо больше.
Квантование KV‑кэша в Q8 уменьшает цифры из таблицы вдвое, а Q4 уменьшает примерно вчетверо с умеренной потерей качества.
Когда это вообще нужно?
YaRN почти ничего не стоит по вычислениям, пересчитать частоты один раз при старте не проблема. Но, как видно из таблицы выше, платить приходится увеличением памяти под KV‑кэш и точностью на коротких запросах, потому что расширяя дальнюю связность мы немного жертвуем локальным разрешением.
Сразу возникает вопрос: может, вместо того чтобы возиться с factor и rope_theta на маленькой модели, проще сразу взять модель покрупнее с большим нативным длинным окном?
Ответ зависит от того, что модель будет делать с этим длинным контекстом.
Расширенное окно на маленькой модели имеет смысл, если задача внутри контекста однообразная и локальная: модели не нужно держать в голове весь текст и делать по нему сложные выводы, достаточно последовательно и независимо обрабатывать куски прямо в рамках одного окна, без ручного разбиения на части.
Например: вытащить все даты, суммы или имена из огромного лога переписки или найти конкретную строку, цитату или конфиг в объемном документе: тут точность как раз упирается в тот самый эффект «потери в середине текста», а не в мощность модели.
Сюда же относится обход большой кодовой базы, когда для каждого файла отдельно нужно выдать краткое описание или список зависимостей: задача решается для каждого файла отдельно, а контекст просто экономит на постоянном открытии и закрытии файлов.
Похожим образом работает массовая разметка тикетов, модерация или расстановка тегов на потоке логов, построчный перевод длинного документа без необходимости помнить первую главу при переводе сороковой, сравнение двух версий документа на предмет различий, или дополнительная фильтрация уже найденных ретривером кусков текста, когда их много и они не помещаются в обычное окно, но глубоко сопоставлять их друг с другом не требуется.
Во всех этих случаях узкое место не в уме модели, а в том, видит она весь вход целиком или нет. Небольшая модель на 4B или 9B, которая умеет находить паттерн и повторять одно и то же простое действие много раз, справится не хуже модели в разы крупнее, если сможет разом увидеть весь текст.
В таких случаях платить памятью KV‑кэша за расширенное окно маленькой модели почти всегда выгоднее, чем гонять большую модель: KV‑кэш растет с длиной контекста одинаково независимо от размера модели, а вот вычисления на каждый сгенерированный токен у крупной модели дороже пропорционально ее размеру. К тому же крупная модель тоже рано или поздно упрется в свое нативное окно и потребует того же самого RoPE‑скейлинга, только обойдется это заметно дороже.
Обратная ситуация возникает, когда контекст нужен не просто чтобы модель все увидела, а чтобы она сделала из этого вывод: многошаговое рассуждение по всему документу сразу, сопоставление противоречивых кусков из разных его концов, заключения, которые зависят от связи начала текста с его концом.
Именно тут проявляется просадка качества в середине контекста и компенсировать это уже нечем, ведь модель маленькая. В таком случае расширенное окно на маленькой модели скорее навредит и разумнее либо использовать RAG с отбором действительно нужного, либо взять модель покрупнее, желательно с изначально длинным нативным окном, а не растянутым YaRN поверх небольшого контекстного окна.
Практическое правило можно сформулировать так: если задачу получается описать словами «примени это простое действие отдельно к каждому куску текста», берите маленькую модель и расширяйте ей окно. Если задачу нельзя описать без слов «сопоставь» или «сделай вывод из всего документа сразу», экономия на KV‑кэше того не стоит и нужна модель помощнее.
Давайте применим метод YaRN к моделям и посмотрим, что это нам даст на практике.
Qwen3.8–27B
Модель состоит из повторяющихся блоков: 16 раз подряд идет группа из четырех слоев, где первые три слоя используют механизм Gated DeltaNet, а четвертый использует обычное внимание с воротами (Gated Attention), и после каждого слоя стоит полносвязная сеть (FFN).
Т.е. на 64 слоя модели приходится всего 16 слоев классического внимания, а остальные 48 слоев обычный Gated DeltaNet, у которого, как обсуждалось раньше, состояние не растет с длиной контекста.
Размерность головы в слоях Gated DeltaNet равна 256, а RoPE применяется только к 64 из этих 256 чисел. Значит нужное нам значение partial_rotary_factor будет 64/256 = 0.25. Второй нужный нам параметр это factor, он показывает, во сколько раз расширяем контекстное окно: 1M/262K ~ 3.8, округляем до 4.
В config.json:
{ "mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144 }
Можно указать то же самое в аргументах командной строки:
# vLLM VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 vllm serve Qwen/Qwen3.8-27B \ --hf-overrides '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}' \ --max-model-len 1000000 # SGLang SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 python -m sglang.launch_server \ --model Qwen/Qwen3.8-27B \ --json-model-override-args '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}' \ --context-length 1000000 # llama.cpp / GGUF llama-server -m Qwen3.8-27B-Q4_K_M.gguf \ --ctx-size 1000000 \ --rope-scaling yarn --yarn-orig-len 262144 --rope-scale 4.0 \ --flash-attn \ --cache-type-k q8_0 --cache-type-v q8_0
YaRN может слегка ухудшить качество на коротких запросах, так как мы платим локальным разрешением за дальность видимости. Его стоит включать, когда вам постоянно необходимы настолько длинные контексты. Лучше всего подобрать значение factor под свои задачи.
Второй момент относится к агентным сценариям на длинных контекстах: общая рекомендация заключается в том, чтобы ограничить reasoning в 262К, а финальный ответ в 131К, иначе размышления модели сами съедят расширенное окно.
Qwen3.8-Flash‑Next
Здесь расширение контекста в основном обеспечивает не RoPE, а сама архитектура модели.
Модель мультимодальная, устроена как MoE (mixture of experts) на 125 миллиардов параметров, к которым добавлена отдельная таблица N‑gram embedding еще на 51 миллиард параметров, но реально на каждый токен активируется всего 6 миллиардов параметров. Я подробно разбирал эту модель в предыдущей статье [10].
Используем тот же рецепт YaRN, что и для 27B: rope_parameters идентичны (rope_theta: 1e7, partial_rotary_factor: 0.25, mrope [11, 11, 10]), так что factor: 4.0 + original_max_position_embeddings: 262144 дают ~1M в vLLM/SGLang/llama.cpp.
Qwen3.5–4B
Рецепт YaRN переносится с 27B практически один в один: формально, окно расширяется почти до 1M, но так как у модели на 4B емкости заметно меньше, то этот миллион на практике нерабочий.
Практичный выбор для этой модели: factor: 2.0 (~0.5M контекст).
Gemma 4 E2B / E4B
Здесь используется гибрид локального скользящего окна и глобального внимания (последний слой всегда глобальный), поэтому конфиг у этой модели устроен иначе, чем у Qwen:
"rope_parameters": { "full_attention": {"partial_rotary_factor": 0.25, "rope_theta": 1000000.0, "rope_type": "proportional"}, "sliding_attention": {"rope_theta": 10000.0, "rope_type": "default"} }
Google использует в своих моделях механизм расширения Proportional RoPE: частоты подбираются пропорционально длине контекста, а на глобальные слои он уже действует «из коробки».
Такой подход накладывает определенные ограничения: sliding‑слои (28 из 35 слоев у E2B и 35 из 42 у E4B) вообще ничего не видят дальше окна в 512 токенов, поэтому RoPE‑скейлинг к ним просто неприменим: сколько бы мы ни расширяли контекст, эти слои так и будут смотреть только на ближайшие 512 токенов. Вся ответственность за дальнюю память в модели лежит только на 7 глобальных слоях.
Эти глобальные слои и без нас уже используют собственный механизм расширения, внедренный Google (proportional RoPE). Поэтому если применить YaRN в лоб, ко всей модели сразу, получится либо бесполезное действие на sliding‑слоях, которые все равно его проигнорируют, либо, что хуже, двойное масштабирование на глобальных слоях, где p‑RoPE и так уже решает эту задачу.
То есть YaRN здесь просто не тот инструмент, так как здесь все уже увеличено до нас.
Если все‑таки необходимо выйти за штатные 128K, например расширить окно примерно до 262K, можно применить умеренный коэффициент масштабирования factor=2.0, но только к слоям с полным вниманием, через переопределение параметров в transformers и после этого обязательно проверить качество на своих данных.
Официально же Google заявляет 128K для маленьких моделей Gemma 4, а более крупные версии (12B, 31B и 26B‑A4B) сами держат 256K контекст по умолчанию.
LFM2 / LFM2.5
Интересная гибридная модель с 22 сверточными и 8 полными слоями внимания (в старшей версии LFM2-1.2B будет 16 слоев). Сверточные слои инвариантны к длине контекста, так что RoPE‑масштабирование затрагивает только 8 из 30 слоев.
Практичный рецепт для llama.cpp: factor=2.0 (131K до ~262K):
llama-server -m LFM2.5-2.6B-Instruct-Q4_K_M.gguf \ --ctx-size 262144 \ --rope-scaling yarn --yarn-orig-len 131072 --rope-scale 2.0 \ --flash-attn
MiniCPM5-1B
Это классическая Llama архитектура, идеальный кандидат для YaRN: все 24 слоя модели состоят из полных слоев внимания. Здесь можно попробовать использовать factor=4.0 для увеличения контекстного окна с 131K до ~524K.
llama-server -m MiniCPM5-1B-Q4_K_M.gguf \ --ctx-size 524288 \ --rope-scaling yarn --yarn-orig-len 131072 --rope-scale 4.0 \ --flash-attn --cache-type-k q8_0 --cache-type-v q8_0
Заключение
Расширение контекста сводится к настройке RoPE частот: PI сжимает все подряд, NTK‑aware сжимает неравномерно, YaRN добавляет к смешанному режиму температуру внимания работая без дообучения, а LongRoPE/LongRoPE2 используют эволюционный поиск масштабов, чтобы взять планку в 2M+ контекста, но уже с обучением.
Для популярных моделей картина такая: Qwen3.8–27B и Qwen3.8-Flash‑Next официально расширяются с 262K до ~1M с YaRN factor: 4.0. Маленькие Qwen модели используют те же настройки, но емкость самих моделей ограничивает рабочий потолок, поэтому factor: 2.0 будет лучшим выбором. Gemma 4 имеет послойные RoPE‑конфиги и нативный p‑RoPE, поэтому YaRN здесь не тот инструмент, который стоит использовать. LFM2/LFM2.5 масштабируются только через 8 attention‑слоев из 30, зато MiniCPM5-1B с плотным вниманием прекрасно масштабируется через YaRN.
И главное: RoPE‑скейлинг бесплатен по вычислениям, но не по памяти и не по точности на коротких текстах. Вы платите за длину контекста размером KV‑кэша, поэтому подбирайте значение factor под свои задачи.
References
Simple Guide to RoPE Scaling in Large Language Models, Floating Bytes (2025)
RoFormer: Enhanced Transformer with Rotary Position Embedding, arXiv (2021)
Lost in the Middle: How Language Models Use Long Contexts, arXiv (2023)
Extending Context Window of Large Language Models via Positional Interpolation, arXiv (2023)
NTK‑aware Scaled RoPE Allows Llama Models to Have Extended Context Size, Reddit (2023)
YaRN: Efficient Context Window Extension of Large Language Models, arXiv (2023)
LongRoPE: Extending LLM Context Window Beyond 2 Million Tokens, arXiv (2024)
Near‑Lossless LLM Context Window Scaling (LongRoPE2), ICML (2025)