Повторная обработка больших промптов в llama.cpp: как llama-swap спасает TTFT | AiManual
AiManual Logo Ai / Manual.
14 Май 2026 Гайд

Проблема повторной обработки больших промптов в llama.cpp: причины и решения для llama-swap

Глубокий разбор причин репроцессинга промптов в llama.cpp и пошаговое руководство по настройке кэширования KV-кэша через llama-swap для ускорения TTFT на больши

Сценарий, от которого дергается глаз

Представь: ты загружаешь в llama.cpp модель Qwen 3.5 с контекстом в 128K токенов. Запрос — это не один вопрос, а большой промпт с историей чата на 10K токенов. Модель выдает ответ. Потом ты делаешь следующий запрос с тем же промптом, но чуть измененным последним сообщением. И… ждешь снова 30 секунд до первого токена. Сервер заново пересчитывает KV-кэш для всех 10K токенов. Твой GPU молотит впустую, а пользователь думает, что всё сломалось.

Это — классическая проблема повторной обработки (репроцессинга) промптов. В llama.cpp она проявляется особенно остро, если не управлять кэшем. И ладно бы речь шла о паре сотен токенов — но когда промпты толстые (генерация кода, анализ документов, диалоги с системными сообщениями), простой префилл становится узким местом. TTFT (Time To First Token) растет линейно от длины промпта, и каждый повторный запрос платит полную цену.

В этой статье я разбираю причины репроцессинга на уровне внутренностей llama.cpp и показываю, как llama-swap — легковесный менеджер моделей — решает эту проблему через интеллектуальное кэширование KV-кэша. Без танцев с бубном и без переписывания кода ядра.

📌 Ключевая метрика: TTFT (time to first token). При префилле 10K токенов на GPU RTX 3090 с моделью Qwen 3.5 (INT4) — около 8–12 секунд. С кэшем — падает до 0.2–0.5 секунд. Разница в 20–60×.

Почему llama.cpp заново считает одно и то же?

Чтобы понять решение, нужно разобраться в механизме. В llama.cpp есть понятие n_past — количество уже обработанных токенов в текущей сессии. Когда ты делаешь новый запрос через HTTP API (например, /completion), по умолчанию создается новая сессия с n_past = 0. Весь KV-кэш из предыдущего вызова остаётся в памяти, но не переиспользуется. Почему? Потому что сервер не знает, что новый промпт начинается с той же последовательности, что и старый.

Вторая причина — context grow. Когда модель упирается в лимит контекста, llama.cpp может автоматически расширять окно (роуминг контекста). Это приводит к сбросу кэша для части токенов, и следующий префилл снова пересчитывает всё.

⚠️ Важный нюанс: Даже если ты используешь одно и то же prompt в двух последовательных запросах, но между ними произошла смена модели (через тот же llama-swap или вручную), KV-кэш обнуляется. Каждая модель имеет свою структуру кэша — его нельзя переиспользовать между разными архитектурами.

Третья причина кроется в том, что llama.cpp из коробки не умеет prefix caching — то есть сопоставлять начало нового промпта с уже вычисленным кэшем. Нужен внешний слой, который запоминает, для каких последовательностей токенов у нас есть готовый KV-кэш, и при новом запросе пропускает префилл совпадающей части.

llama-swap: прокси с кэшем, который реально работает

llama-swap — это не еще один рантайм, а прокси-сервер, который садится перед одним или несколькими экземплярами llama.cpp (или его форками). Он умеет:

  • держать несколько моделей в памяти и переключаться между ними без остановки сервера;
  • сохранять KV-кэш при смене модели (если кэш корректен для нового контекста);
  • реализовывать prefix caching на уровне последовательности токенов (сопоставление по хэшу);
  • управлять размером кэша и политикой вытеснения (LRU).

Ключевая фича для нашей проблемы — именно prompt caching. Когда приходит запрос, llama-swap вычисляет префиксный хэш от промпта (или его части), ищет в своём KV-хранилище подходящий слот. Если находит — отправляет в llama.cpp не весь промпт, а остаток, начиная с первого несовпавшего токена, плюс флаг n_past, равный длине закэшированной части. Модель достраивает только новый кусок.

В версии llama-swap v0.8.12 (актуальна на май 2026) появилась экспериментальная поддержка prefix caching с привязкой к слоям — это позволяет кэшировать части контекста даже при смене системного промпта, если тело запроса остаётся. А ещё он умеет работать с форматом openai через тот же endpoint /v1/chat/completions, что делает его совместимым с любым софтом, от opencode до pi.dev.

💡
llama-swap не требует перекомпиляции llama.cpp. Достаточно запустить его как отдельный процесс, который проксирует запросы к вашему существующему серверу. Это минимально инвазивное решение.

Пошаговый план: как настроить кэширование больших промптов

