MAATRIX / Блог / Контекст на 128 тысяч токенов: цена в памяти и в скорости

Контекст на 128 тысяч токенов: цена в памяти и в скорости

MAATRIX

В карточке модели написано «контекст 128k токенов», и это звучит как готовая индульгенция: можно скормить модели весь проект, всю переписку, всю документацию разом. На практике сервер с той же моделью, что бодро отвечала на короткие вопросы, начинает тормозить и падать по памяти именно тогда, когда в диалог наконец загружают длинный документ. Разгадка не в том, что модель «врёт» про поддерживаемую длину — а в том, что поддержка длинного контекста и его реальное бесплатное использование на скромном железе — это два разных утверждения. Разберёмся, откуда берётся эта цена и как её посчитать заранее.

Что на самом деле означает цифра "128k контекста" в характеристиках модели

Заявленное число — это архитектурный лимит: максимальная длина последовательности, на которой модель обучалась и с которой умеет работать корректно, не «разваливаясь» по качеству. Это утверждение о модели, а не о вашем сервере. Оно ничего не говорит о том, сколько памяти и времени потребуется рантайму (vLLM, llama.cpp, Ollama, TGI и так далее), чтобы реально прогнать через модель промпт такой длины.

Разница похожа на характеристику автомобиля «максимальная скорость 220 км/ч» — это правда, но она не говорит, сколько топлива вы сожжёте, если действительно поедете 220 км/ч, и не гарантирует, что на вашей дороге это безопасно и осмысленно. Так и с контекстом: «модель поддерживает 128k» — это про архитектуру, а «во что вам обойдётся использовать 128k» — отдельный вопрос про инфраструктуру, и ответ на него в характеристиках модели не пишут.

Ключевая причина разрыва — механизм внимания (attention), на котором построены все современные языковые модели, и практика, без которой инференс на большом контексте был бы мучительно медленным — кеш ключей-значений, он же KV-кеш. Дальше — как это устроено и почему это стоит памяти и времени.

Механизм внимания и KV-кеш: почему нельзя просто "забыть" старые токены

Трансформер генерирует ответ по одному токену за раз: посчитал токен, приписал к последовательности, считает следующий — и так до конца ответа или до стоп-токена. На каждом слое каждый новый токен проходит через self-attention, и в этом механизме токен должен «посмотреть» на представления всех предыдущих токенов контекста, чтобы решить, какие из них релевантны именно для его собственного смысла. Это не опция — это то, как вообще работает внимание: без доступа к предыдущим токенам модель не смогла бы поддерживать связность текста, ссылаться на сказанное раньше, учитывать инструкции из начала промпта.

Технически на каждом слое и в каждой голове внимания для каждого токена считаются три вектора: query (Q — «что я ищу»), key (K — «что я предлагаю для сравнения») и value (V — «что я отдаю, если меня выбрали релевантным»). Генерация следующего токена сравнивает его query с key всех токенов контекста и взвешивает их value результатом этого сравнения.

Наивная реализация без кеша при генерации каждого нового токена пересчитывала бы K и V заново для абсолютно всех предыдущих токенов — просто потому что формально ничто не мешает это сделать. Но это расточительно: K и V токена номер 5 не зависят от того, что вы дописали токен номер 4000 позже — они определяются только самим токеном номер 5 и токенами перед ним. Поэтому ещё на самых ранних практических реализациях GPT-подобных моделей эту работу перестали дублировать: K и V каждого токена вычисляются один раз, когда токен впервые попадает в контекст, и затем кешируются в памяти GPU. При генерации следующего токена модели достаточно посчитать Q, K и V только для него самого, а K и V всех предыдущих токенов просто достаются из кеша.

Это и есть KV-кеш — тот самый механизм, что делает потоковую генерацию практически применимой по скорости. Подробнее про его устройство и почему именно он, а не веса модели, чаще всего оказывается неожиданной причиной CUDA out of memory в диалоге, разобрано в материале про KV-кеш и рост видеопамяти в диалоге — здесь же сфокусируемся именно на том, как эта цена связана с заявленным размером контекстного окна.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Ключевое следствие: кеш растёт пропорционально длине контекста

Из механизма выше прямо следует главный факт этой статьи: KV-кеш хранит K и V для каждого токена, который сейчас находится в контексте, на каждом слое и в каждой голове внимания. Значит, объём кеша растёт линейно с числом токенов в контексте — чем длиннее реально используемый диалог или промпт, тем больше памяти занимает кеш, помимо памяти, уже занятой весами самой модели.

Формула для оценки размера кеша (в байтах) в общем виде:

размер KV-кеша ≈ 2 × n_layers × n_kv_heads × d_head × seq_len × batch_size × bytes_per_element

