Исходные условия: недорогой системник с видеокартой на 16 ГБ VRAM. Один из главных вопросов - а влезет ли большая модель в маленькую карту. Влезает, если правильно подойти. Ниже весь мой практический опыт, включая грабли, на которые я наступил.
Железо
Собирал на такой конфигурации:
- CPU: AMD Ryzen 5 7500F (6 ядер / 12 потоков)
- RAM: 64 ГБ DDR5
- GPU: NVIDIA RTX 5060 Ti 16 ГБ (Blackwell, sm_120)
- Диск: NVMe 937 ГБ
- ОС: Ubuntu 24.04
64 ГБ оперативки и NVMe тут не роскошь, а необходимость. Большие модели (например 122B) целиком в 16 ГБ VRAM не влезают, поэтому веса ложатся в RAM, а часть ещё дальше - в swap. Диск нужен быстрый, чтобы читать модель без фризов.
Почему Linux, а не Windows
- llama.cpp и форки собираются под Linux в разы проще. Windows чаще всего требует WSL или готовые бинарники, а когда нужен свой форк с нестандартными флагами (как MoE Hot-Cache) - под WSL это мучение.
- Headless. Сервер не должен грузить графическую оболочку. На Windows интерфейс и фоновая графика висят в памяти и постоянно мешают. На Linux серверную часть можно оставить вообще без десктопа.
- Меньше оверхеда в RAM. Голая Ubuntu без GUI ест полтора-два гигабайта. Windows со своим интерфейсом отжирает в несколько раз больше, а каждый гигабайт в 64 ГБ на счету.
- NVIDIA-драйвер и CUDA ставятся из коробки под Ubuntu. Плюс Secure Boot я отключил в BIOS, чтобы обновления ядра не ломали модуль драйвера (об этом ниже).
- Автозапуск и systemd. Сервисы, ватт-лимит GPU, поднимание swap - всё это штатно описывается юнитами и поднимается при старте, без шаманств.
Если коротко: для серверного процесса Linux даёт предсказуемое окружение, которое работает само и не мешает. Windows в этой роли просто неудобен.
Почему именно llama.cpp с MoE Hot-Cache
Классический llama.cpp раскладывает слои MoE-модели по номерам: первые N слоёв в VRAM, остальные в RAM - без учёта того, какие эксперты реально работают. А в MoE-моделях на каждый токен активируется только маленькая часть экспертов (top-k), остальные сотни просто ждут.
Форк с MoE Hot-Cache работает умнее: он собирает статистику, какие эксперты роутер реально дёргает под ваши задачи, и держит именно их в VRAM. Холодные эксперты живут в RAM и догружаются при необходимости. Для кодинга на Битриксе горячий набор экспертов стабильный, поэтому ускорение реальное, а не на бумаге.
На практике на моих моделях hit-rate (доля обращений к экспертам, которые уже лежат в VRAM) держится на уровне 50-70%. Скорость decode у KAT-Coder (35B MoE) получилась 42-45 t/s на 16 ГБ карте. Для агентной работы с большим кодом это комфортно.
Что в итоге за модели я гоняю
Остановился на связке из нескольких моделей под разные задачи:
- KAT-Coder-V2.5 (35B MoE, Q8_0) - основная рабочая лошадка для кода. Быстрая, хорошо понимает PHP и Битрикс.
- Qwen3.6-Heretic (35B MoE, Q8_0) - без цензуры, для творческих и общих задач.
- Qwen3.5-122B (MoE, UD-Q6_K, ~117 ГБ) - когда нужно максимальное качество и не жалко времени. Работает через swap, медленно (~4 t/s), но для сложных архитектурных вопросов незаменима.
- Gemma-4-12B (dense, Q8_0 / Q6_K / uncensored-Q4) - лёгкие dense-модели для быстрых задач.
- Qwen3.8-4B (dense, BF16 / Q8_0) - самая маленькая и мгновенная.
Все они висят в одном конфиге и переключаются одной командой.
Установка: драйвер и CUDA
Ставил свежие драйвер и CUDA Toolkit. У RTX 5060 Ti архитектура Blackwell (sm_120), поэтому нужен именно свежий тулкит, старые версии не поймут эту архитектуру.
# Драйвер NVIDIA
sudo apt install nvidia-driver-595
# CUDA Toolkit (в моём случае 13.2)
wget https://developer.download.nvidia.com/compute/cuda/
sudo sh cuda-toolkit.run --toolkit
# Проверка
nvidia-smi
Обязательно отключи Secure Boot в BIOS. Иначе после первого же обновления ядра DKMS пересоберёт nvidia-модуль под новый MOK-ключ, ключ не зарегистрирован, и драйвер перестанет грузиться с ошибкой "Key was rejected by service". Для домашнего сервера Secure Boot только мешает.
Сборка llama.cpp с MoE Hot-Cache
Скачивал форк, собирал под свою архитектуру. Ключевой момент - сборка с поддержкой CUDA именно для sm_120.
git clone https://github.com/adrianhoehne/llama.cpp-hotcache.git
cd llama.cpp-hotcache
mkdir build && cd build
cmake .. -DGGML_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES=120 \
-DGGML_NATIVE=OFF
make -j$(nproc)
На выходе получаем бинарник llama-server, который умеет в флаги MoE hot-cache. Проверить наличие можно так:
./bin/llama-server --help | grep -i moe-hot-cache
Куда складывать модели
Все модели лежат в отдельной папке, чтобы легко управлять размером и не путаться. Для многотомных GGUF (когда модель разбита на несколько файлов) llama.cpp сам подхватывает все части, если указать первый файл с суффиксом вида -00001-of-00004.
mkdir -p /opt/llm/LLM_MODELS/MY_MODELS
Качал с HuggingFace прямым curl-ом с поддержкой докачки. Удобно, когда оборвалось или комп переносили:
curl -L --fail --retry 3 -C - \
-o ModelName-Q8_0.gguf \
"https://huggingface.co/author/Model-GGUF/resolve/main/ModelName-Q8_0.gguf"
Флаг -C - позволяет продолжить докачку с места обрыва. Для больших моделей это спасение.
Конфиг моделей
Завёл файл models.conf, в котором каждая строка описывает одну модель. Поля разделены символом |:
# имя|путь|reasoning(off/on)|MOE_MAX_MIB|MOE_RESERVE_MIB|extra
# MoE модели (горячий кэш экспертов работает)
KAT-Coder-V2.5-Q8_0|/opt/llm/LLM_MODELS/MY_MODELS/KAT-Q8_0.gguf|off|-1|3072|
# Dense модели (hot-cache не нужен, MOE_MAX_MIB=0)
Gemma-4-12B-Q8_0|/opt/llm/LLM_MODELS/MY_MODELS/gemma-q8_0.gguf|off|0|3072|--ctx-size 155000
# Большая MoE с особыми флагами (колонка extra)
Qwen3.5-122B|/opt/llm/LLM_MODELS/MY_MODELS/qwen-00001-of-00004.gguf|on|-1|5120|--batch-size 1024 --ubatch-size 1024
Разберу поля:
- reasoning -
offотключает блок размышлений модели,onвключает. Для думающих моделей я ставлю лимит бюджета размышлений. - MOE_MAX_MIB - размер горячего кэша экспертов в мегабайтах.
-1это автосайзинг под свободную VRAM,0отключает hot-cache (для dense-моделей он не нужен), положительное число фиксирует размер. - MOE_RESERVE_MIB - резерв памяти, который не трогаем, чтобы хватило на KV-кэш и рабочие буферы.
- extra - дополнительные флаги, которые перекрывают базовые. Пригодилось для 122B (уменьшить batch) и для ограничения контекста у некоторых моделей.
Скрипт управления llm.sh
Написал простой менеджер llm.sh, который умеет показывать статус, запускать и останавливать любую модель из конфига. Вызывается так:
# Показать статус и список моделей
llm.sh status
# Запустить конкретную модель
llm.sh start KAT-Coder-V2.5-Q8_0
# Остановить
llm.sh stop
# Интерактивное меню
llm.sh menu
Скрипт собирает аргументы для запуска. Базовый набор общий для всех:
--ctx-size 200000 --batch-size 4096 --ubatch-size 4096 \
--n-gpu-layers 99 --flash-attn on \
--cache-type-k q8_0 --cache-type-v q8_0
Для MoE-моделей (когда MOE_MAX_MIB не ноль) добавляются флаги горячего кэша:
--cpu-moe \
--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-pp-reduce-merge auto \
--moe-hot-cache-shrink-low-water-mib 1024 \
--moe-hot-cache-refill-min-gain-mib 256 \
--auto-learn
--auto-learn собирает профиль активации экспертов под warmup-промпт. У меня warmup-промпт под Битрикс-кодинг: попросить написать PHP-функцию с MySQL и HTML-таблицей. Профиль сохраняется и переиспользуется, так что при следующем старте распределение экспертов уже точное.
Лимит размышлений для думающих моделей
У моделей вроде Qwen есть режим рассуждений (reasoning), когда модель сначала "думает" и выдаёт блок с размышлениями, а потом ответ. На длинных задачах этот блок может раздуться до десятков тысяч токенов и съесть всё время и контекст.
Ставлю жёсткий лимит бюджета размышлений, чтобы модель не улетала в бесконечное самокопание:
--reasoning on --reasoning-preserve --reasoning-budget 500
Модель может подумать, но не больше 500 токенов на размышления. Для кодинга этого обычно хватает. Когда лимит исчерпан, сервер принудительно закрывает блок думания тегом </think>, и модель переходит к обычному ответу. Ответ получается менее продуманным на сложных задачах, но он всегда будет целым - сервер не зависнет и не упадёт.
Параметры лимита задаются в конфиге по полю reasoning: on включает размышления с лимитом, off отключает их полностью.
Грабли 5: модель зацикливается в размышлениях
Тут я наступил на интересную граблю. Думающая модель иногда начинала генерировать одно и то же предложение внутри блока размышлений бесконечно - по кругу, пока не упрётся в лимит контекста. Выглядит как зависание, хотя процесс жив.
Причина - повтор токенов при "жадном" (низкотемпературном) сэмплинге, который используется на этапе размышлений. Любой малейший bias в распределении вероятностей выливается в зацикленное повторение одной фразы.
Лечится тремя параметрами, которые в llama.cpp борются именно с повторами:
--repeat-penalty 1.3 \
--presence-penalty 0.4 \
--frequency-penalty 0.4
Разбор:
- --repeat-penalty - штраф за повторение токенов. Значение 1.3 значит, что уже сгенерированный токен получает меньше шансов появиться снова. Это главный борец с зацикливанием.
- --presence-penalty - штраф за то, что токен уже встречался в тексте. Давит на повторение целых фраз.
- --frequency-penalty - штраф тем сильнее, чем чаще токен попадался. Отсекает очень частые повторы.
Один важный нюанс. В моём менеджере llm.sh сначала была глупая ошибка: переменная из конфига записывалась под именем reason, а проверялась как reasoning. Из-за этого значение из конфига никогда не подхватывалось, и все модели запускались с --reasoning off, как бы я их ни настраивал. Модель "думала как попало", а некоторые и вовсе без размышлений. Так что если у вас думающая модель ведёт себя не так, как настроена - в первую очередь проверьте, что скрипт реально читает нужную переменную и передаёт флаг в команду запуска (я проверял через ps -o args).
Анти-повтор флаги добавляю только думающим моделям (когда reasoning=on). Для кодеров без размышлений они не нужны - там точность важнее разнообразия, а без повторов в обычном ответе она и так редко зацикливается.
Грабли 1: CUDA OOM при старте большой модели
Первое, с чем я столкнулся, запуская 122B, - сервер падал на старте с ошибкой cudaMalloc failed: out of memory. Причина в том, что горячий кэш экспертов забирал много VRAM, а потом рабочему буферу не хватало места. Итоговое потребление (кэш + буферы) превышало свободную память карты.
Лечится так:
- уменьшить
--batch-sizeи--ubatch-size(с 4096 до 1024), это напрямую уменьшает размер рабочих буферов; - поднять
MOE_RESERVE_MIB, чтобы hot-cache сам не съел весь запас.
В конфиге для 122B это выглядит так:
Qwen3.5-122B|/opt/.../qwen-00001-of-00004.gguf|on|-1|5120|--batch-size 1024 --ubatch-size 1024
После этого 122B стартует и работает, хоть и медленно (через swap).
Грабли 2: модель "зависает" после закрытия SSH
Запускаешь сервер через SSH, закрываешь сессию - и модель молчит, хотя процесс живой. Долго не мог понять, пока не глянул статус процесса: он был в состоянии T (stopped).
Причина в том, что в команде запуска я перенаправил в лог только stdout и stderr, а stdin остался привязан к SSH-терминалу. Когда терминал закрывается, процесс пытается читать stdin, получает сигнал и замирает.
Лечение - перенаправить stdin из /dev/null, чтобы процесс полностью отвязался от терминала:
nohup "$SERVER" "${args[@]}" < /dev/null > server.log 2>&1 &
После этого закрывай SSH сколько хочешь, сервер продолжает работать.
Грабли 3: dense-модели не требуют hot-cache
По началу я было хотел применить ко всем моделям одинаковые флаги. Но у dense-моделей (Gemma-4-12B, Qwen3.8-4B) нет экспертов, значит и горячий кэш им не нужен. Для них ставлю MOE_MAX_MIB=0, тогда скрипт не добавляет MoE-флаги и модель стартует как обычная. Это и быстрее, и меньше мест для ошибок.
Грабли 4: контекст в VRAM для dense
У dense-моделей весь KV-кэш живёт в VRAM (в отличие от MoE, где часть уходит в hot-cache). Если модель весит 12 ГБ, а контекст огромный, KV-кэш может не влезть в 16 ГБ. Тогда ограничиваю контекст через поле extra:
--ctx-size 155000
Для лёгких моделей огромный контекст и не нужен, так что ничего не теряем.
Долгие закачки и перенос компа
Модели на 60-120 ГБ качаются часами. Качал в tmux-сессиях, чтобы закачка жила независимо от SSH. Когда понадобилось выключить комп и перенести, я просто остановил curl - файл остался на диске, а при включении докачал его с того же места флагом -C -. Ничего не потерял.
Что проверить сразу после запуска
curl localhost:8080/v1/models- сервер отвечает по OpenAI-совместимому API;curl localhost:8080/health- здоровье;nvidia-smi- сколько занято VRAM, не должно быть 0 и не должно упираться в потолок;- в логе запуска посмотреть строку про hot-cache budget, чтобы понять, сколько экспертов реально влезло;
- прогнать тестовый запрос с реальным кодом и посмотреть hit-rate и скорость decode.
Результаты
На RTX 5060 Ti 16 ГБ с форком llama.cpp и MoE Hot-Cache получается так:
- KAT-Coder-V2.5 (35B MoE, Q8_0): decode 42-45 t/s, hit-rate 50-70%.
- Qwen3.5-122B (117 ГБ, через swap): ~4 t/s. Медленно, но работает, что для такого размера уже чудо.
- Dense-модели (Gemma, Qwen3.8-4B) - почти полностью в VRAM, летают.
Всё отдаётся по OpenAI-совместимому API на порту 8080, так что к серверу цепляются любые инструменты: Continue, Kilo, любые клиенты. Локально, бесплатно, без интернета.
Итог
Связка Linux + llama.cpp с MoE Hot-Cache + RTX 5060 Ti 16 ГБ оказалась очень рабочей для разработчика. MoE-модели на 35B спокойно живут на 16 ГБ видеокарте и выдают приличную скорость, а большую модель можно поднять через swap, когда нужно качество.
Главные грабли, которые стоит держать в голове: не отключайте stdin при запуске через SSH (иначе процесс зависнет), для dense-моделей выключайте hot-cache, для больших MoE уменьшайте batch, ставьте лимит размышлений, а против зацикливания думания добавляйте --repeat-penalty и штрафы повторов. И обязательно проверяйте, что скрипт реально передаёт в команду те флаги, что вы настроили.
Всё, что описано в этом гайде, добыто методом проб и ошибок на реальном железе. Модель ошибается, я ошибаюсь, но вместе мы ошибаемся реже, чем я один без модели. Особенно когда речь о сигнатурах методов и событиях Битрикса.