1. Задача: поднять локальный LLM-сервер, который не падает на длинных контекстах
Требования: контекст не меньше 190K токенов (это жёсткое требование для агентных сессий с большим кодом), адекватная скорость генерации, бесплатный запуск.
Железо:
- CPU: AMD Ryzen 5 7500F (6 физических ядер / 12 потоков)
- RAM: 64 ГБ DDR5
- GPU: NVIDIA RTX 5060 Ti 16 ГБ (Blackwell, sm_120)
- Диск: NVMe 1 ТБ
- ОС: Linux Mint 22 (база Ubuntu 24.04)
Модель: Ornith-1.0-35B, архитектура Qwen-MoE (qwen35moe). Всего 40 слоёв, в каждом 256 экспертов, активных 8 (top-k). Есть две копии весов: GGUF Q8_0 (35 ГБ) и полные safetensors BF16 (66 ГБ).
2. Почему KTransformers, а не llama.cpp
llama.cpp размещает слои MoE по номерам: первые N слоёв на GPU, остальные на CPU. Тупое последовательное деление, без учёта того, какие эксперты реально работают.
KTransformers размещает по частоте использования: горячие эксперты (которые роутер реально дёргает) живут в VRAM, холодные на CPU. В теории умеет динамически перекладывать экспертов по ходу работы (об этом отдельный нюанс ниже, там не всё гладко).
На практике это даёт примерно 20-24 t/s на 35B модели при 16 ГБ VRAM, когда модель целиком в VRAM не влезает.
3. Что нужно для запуска: две копии модели
Для KTrans нужны ОБЕ копии весов одной модели:
- safetensors (BF16, полные) - для GPU-части
- GGUF (Q8_0, квантованный) - для CPU-части
Это не дубль. Это два разных набора весов для двух разных вычислителей. Без safetensors GPU-экспертам неоткуда взять точные веса, без GGUF CPU-часть считала бы в 2 раза больше байт.
4. Как устроены эксперты: BF16 на GPU, Q8 на CPU
Тут важный нюанс, который легко понять неправильно:
- GPU-эксперты = BF16 из safetensors (полная точность)
- CPU-эксперты = Q8_0 из GGUF (квантованные)
- Внимание, эмбеддинги, норм-слои, роутер - всегда на GPU в BF16
То есть "мозг" (что считать и как связывать токены) максимально точный, а "работяги" (масса экспертов) сжаты до 1 байта на вес. Это и есть гениальность гибридной схемы.
Было ошибкой считать, что горячие эксперты на GPU в Q8_0. Нет: код sglang-kt копирует GPU-экспертов из safetensors в BF16. Отсюда честный расчёт VRAM: один эксперт Ornith = 6 МБ в BF16 (или 3.2 МБ в Q8_0).
5. Установка KTransformers
Установка в отдельный venv, CUDA 12.8 для Blackwell:
python3 -m venv /opt/ktrans/venv
/opt/ktrans/venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cu128
/opt/ktrans/venv/bin/pip install kt-kernel sglang-kt
/opt/ktrans/venv/bin/pip install nvidia-cudnn-cu12==9.16.0.29
sudo apt install ninja-build
Проверка что всё поднялось: curl сервера должен отвечать по OpenAI-совместимому API.
6. Финальный конфиг запуска
Проверенный рабочий конфиг для Ornith-35B на 16 ГБ VRAM с контекстом 200K:
python -m sglang.launch_server \
--host 0.0.0.0 --port 8080 \
--model /models/ornith-35b-safetensors \
--kt-weight-path /models/ornith-35b/Ornith-1.0-35B-Q8_0.gguf \
--kt-cpuinfer 6 --kt-threadpool-count 1 \
--kt-num-gpu-experts 4 \
--kt-expert-placement-strategy uniform \
--kt-method LLAMAFILE \
--attention-backend triton \
--trust-remote-code --mem-fraction-static 0.90 \
--max-running-requests 1 --max-total-tokens 200000 \
--max-mamba-cache-size 23 \
--chunked-prefill-size 4096 --enable-mixed-chunk \
--served-model-name ornith-35B-Q8_0 \
--reasoning-parser qwen3-thinking \
--tool-call-parser qwen3_coder
Каждый флаг тут стоит внимания, и за несколькими спрятаны грабли, которые мы откопали. Обратите внимание: в конфиге НЕТ --kt-enable-dynamic-expert-update. Почему - в нюансе 4, это важно.
7. Нюанс 1: mem-fraction-static и настоящий KV-кэш
mem-fraction-static это доля видеокарты, которую sglang отдаёт под рабочую зону: KV-кэш + модель + эксперты. Остальное резерв под буферы.
Мы стартовали с 0.72 (по советам из старых статей) и получили сюрприз. Таблица замеров (на ранней конфигурации с 400 экспертов и без лимита mamba):
| mem-fraction | KV-кэш (токенов) | Вердикт |
|---|---|---|
| 0.72 | 82K | контекст режется, плохо |
| 0.85 | 168K | лучше |
| 0.88 | 182K | близко |
| 0.90 | 189K | почти |
| 0.92 | 198.9K | цель достигнута |
Проблема: с mem-fraction 0.72 контекст молча обрезался до 82K токенов. Клиент думал, что шлёт 200K, а сервер резал на 82K. Реальный размер KV-кэша видно только в логах запуска, строка "KV Cache is allocated. #tokens".
Вывод: если нужен честный большой контекст, mem-fraction надо поднимать. И всегда проверять фактический #tokens в логе, а не верить на слово.
8. Нюанс 2: сколько экспертов реально влезает
Сначала думал так: поставлю 400 экспертов (10/слой), места много. Арифметика: 400 x 6 МБ BF16 = 2.4 ГБ, вроде влезает. Не влезло вместе с 200K контекстом.
| Экспертов (на слой) | Всего | KV-кэш получился |
|---|---|---|
| 10 | 400 | 82K |
| 5 | 200 | 198.9K (при mem-fraction 0.92) |
| 4 | 160 | 200K ровно (при mem-fraction 0.90) |
Ограничитель не один: эксперты + KV-кэш + плотные слои должны поместиться в рабочий пул. Плотная часть (внимание, эмбеддинги, mamba-состояния) занимает ~6.7 ГБ и её не трогать. Остальное делим между экспертами и KV.
Рабочая формула для 16 ГБ и 200K: 160 экспертов (4/слой) + mem-fraction 0.90 дают KV ровно 200000 и свободные 3.25 ГБ. Хот-сет Битрикс-кодинга это 50-80 экспертов, так что 160 это 80 рабочих + 80 запасных.
Шаг не фиксирован: --kt-num-gpu-experts задаётся на слой (целое), итог = N x 40 слоёв. 300 ровно нельзя целым (300/40=7.5), тогда --kt-gpu-experts-ratio.
9. Нюанс 3: frequency без статистики даёт кривое распределение
Мы запустили с --kt-expert-placement-strategy frequency и обнаружили в логах странность: все 400 экспертов свалились в 3 слоя (25:80, 26:192, 27:128), остальные 37 слоёв пустые.
Разобрались по исходникам: frequency без файла активаций (--init-expert-location) ставит всем экспертам частоту 1.0 и делает torch.topk по всей модели. При равных весах topk выбирает произвольно, отсюда кучкование.
Нюанс: frequency имеет смысл ТОЛЬКО с файлом статистики активаций. Без него лучше uniform: тот раскладывает ровно по 4 эксперта на слой.
10. Нюанс 4: dynamic update ЛОМАЕТ LLAMAFILE
Главный грабли, на которые мы потратили больше всего времени. Документация обещает: включи --kt-enable-dynamic-expert-update + --kt-gpu-prefill-token-threshold, и эксперты будут перекладываться в VRAM на лету под текущую задачу.
Реальность: на LLAMAFILE-бэкенде это валит сервер. При первом же перераспределении экспертов (когда наступает prefill длиннее порога):
AttributeError: 'LlamafileMoEWrapper' object has no attribute 'submit_write_weight_scale_to_buffer'
SIGQUIT received. It usually means one child failed.
Причина по исходникам: метод submit_write_weight_scale_to_buffer существует только у NativeMoEWrapper (бэкенды FP8/RAWINT4/BF16). У LlamafileMoEWrapper и AMXMoEWrapper его нет, это недостроенная функция, не намеренно выброшенная. Пока нет фикса (все версии до 0.6.4 включительно). Есть открытый issue в репозитории ktransformers, и это ограничение --kt-method, а не конкретного железа.
Вывод: на LLAMAFILE dynamic update не включаем. Используем static uniform. Полноценная динамика (перераспределение экспертов на лету) на LLAMAFILE станет возможна, когда разработчики допилят метод для LlamafileMoEWrapper. Альтернатива уже сейчас: собрать профиль частот через --init-expert-location и дать его frequency на старте - маска будет точной под ваши задачи.
11. Нюанс 5: max_model_len в API обманчив
После запуска /v1/models отвечает max_model_len: 262144, красиво и больше нашего требования. Но это потолок модели из конфига, а не реальный KV-кэш. Реальный KV-кэш виден только в логе запуска.
Проверка: journalctl -u ktransformers | grep "KV Cache is allocated". Если там меньше 200000, контекст будет обрезаться молча.
12. Нюанс 6: kt-cpuinfer считаем по физическим ядрам
--kt-cpuinfer это число потоков CPU для экспертов. Документация прямо говорит: физические ядра, а не hyperthreads. У Ryzen 7500F физических ядер 6, поэтому kt-cpuinfer 6. Не поднимать выше.
13. Нюанс 7: Secure Boot ломает драйвер после обновления ядра
Сначала сервер работал. Потом apt upgrade обновил ядро, DKMS пересобрал nvidia-модуль под новый MOK-ключ, а ключ не был зарегистрирован в Secure Boot. Начало сыпаться "modprobe: Key was rejected by service".
Лечится отключением Secure Boot в BIOS или регистрацией MOK-ключа через mokutil. Для домашнего сервера проще отключить Secure Boot: он только мешает при каждом обновлении ядра.
14. Нюанс 8: Blackwell требует attention-backend triton
RTX 5060 Ti это Blackwell (sm_120). Для гибридных GDN-моделей sglang на Blackwell разрешает только два backend: triton или trtllm_mha. Без --attention-backend triton будет AssertionError.
Из этого вытекает ещё один нюанс: FP8 KV-кэш (--kv-cache-dtype fp8_e4m3) на triton backend не работает, потому что в triton-кернелах нет FP8. Если захочется экономить VRAM на KV через FP8, придётся менять весь attention-путь на trtllm_mha, а его докстринг заявляет поддержку только sm100. Мы решили не трогать и оставить KV в BF16.
15. Нюанс 9: лимит mamba-кэша обязателен
Сначала этого нюанса в статье не было, но он чуть не похоронил сервер. У гибридных моделей (30 linear-attention слоёв из 40) есть mamba-подобные состояния, и sglang без лимита выделяет под них с запасом.
Без --max-mamba-cache-size 23 при mem-fraction 0.92 сервер выделил 3.46 ГБ под mamba-кэш, свободно осталось 1.2 ГБ. Когда Kilo запустил компакцию контекста (большой prefill на ~155K токенов), ей понадобилось 2.5 ГБ временных буферов - произошёл OOM, сервер упал с Connection reset.
С лимитом 23: mamba-кэш занимает 1.41 ГБ, свободно 3.25 ГБ, компакция проходит спокойно. Дополнительно в Environment юнита добавили PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True против фрагментации памяти.
Второй урок про компакцию: на больших контекстах (150K+) компакция это самый жадный момент, она резко резервирует ~2.5 ГБ буферов. Плюс снизили --chunked-prefill-size с 8192 до 4096, чтобы пик памяти при prefill был вдвое меньше.
16. Результаты: скорость и распределение
Реальные замеры на рабочей нагрузке (агентная сессия, кодинг):
- Decode: 21-24.5 t/s при контексте 16-46K токенов
- Prefill прогретый: до 290 t/s (первый холодный запрос медленный, дальше быстрый)
- Radix-кэш работает: повторные префиксы не пересчитываются (cached-token)
- GPU util во время генерации: 43%
- CPU util: 63% (шесть потоков считают экспертов)
- VRAM в работе: 13.5-15 ГБ из 16.3
Финальная конфигурация: 160 экспертов (4/слой, uniform), KV-кэш ровно 200000 токенов, свободно 3.25 ГБ. Сервер стабильно работает часами, включая компакции на 150K+ контекстах.
17. Что проверить сразу после запуска
- curl localhost:8080/v1/models - сервер отвечает
- journalctl -u ktransformers | grep "KV Cache is allocated" - смотрим настоящий #tokens, должно быть >= 190K
- journalctl -u ktransformers | grep "total GPU experts" - смотрим сколько экспертов реально на GPU
- nvidia-smi - сколько свободно VRAM, не должно быть 0
- Прогнать длинный запрос 20K+ токенов, проверить что не OOM
- Проверить компакцию: загнать контекст на 150K+, убедиться что сервер не падает
18. Планы на эксперименты
- Профиль частот (--init-expert-location): сервер умеет собирать статистику активаций экспертов (--expert-distribution-recorder-mode stat, буфер можно сделать бесконечным). Прогнать реальные задачи, выгрузить профиль, скормить frequency - стартовое распределение станет точным под ваши задачи.
- Deferral экспертов (--kt-max-deferred-experts-per-token): CPU считает прошлый токен, пока GPU считает текущий. Обещает +45% к decode при потере точности меньше 0.5%. Начинать с 1.
- Динамическое перераспределение: ждём фикса в ktransformers (issue про LlamafileMoEWrapper), тогда вернём --kt-enable-dynamic-expert-update.
Итог
KTransformers реально работает на 16 ГБ видеокарте с 35B MoE моделью и честным 200K контекстом. Скорость 21-24 t/s комфортна для агентной работы.
Но конфигурация требует внимания, главные грабли: mem-fraction-static (молча режет контекст, смотрите #tokens в логе), частота экспертов (frequency без профиля даёт кривое распределение, используйте uniform), лимит mamba-кэша (без него OOM на компакции) и dynamic update (на LLAMAFILE пока не работает, ждём фикс).
Проверяйте фактический KV-кэш в логах, а не max_model_len из API. И помните: всё, что вы видите в этом гайде, добыто методом проб и ошибок на реальном железе.