где:

  • 2 — отдельно храним K и V;
  • n_layers — число слоёв трансформера;
  • n_kv_heads — число голов внимания, отвечающих за K/V (в архитектурах с GQA/MQA их меньше, чем голов запросов — это одна из оптимизаций, снижающих именно эту статью расходов);
  • d_head — размерность одной головы внимания;
  • seq_len — длина контекста в токенах (именно она растёт до 128k и больше);
  • batch_size — число параллельно обслуживаемых запросов/диалогов;
  • bytes_per_element — байт на число: обычно 2 байта для fp16/bf16, кеш можно дополнительно квантовать до int8/fp8, тогда 1 байт.

Считать точные гигабайты для конкретной модели без сверки с её реальной архитектурой (числом слоёв, голов, размерностью — это публикуется в конфиге модели на её странице) не стоит: слишком легко ошибиться на порядок. Но методика показывает главное — три множителя в этой формуле фиксированы архитектурой модели, а один, seq_len, целиком зависит от того, сколько контекста вы реально скармливаете модели прямо сейчас. Удвоили используемый контекст — удвоили память под кеш. Это не побочный эффект, а прямое следствие того, как устроено внимание.

Методику пересчёта размера контекста в память и обратно — сколько токенов влезет в оставшуюся после весов память — подробно разбирали в статье про контекстное окно и расчёт памяти. Здесь важно зафиксировать вывод: KV-кеш — это не константа, зашитая в модель, а переменная величина, которая целиком в руках того, кто формирует запрос.

Почему кеш длинного контекста может весить как сама модель — и даже больше

Дальше — та самая ловушка «128k заявлено, но…». Пока диалог короткий (сотни, пусть даже пара тысяч токенов), кеш занимает скромную, часто незаметную на общем фоне долю видеопамяти — основной вес несут параметры модели. Но множитель seq_len в формуле выше линеен и ничем не ограничен снизу лимитом самой модели — он растёт до тех пор, пока растёт реально используемый контекст.

Когда контекст дорастает до десятков и сотен тысяч токенов — то есть до тех значений, которые собственно и рекламируются как «128k» — множитель seq_len вырастает на два-три порядка относительно короткого диалога, а число слоёв, голов и размерность головы остаются теми же самыми. В результате объём памяти, который занимает только кеш, сверх памяти самих весов, может стать сопоставимым с весом модели, а на действительно длинных контекстах — и превысить его. Модели с большим числом слоёв и без архитектурных оптимизаций кеша (GQA/MQA) особенно чувствительны к этому эффекту — у них множитель n_layers × n_kv_heads × d_head изначально больше, и рост от seq_len даёт более резкий скачок.

Отсюда практический вывод, который стоит держать в голове при планировании железа: заявленная поддержка «128k контекста» — это утверждение «модель не сломается на такой длине», а не утверждение «эта длина обойдётся вам так же дёшево, как короткий диалог, на том же GPU». Реальное использование длинного контекста требует значительно больше памяти, чем работа с коротким контекстом на той же самой модели — и это дополнительная память нужна сверх того, что уже заложено под веса. Если бюджет по VRAM или RAM считался только «под модель», длинный контекст на боевой нагрузке — это тот момент, когда сервер упирается в out of memory, даже если формально «128k заявлено — значит, поддерживается».

Цена в скорости: почему длинный промпт долго "думает" перед первым словом

Память — не единственная статья расходов. У длинного контекста есть отдельная, самостоятельная цена по времени, и она проявляется в первую очередь на этапе первичной обработки промпта — так называемом prefill (иногда его называют этапом заполнения кеша). Именно на этом этапе модель впервые видит весь контекст целиком — весь ваш промпт, документ, историю диалога — и должна посчитать K и V для каждого его токена, чтобы заполнить кеш, прежде чем сможет сгенерировать хотя бы первый токен ответа.

Prefill — принципиально более тяжёлая по вычислениям операция, чем генерация одного токена. При генерации ответа каждый новый токен считается один за другим, опираясь на уже готовый кеш предыдущих. При prefill же модели приходится обработать сразу весь промпт, и внимание при этом сопоставляет каждый токен с каждым — из чего следует, что вычислительная нагрузка на этом этапе растёт быстрее, чем линейно от длины промпта (квадратично для базового механизма внимания без дополнительных оптимизаций; современные реализации вроде FlashAttention снижают требования к памяти на этом шаге, но не отменяют сам квадратичный рост вычислений).

Практическое следствие — задержка до первого токена ответа (так называемый time-to-first-token, TTFT) растёт вместе с длиной промпта, и растёт не линейно, а быстрее. Короткий вопрос на пару десятков токенов получает первый символ ответа почти мгновенно. Промпт на несколько десятков тысяч токенов — например, при загрузке документа целиком в контекст — заставит модель ощутимо дольше «читать» его перед тем, как начнёт отвечать, и это ощущается пользователем как зависание, даже если сама генерация текста дальше идёт с обычной скоростью. Конкретные цифры задержки сильно зависят от модели, железа, реализации инференса и того, насколько эффективно рантайм умеет параллелить prefill — здесь не будем выдумывать секунды и токены в секунду для конкретных конфигураций, это стоит измерять на своём стеке, но сам факт роста задержки с длиной промпта — architectural, а не случайность конкретной реализации.