Я покажу реальную конфигурацию, которую я использую на своей железке (Ubuntu 24.04, RTX 3090, 64GB RAM). Все команды проверены на llama.cpp b4732 и llama-swap v0.8.12.

1 Собери или обнови llama.cpp с поддержкой кэша

Для работы prefix caching нужна последняя версия llama.cpp. Я рекомендую собирать из исходников с флагами -DLLAMA_CUDA=ON -DLLAMA_K_QUANTS=ON. Альтернатива — использовать форк BeeLlama.cpp, который даёт дополнительный прирост скорости на префилле за счёт TurboQuant. Как настроить сборку, я писал в гайде по оптимальной компиляции.

2 Установи llama-swap

Ставится через pip или go (предпочитаю второй вариант, меньше зависимостей):

# Сборка из исходников Go
git clone https://github.com/mozilla-ai/llama-swap.git
cd llama-swap
go build -o llama-swap

# Или через go install
go install github.com/mozilla-ai/llama-swap@latest

После сборки появится бинарник llama-swap. Конфигурация — YAML файл.

3 Конфигурация для кэширования

Создайте файл config.yaml:

models:
  - name: qwen35
    source: /path/to/models/qwen3.5-14b-q4_k_m.gguf
    backend: llama
    args:
      n_ctx: 65536  # Фиксируем контекст
      n_gpu_layers: -1
      flash_attn: true
      cache_size_kv: 4096  # Размер KV-кэша в MB

cache:
  type: lru
  max_entries: 100
  max_size_mb: 8192  # 8 GB под кэш
  prefix_caching: true
  prefix_len: 32      # Длина префикса для хэширования

server:
  host: 0.0.0.0
  port: 8080
  backend_url: http://localhost:8081  # адрес оригинального llama.cpp

Ключевые параметры:

  • cache_size_kv — размер внутреннего KV-кэша модели. Если вы используете большие контексты (32K+), ставьте минимум 4096. Иначе кэш будет вытесняться на каждом новом токене.
  • prefix_caching: true — включает сопоставление префиксов. При длине префикса 32 токена вероятность коллизий ничтожна, а производительность высокая.
  • max_entries — количество хранимых в памяти разных префиксов. Для одного активного диалога достаточно 10–20.

4 Запуск и проверка

Сначала запустите ваш экземпляр llama.cpp на порту 8081:

./llama-server -m model.gguf --port 8081 --n-gpu-layers -1 --ctx-size 65536 --flash-attn

Потом запустите llama-swap:

./llama-swap --config config.yaml

Теперь все запросы идут через порт 8080. Отправьте первый запрос с большим промптом, замеряйте TTFT. Затем отправьте тот же промпт — второй ответ придёт в разы быстрее. Можно проверить в логах: если видите строчку cache hit for prefix ... — всё работает.

💡
Для точной настройки производительности рекомендую прочитать статью про оптимизацию llama.cpp под Linux — там есть советы по настройке NUMA и thread pinning.

Нюансы и типичные грабли

Кэширование — штука тонкая. Перечислю основные ошибки, с которыми я сталкивался сам и которые видел в issues:

1. Context grow ломает кэш

Если у модели n_ctx не фиксирован и llama.cpp самостоятельно расширяет контекст (например, через rope scaling), позиции токенов меняются — кэш становится невалидным. Решение: всегда задавайте ctx-size равным максимальному ожидаемому контексту. Используйте --no-context-grow в llama.cpp (флаг появился в b4600+).

2. Неподдерживаемые модели

Prefix caching работает только для моделей на архитектуре LLaMA (включая Qwen, Mistral, Gemma). Для Mamba или RWKV не поддерживается. Проверьте, что ваша модель использует стандартный KV-кэш с масками внимания.

3. Маленький размер кэша

cache_size_kv слишком мал — кэш вытесняет старые записи, и вы не получаете попаданий. Правило: для контекста 32K токенов нужно минимум 2 GB KV-кэша (с учётом 4-битных квантов). Если модель 70B — ещё больше.

4. Конфликты при смене модели

llama-swap может держать несколько моделей, и кэш для каждой хранится отдельно. Но если вы переключаетесь между моделями разных семейств, кэш сбрасывается. Если же модели одной архитектуры (например, разные GGUF варианты Qwen 2.5), можно попробовать переиспользовать кэш, но это нестабильно.

5. Проблемы с flash attention

Некоторые реализации flash attention несовместимы с prefix caching (лязг в бекенде). В llama-swap есть флаг --disable-flash — если кэш перестал работать, попробуйте его включить. Скорость префилла упадёт, но TTFT всё равно будет меньше, чем без кэша.

Как НЕ надо делать: пример ошибки

Вот типичный конфиг новичка, который приводит к тому, что кэш не работает:

cache:
  type: lru
  max_entries: 1000
  max_size_mb: 512  # Too small!
  prefix_caching: false  # Забыл включить
  prefix_len: 256  # Слишком длинный префикс

