Репозиторий: gitverse.ru/mibazarow/llama-cpp-hot-cache-qwen4exp
Что за задача: большая модель и маленькая карта
У меня домашний сервер с RTX 5060 Ti на 16 ГБ VRAM. Хочется гонять локальную нейросеть для разработки (PHP, 1С-Битрикс, кодинг) - без облаков, подписок и интернета. Но современные сильные модели весят куда больше, чем 16 ГБ: одна только Qwen3.8-Flash-Next в BF16 занимает ~335 ГБ. В видеопамять такое не влезет никогда.
Более подробно описано в заметке Большие LLM на маленькой видеокарте - впихнуть не впихуемое
И тут спасает то, что почти все сильные открытые модели - это MoE (Mixture of Experts). Внутри у них сотни экспертов на слой, но на каждый токен роутер активирует лишь маленькую часть (top-k). То есть модель «толстая», а работает в каждый момент лишь малая её доля.
Что такое MoE Hot-Cache
Обычный llama.cpp раскладывает MoE-модель по слоям: первые N слоёв в VRAM, остальные в RAM - без разбора, какие эксперты реально нужны. А ведь роутер под конкретную задачу дёргает довольно стабильный набор экспертов.
MoE Hot-Cache работает умнее: он определяет, какие эксперты «горячие» именно под ваши задачи, и держит их в VRAM. Холодные эксперты живут в RAM (или подтягиваются с диска) и считаются на CPU. В итоге большая модель запускается на маленькой карте, а значимая доля вычислений уходит на GPU.
На моей связке в VRAM помещается ~5-6 ГБ экспертов (порядка 1200-1900 экспертов на модель). Может показаться, что это мало, но это заметно лучше, чем ничего: часть вычислений уходит на GPU, а остальное считается на CPU. Hit-rate (доля обращений к экспертам, обслуженных из VRAM) по ходу работы растёт и выходит на 40-45%.
Зачем я сделал этот форк
Готовый форк с Hot-Cache (adrianhoehne/llama.cpp, ветка cached-experts-v2) - отличный, но он застыл на upstream от начала июля 2026 года. А за это время в llama.cpp влили поддержку новой архитектуры qwen4exp - это как раз Qwen3.8-Flash-Next.
Итог: модель новая, а hot-cache про неё ничего не знает. В форке её архитектуры просто нет - модель либо не грузится, либо работает без кэша экспертов, то есть всё считает CPU. А без hot-cache теряется весь смысл затеи.
Второй момент: вокруг Qwen3.8-Flash-Next уже куча квантов и производных (GGUF, REAP, abliterated), но всё это требует свежего upstream - а его в форке не было.
Что мы сделали
Коротко: смёрджили hot-cache-ветку со свежим upstream и допилили поддержку новой архитектуры.
- Мердж. Ветка hot-cache (73 коммита поверх upstream от 3 июля) смёрджена с актуальным master, куда уже влит
qwen4exp. Разрешено 13 конфликтов в ядровых файлах -ggml-backend.cpp,ggml-cuda.cu,llama-context.cpp,arg.cpp,CMakeLists, моделиqwen3next/glm4-moeи др. - Qwen3.8-Flash-Next в hot-cache. Архитектура зарегистрирована в hot-cache-адаптере, а MoE-граф модели обёрнут через hot-cache-путь (по той же схеме, что и для Qwen3Next): сначала считаются логиты роутера, потом hot-cache решает, кого считать на GPU, а кого на CPU.
- Восстановлено рантайм-обучение hot-cache. После каждого ответа движок обновляет горячий набор из накопленной статистики роутинга и перезаписывает профиль. То есть набор экспертов доучивается прямо во время работы, а не подбирается один раз при старте.
- Починены два рантайм-бага (см. «грабли» ниже).
Грабли
1. CUDA-графы и cudaStreamSynchronize
На длинной генерации сервер падал с ошибкой:
CUDA error: operation not permitted when stream is capturing
in function ggml_cuda_mul_mat_id
Причина: hot-cache использует отрицательные expert-id, из-за чего включается путь с синхронизацией потока. А апстрим-логика отключала CUDA-графы не для всех таких случаев - и синхронизация потока вызывалась прямо во время записи графа, что запрещено. Лечится условием: отключать графы для MUL_MAT_ID, если выставлены op-флаги (отрицательные id).
2. Профиль hot-cache и «0 observed candidates»
В логе было selected 1306 experts from 0 observed candidates - то есть профиль пустой, и горячие эксперты выбираются вслепую. Плюс это вскрыло вторую проблему: сам вызов обновления набора жил в серверном слое исходного форка, который мы при мердже заменили на upstream-версию, - поэтому профиль никогда не пополнялся.
Лечится двумя шагами: вернуть обновление hot-cache из статистики после ответа и прогреть профиль на своих задачах (об этом ниже).
3. IQ-квант даёт мусор
На кванте IQ4_XS hot-cache для qwen4exp выдавал мусор (длинные повторы символов вместо ответа). На Q6_K_XL и Q4_K_XL - всё чисто, включая tool-вызовы. Так что для этой архитектуры лучше брать не-IQ кванты. Кстати, это же подтверждается и на посторонних сборках - я не стал возиться с IQ4 и просто удалил его.
4. Контекст упирается в ubatch, а не в ctx
При ctx 155000 и ubatch 4096 связка активного hot-cache + compute-буферов не влезала в 16 ГБ и падала с CUDA OOM. Оказалось, дело в первую очередь в ubatch: prefill-буфер при 4096 разрастается почти до 9 ГБ. С ubatch 1024 тот же контекст 155000 влезает нормально, и на hot-cache остаётся ~5.6 ГБ.
Как собрать и запустить
Сборка - как обычно для llama.cpp с CUDA под Blackwell (sm_120):
git clone https://gitverse.ru/mibazarow/llama-cpp-hot-cache-qwen4exp.git
cd llama-cpp-hot-cache-qwen4exp
cmake -B build \
-DGGML_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES=120 \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_NATIVE=ON \
-DLLAMA_BUILD_TESTS=OFF
cmake --build build -j --target llama-server
Проверить, что hot-cache на месте:
./build/bin/llama-server --help | grep -i moe-hot-cache
Запуск модели с hot-cache (пример для Q6):
./build/bin/llama-server \
--model Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf \
--host 0.0.0.0 --port 8080 \
--device CUDA0 --split-mode none --main-gpu 0 --n-gpu-layers 99 \
--flash-attn on --cache-type-k q8_0 --cache-type-v q8_0 \
--cpu-moe \
--ctx-size 155000 --batch-size 2048 --ubatch-size 1024 \
--moe-hot-cache profile.json \
--moe-hot-cache-max-mib -1 \
--moe-hot-cache-auto-reserve-mib 3072 \
--moe-hot-cache-update-rate 0.10 \
--moe-hot-cache-warmup-prompt "Write a PHP function using the Bitrix API..."
Ключевые флаги: --cpu-moe отправляет экспертов в RAM, --moe-hot-cache-max-mib -1 включает автосайзинг кэша под свободную VRAM, --moe-hot-cache-auto-reserve-mib 3072 оставляет резерв под рабочие буферы (меньше - будет OOM на старте).
Профиль и прогревание: где берётся скорость
Профиль - это JSON со статистикой вызовов экспертов. Если он пустой, hot-cache выбирает горячий набор вслепую, и большая часть экспертов считается на CPU. Чтобы получить реальное ускорение, профиль нужно прогреть под свои задачи:
- запустить сервер, указав один и тот же файл в
--moe-hot-cacheи--moe-layer-perf-out; - прогнать несколько промптов типичной нагрузки (у меня - PHP/Bitrix/HTML/CSS);
- завершить сервер или просто поработать - профиль запишется.
Дальше статистика обновляется автоматически по ходу работы, и профиль перезаписывается. В логе это видно строкой вида:
MoE hot-cache: hit 41.83% (hot 175544 / cold 244166 slots), hot_experts=1179 exchanged=118
Результаты
Замеры на одном и том же PHP-промпте (300 токенов вывода), карта 16 ГБ:
- Профиль пустой (hot-cache вслепую): ~35-37 секунд, ~8 ток/с.
- Профиль прогретый: ~21-24 секунды, ~13-14 ток/с.
- Прогретый + живое обучение (Q4): ~16-17 секунд, ~18 ток/с.
Итого прирост от прогрева и обучения - примерно +60-90% к скорости, а hit-rate растёт по ходу работы (36% → 42% и выше). Модель Q4 (квант полегче) работает быстрее Q6, потому что меньше данных читать с диска и меньше подкачки.
Итог
Форк закрывает конкретную проблему: свежую большую MoE-модель надо запускать на маленькой карте, а готовый hot-cache её архитектуру не знал. Мы смёрджили hot-cache со свежим upstream, добавили поддержку qwen4exp (Qwen3.8-Flash-Next), вернули рантайм-обучение профиля и поправили два рантайм-бага.
На практике это даёт рабочую связку: Qwen3.8-Flash-Next на 16 ГБ VRAM, ~13-18 ток/с на кодогенерации, с живым обучением горячих экспертов. Плюс 35B-модели в Q8 работают стабильно и быстро, если нужно максимум точности на точном коде и API Битрикса.
Все подробности, сборка и код - в репозитории на GitVerse (ссылка в начале заметки). Он основан на форке adrianhoehne/llama.cpp - автору исходного hot-cache отдельное спасибо.