Отдельно стоит учитывать нагрузку batch-режима: если сервер параллельно обслуживает несколько диалогов, каждый со своим (пусть и не максимальным) контекстом, суммарная память под кеши всех активных сессий складывается, а не делится между ними — расчёт из предыдущего раздела нужно домножать на реальное число одновременных диалогов, а не считать «на один запрос».

Практическая методика: сколько контекста вам реально нужно заложить

Прежде чем закладывать в архитектуру приложения использование длинного контекста «про запас» — просто потому что модель формально его поддерживает — стоит пройти через несколько практических шагов, а не ориентироваться на цифру из характеристик модели:

  1. Оцените реальную задачу, а не максимум возможностей. Часто задача — это не «загрузить весь репозиторий», а «ответить по одному документу на пару тысяч слов плюс системный промпт плюс история последних нескольких реплик». Посчитайте реалистичный верхний предел токенов для вашего типичного запроса — как правило, он заметно меньше заявленных моделью 128k.
  2. Заложите отдельный запас под KV-кеш, а не только под веса. Используйте формулу из раздела выше (или обратный расчёт из статьи про контекстное окно и расчёт памяти), подставив вашу реальную ожидаемую длину контекста и реальное ожидаемое число параллельных диалогов — а не архитектурный максимум модели.
  3. Учтите пиковую, а не среднюю нагрузку. Если сервис может получить несколько длинных запросов одновременно (например, несколько пользователей одновременно грузят документы), считайте память под пиковый, а не средний сценарий — иначе out of memory будет происходить именно в момент всплеска, самый неудобный.
  4. Проверьте, есть ли смысл резать контекст программно. Не всегда нужно тащить в модель весь документ целиком — суммаризация, поиск релевантных фрагментов (RAG) или скользящее окно диалога часто снижают реальный seq_len на порядок без потери качества ответа для конкретной задачи, и это дешевле, чем наращивать память под кеш.
  5. Проверьте, доступно ли квантование кеша. Часть рантаймов (например, vLLM) умеет хранить KV-кеш не в fp16, а в int8/fp8, что примерно вдвое снижает его вес почти без потери качества — это отдельная ручка снижения расходов, независимая от квантования самих весов модели (о котором подробнее в материале про выбор квантования моделей).
  6. Заложите тест на реальной длине, а не на коротких запросах. Прогон с короткими тестовыми промптами ничего не скажет о поведении сервера на боевой длине контекста — тестируйте именно на той длине и с тем параллелизмом, что ожидаете в проде, до запуска, а не после первого падения.

Итог методики простой: считайте требования к железу под пару чисел — реальную ожидаемую длину контекста и реальное ожидаемое число одновременных диалогов, — а не под красивую цифру «128k» из карточки модели. Она говорит о возможностях модели, а не о вашем бюджете памяти.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Если модель заявляет 128k контекста, но я обычно использую 4k — плачу ли я за неиспользуемые 124k?

Нет. KV-кеш занимает память под реально находящиеся в контексте токены — если в диалоге сейчас 4k токенов, кеш посчитан под 4k, а не под архитектурный максимум модели. Платить приходится только за то, что реально загружено в контекст в моменте.

Можно ли уменьшить рост KV-кеша, не трогая длину контекста?

Да, частично. Архитектурные оптимизации на стороне модели (GQA/MQA — меньше голов для K/V) и квантование самого кеша (int8/fp8 вместо fp16) снижают вес каждого токена в кеше, но не отменяют сам линейный рост от длины контекста — это снижает коэффициент, а не убирает зависимость.

Правда ли, что вся задержка ответа — это только prefill?

Нет, это лишь задержка до первого токена. После неё генерация идёт токен за токеном с использованием уже готового кеша и обычно ощутимо быстрее prefill в пересчёте на токен — но для длинного промпта именно вклад prefill в общее время ответа становится заметным, особенно если ответ короткий, а промпт длинный.

Стоит ли всегда брать модель с максимально заявленным контекстом "про запас"?

Не обязательно. Если реальная задача укладывается в несколько тысяч токенов, модель с меньшим заявленным максимумом контекста (и, как следствие, часто более скромными архитектурными требованиями к памяти под кеш) может обойтись заметно дешевле по железу при той же практической пользе.

Как понять, что именно KV-кеш, а не веса модели, стал причиной нехватки памяти?

Косвенный признак — память заканчивается не сразу при загрузке модели, а через какое-то время использования, по мере роста диалога, или резко при первом же длинном промпте, а не при коротких запросах. Это характерный почерк роста именно кеша, а не фиксированного веса модели.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →