Кэш промптов Ollama — где правите, там и счёт
Шесть зондов, одна свежая ВМ: точный повтор переоценил 1,539 токенов за 0.2 с против 75.6 с в холодную — а одно слово в середине промпта вернуло в счёт 49 с.
verify: шесть зондов, одна свежая ВМ, seed 42 · temp 0 · ollama 0.40.16 · квитанции ниже, log promptcache-2026-11-12.log
Fig 1 — шесть зондов, один промпт. Приглушённые столбцы — квитанция: правка в середине промпта вернула 65% холодного счёта, а перестановка выставила счёт выше самой холодной загрузки.
Что на самом деле хранит кэш промптов ollama?
Не ваш текст. Когда ollama оценивает промпт, он считает KV-тензор для каждого токена — след внимания этого токена против всех предыдущих — и рантайм держит эти тензоры в RAM, пока модель загружена. Новый промпт сверяется с этим хранилищем токен за токеном, спереди. Каждый токен, точно продолжающий старый префикс, бесплатен. Первый отличившийся токен — граница: всё оттуда до конца перечитывается по полной цене. Это та же KV-резиденция, что наша статья про KV-cache разбирала для тайминга выгрузки, — кэш, который служит скорости, есть кэш, который держит RAM (а keep_alive решает, сколько проживёт и то и другое, — расчётную сторону этого счёта держит статья про окно контекста).
keep_alive, если ваш цикл болтлив с паузами.Почему точный повтор стоил 0.2 секунды?
Зонд: промпт в 1,539 токенов — восемь пронумерованных секций фактов из логов фикстуры, затем вопрос — задан в холодную на свежей ВМ. Движок прочёл все 1,539 токена на 20.4 tok/s и выставил 75.6 с оценки промпта. Тот же запрос, отправленный снова с резидентной моделью: 0.2 с. Тот же endpoint, то же тело, ничего не переключено — ollama делает это без флага, без cache: true, без заговоров на прогрев. Скидку вы получаете при каждом повторном запросе; этот пост о том, как её не отдать.
curl http://localhost:11434/api/generate -d '{
"model": "qwen3:4b",
"prompt": "<your 1,500-token prompt>",
"keep_alive": "45m",
"think": false,
"options": {"seed": 42, "temperature": 0, "num_predict": 128}
}'
verify: P1 холод 75.6 с → P2 повтор 0.2 с, тело зонда побайтово идентично, модель резидентна · promptcache-2026-11-12.log
Где правите — там и платите
Мы сохранили тело в 1,539 токенов и двигали ровно одно слово, тремя способами. Поменяйте слово в последнем предложении: 4.2 с — общая голова выжила, перечитан только хвост. Добавьте предложение после вопроса: 1.4 с — почти полное попадание, пять новых токенов оплачены на полях. Поменяйте одно слово в факте 41 из 96, в середине тела: 49.2 с — 65% холодного счёта, уплачено за перемещение одного прилагательного. У кэша память слона и прощение таможенника: он помнит всё, и одна неверная форма аннулирует все штампы в книжечке — для всего, что за неверной формой.
| зонд | что изменилось | prompt-eval | весь запрос | против холода |
|---|---|---|---|---|
| P1 холод | ничего — свежая ВМ, первый запрос | 75.6 s | 111.8 s | база |
| P2 повтор | ничего — идентичное тело | 0.2 s | 26.6 s | 370× быстрее |
| P3 суффикс | +1 предложение после вопроса | 1.4 s | 27.2 s | 54× быстрее |
| P4 правка в середине | 1 слово в факте 41/96 | 49.2 s | 75.2 s | 65% холода |
| P5 перестановка | секции 1–2 ↔ 7–8 переставлены | 79.9 s | 135.5 s | хуже холода |
| P6 у конца | 1 слово в последнем предложении | 4.2 s | 34.0 s | 18× быстрее |
промпт 1539 токенов · prompt-eval — счёт за перечитывание; «весь запрос» добавляет генерацию 128 токенов, державшуюся ~25–29 с всю дорогу.
Почему перестановка выставляет счёт дороже холодной загрузки?
Потому что граница на токене ~1. Поменяйте первые две секции с последними двумя — и общий префикс пуст: совпадение умирает сразу, и каждый токен пересчитывает счёт. Но P5 выставил 79.9 с, а не 75.6 с: 5.7% выше холодной загрузки, ради которой он выбросил скидку. Мы не будем притворяться, что поймали виновника, — честная пометка в том, что проход генерации на этом зонде тоже шёл в 2.1× медленнее (55.4 с против 25.5–29.4 с у любого другого запроса, тот же seed, тот же бюджет). Тёплое состояние сервера присоединяется к счёту так, как диаграмма кэша не показывает; статья про детерминизм уже встречала этого свидетеля с перевёрнутым токеном при temp 0. Что мы можем заявить как квитанцию: переставленный промпт — не «тот же промпт, другой порядок». Это совершенно новый счёт плюс шум.
Список исправлений
| симптом | что происходит | исправление |
|---|---|---|
| каждый запрос ощущается как холодная загрузка | модель выгружается между запросами — кэш умирает с ней | поднимите keep_alive; проверьте load_duration в ответе |
| крошечные правки промпта, огромная задержка | правка сидит в середине промпта; хвост считает счёт от точки расхождения | двиньте волатильную часть в хвост промпта |
| «тот же промпт» считает счёт по-разному от запуска к запуску | порядок или boilerplate дрейфовал между запросами | заморозьте голову побайтово; делайте diff промптов, а не вайбов |
P1 cold-full-ask prompt_eval 75,646 ms (1,539 tok · 20.4 tok/s) wall 111.8 s P2 exact-repeat prompt_eval 203 ms wall 26.6 s P3 suffix-only prompt_eval 1,402 ms (+5 tok) wall 27.2 s P4 mid-edit-kill prompt_eval 49,176 ms (1 word, fact 41/96) wall 75.2 s P5 reorder-full-miss prompt_eval 79,878 ms (secs 1–2 ↔ 7–8) wall 135.5 s P6 edit-near-end prompt_eval 4,189 ms (1 word, last sentence) wall 34.0 s
Один промпт. Шесть счетов, от 0.2 до 79.9 секунды — разброс в 370× без единого изменённого сеттинга и без единого переключённого флага. Кэш был включён всегда. Единственной переменной было то, где мы осмеливались править.
последний тест 2026-11-12
qwen3:4b · 2,497,293,931 B · sha256 359d7dd4…74e7 · ollama 0.40.16 · одна свежая Arch ВМ, 2 vCPU — CPU-бенч, никаких заявлений про VRAM