Что здесь не так?

  • 512 MB хватит только на пару коротких диалогов. Для 10K токенов нужно 2–4 GB.
  • prefix_caching: false — кэш работает, но только если вы отправляете полностью идентичный промпт. Малейшее изменение — и промах.
  • prefix_len: 256 — хэш от 256 токенов считается долго и требует много памяти для хранения индексов. Оптимально 16–32 токена.

Правильная конфигурация показана выше в шаге 3.

⚠️ Важно: Если вы используете тензорный параллелизм (несколько GPU), убедитесь, что каждый экземпляр llama.cpp запущен с одинаковыми параметрами контекста и кэша. Llama-swap в текущей версии не агрегирует кэш между разными backend'ами, поэтому каждый GPU держит свой кэш. Подробнее про тензорный параллелизм — в статье на ту же тему.

FAQ: частые вопросы и спорные моменты

Вопрос: Как проверить, что кэш реально используется?

В llama-swap включено логирование. При запуске с флагом --verbose вы увидите сообщения вида cache hit for prefix 0x3f8a... (len=5248). Также можно смотреть метрики Prometheus на /metrics.

Вопрос: Влияет ли кэширование на качество генерации?

Нет, если попадание точное (совпадает последовательность токенов). При prefix caching мы гарантируем, что первые N токенов идентичны — модель выдаёт те же логиты для остатка. Риск — только при коллизии хэшей (вероятность ничтожна для 32-токенных префиксов).

Вопрос: Можно ли кэшировать ответы модели, а не только промпт?

Только если вы используете системные промпты и задаёте фиксированную префиксную часть. Для повторяющихся пользовательских запросов — да, кэш префикса сработает. Но генерация (decode) не кэшируется — это фундаментально сложно из-за автокорреляции токенов.

Вопрос: Как работает кэш при длинных чатах, где промпт всё время растёт?

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

Сравнение: llama-swap vs нативный репроцессинг в llama.cpp

Сценарий Без кэша (llama.cpp raw) С llama-swap (prefix cache)
TTFT для промпта 10K токенов ~12 сек ~0.3 сек (hit)
Повторный запрос с изменением последнего сообщения ~12 сек (полный префилл) ~0.5 сек (префилл только нового)
Смена модели (той же архитектуры) ~12 сек ~0.8 сек (кэш переиспользуется частично)
Context grow при переполнении Сброс кэша, повторный префилл Кэш остаётся, если n_ctx фиксирован

Почему это не решили на уровне llama.cpp?

Законный вопрос: если проблема настолько очевидна, почему разработчики llama.cpp не встроили prefix caching прямо в сервер? Ответ — философия проекта. llama.cpp — это низкоуровневый движок. Он предоставляет базовые примитивы (префилл, декод, управление контекстом), а всю логику кэширования ожидает от пользователя или внешних прослоек. Сейчас в мастер-ветке есть экспериментальный флаг --prompt-cache, который пишет бинарный кэш на диск, но он не умеет частичное совпадение и требует точного повторения промпта.

В форке BeeLlama.cpp (о нём у меня отдельная статья) есть встроенный кэш префиксов, но он завязан на API форка и не совместим с основным. Llama-swap решает проблему независимо от бекенда — вы можете использовать любой форк, хоть RPC-server для распределённых вычислений (читайте тесты RPC).

Будущее: cache-aware prefill-decode disaggregation

В 2025–2026 годах активно развивается подход cache-aware prefill-decode disaggregation. Идея: разделить префилл и декод на разные ноды, причём префилл-нода может возвращать готовый кэш, который затем используется нодой декода. Это даёт выигрыш до 40% на длинном контексте. Llama-swap уже сейчас может выступать в роли такой ноды, если запущен в режиме proxy с двумя разными инстансами llama.cpp. Подробности в статье про disaggregation.

Но даже без этого хайпа — простая настройка llama-swap с prefix caching решает проблему репроцессинга на 90%. Остальные 10% — это edge cases, которые мы разобрали.

Неочевидный совет напоследок

Если вы используете llama-swap вместе с Ollama (как бекенд), не делайте так. Ollama сама по себе добавляет слой абстракции, который мешает кэшированию. Лучше поднимите голый llama.cpp сервер — он быстрее и даёт полный контроль. Разница в скорости между llama.cpp и Ollama на одинаковом железе может достигать 1.7 раза, как показано в нашем расследовании.

Ещё один практический совет: если ваши промпты содержат много повторяющихся частей (системные промпты, шаблоны кода), сгенерируйте префиксный кэш один раз запуском пустого запроса с этим промптом, а затем пользуйтесь им во всех сессиях. В llama-swap для этого есть команда prefill — просто передайте промпт в специальный эндпоинт, и кэш запишется без генерации ответа.

Повторная обработка больших промптов — это не баг, а особенность архитектуры. Но мы, инженеры, не обязаны с этим мириться. Берём прокси в руки — и вуаля.

Подписаться на канал