Split-KV по реальной длине
Декодное внимание делит работу по фактической длине KV, а не по окну пула, и укладывается ровно в одну волну на SM. До правки половина блоков простаивала.
декод 45,8 → 47,2 т/с
Сервер локального инференса · Rust + CUDA
Yttri Forge — сервер инференса GGUF-моделей на собственном форке candle и своих CUDA-ядрах. Один бинарь yforge, API OpenAI и Anthropic, окно до 256K токенов.
RTX 4090 48 ГБ · Qwen3.8-27B Q8_0 · оба движка с окном 256K · сверка 18.09.2026
Замеры
Сравниваем с llama.cpp b10375 на одном стенде: одна модель, один протокол, движки по очереди. Каждый прогон — новый промпт, поэтому кеш промпта не помогает ни одному движку.
Один слот, без спекуляции · больше — лучше
Один слот, уникальный промпт · больше — лучше
Токенов в секунду · голова MTP, ширина 2 · больше — лучше
Прирост — к декоду yforge без MTP: 30,1 и 30,0 т/с. В сборке llama.cpp, с которой сравнивали, MTP нет. Цена — +0,16 ГиБ VRAM на 8K и +0,97 ГиБ на 32K.
Меньше — лучше
Декод каждого из четырёх запросов, т/с · промпт — 60 % окна · «пачка» — время всех четырёх
yforge ведёт слоты одним батчем: все четыре запроса идут с одной скоростью, и пачка завершается раньше — на 12 % при 8K и на 4 % при 32K. У llama.cpp слоты получают время неравномерно: на 32K один запрос идёт на 3,0 т/с, другой — на 24,4.
| Метрика | llama.cpp b10375 | yforge | Разница |
|---|---|---|---|
| Старт → первый токен | 10,03 с | 8,09 с | −1,94 с |
| Время до первого токена | 0,342 с | 0,250 с | −27 % |
| Префил, 8K | 1 826 т/с | 2 179 т/с | +19 % |
| Декод, 8K | 30,2 т/с | 30,8 т/с | +2 % |
| Префил, 32K | 2 100 т/с | 2 184 т/с | +4 % |
| Декод, 32K | 29,2 т/с | 30,1 т/с | +3 % |
| Префил, 128K | 1 677 т/с | 1 683 т/с | +0,4 % |
| Декод, 128K | 24,5 т/с | 27,0 т/с | +10 % |
| Префил, 256K | 1 233 т/с | 1 268 т/с | +3 % |
| Декод, 256K | 20,1 т/с | 23,4 т/с | +16 % |
| Видеопамять, 8K и 32K | 36 659 МиБ | 35 177 МиБ | −1,45 ГиБ |
| Видеопамять, 128K | 36 659 МиБ | 35 209 МиБ | −1,42 ГиБ |
| Видеопамять, 256K | 36 659 МиБ | 35 241 МиБ | −1,39 ГиБ |
RTX 4090 48 ГБ, Qwen3.8-27B-Q8_0.gguf, 18.09.2026. yforge: GPU_ONLY=1, KV_POOL_Q8=1, CUDA_GRAPHS=1, PGRAPH=on, FLASH_ATTN=1. llama.cpp: -ngl 999 -fa on --cache-type-k q8_0 --cache-type-v q8_0. MTP и четыре слота — на графиках, все значения подписаны.
Пять чередующихся пар, F16-KV · меньше — лучше
yforge быстрее во всех пяти парах: в среднем 19,661 против 19,716 с. Шкала начинается с 19,60 с, чтобы разница была видна.
Те же пять пар, 200 токенов · больше — лучше
yforge быстрее во всех пяти парах: в среднем 47,11 против 46,81 т/с. Шкала начинается с 46,7 т/с.
int8-KV пул и int8-QK · тот же стенд и та же модель
Префил в 128K-профиле пока уступает: 20,4 против 19,8 с на 30K — это цена страничного префила. В 64K-профиле с F16-пулом префил быстрее llama.cpp во всех пяти парах, см. выше.
| Замер | llama.cpp b10375 | yforge | Разница |
|---|---|---|---|
| Префил, F16-KV, пять пар | 19,716 с | 19,661 с | −0,3 % |
| Декод, F16-KV, пять пар | 46,81 т/с | 47,11 т/с | +0,6 % |
| Декод, int8-KV, окно 128K, три пары | 45,20 т/с | 45,60 т/с | +0,9 % |
| Префил, int8-KV, окно 128K | 19,80 с | 20,40 с | +3 %, уступаем |
Стенд yttri-win: Windows, RTX 3060 12 ГБ. Один файл Ornith-1.5-9B-Q4_K_M.gguf для обоих движков, движки по очереди, flash-attention у обоих.
Повтор одного текста попадает в кеш промпта llama.cpp и даёт невозможные 7 781 т/с. Поэтому каждый прогон — новый текст.
Считаем по usage.completion_tokens, а не по SSE-чанкам: при спекуляции один чанк несёт несколько токенов.
Одна карта, чередующиеся пары, никогда одновременно. Одиночный прогон ничего не доказывает.
Оба движка с окном 256K. Малое -c у llama.cpp давало ложный перерасход памяти у нас — это разница конфигураций.
Воспроизвести:python3 scripts/bench/final_verify.pyхарнессы лежат в репозитории сервера, пути настраиваются в шапке скрипта
Почему быстрее
Мы профилируем шаг декода по ядрам, сравниваем с llama.cpp покернельно в nsys и чиним найденное. Вот правки, которые дали отрыв.
Декодное внимание делит работу по фактической длине KV, а не по окну пула, и укладывается ровно в одну волну на SM. До правки половина блоков простаивала.
декод 45,8 → 47,2 т/с
Ключи читаются из int8-страниц как есть, запрос квантуется прямо в ядре, строки staging выровнены против конфликтов банков памяти.
q8-KV быстрее f16: +8,3 % на 127K
Пул выделяется на всё окно при старте, и KV пишется в него сразу, без F16-копии. int8 вдвое экономит память при ошибке round-trip 0,75 %.
окно 128K в 12 ГБ: пул 2 ГБ вместо 4
Запись кеша — ссылка на страницы пула: поколение слота и длина. 50 МиБ вместо гигабайтной копии KV, и длинный промпт не считается заново.
повтор 127K-промпта: 138,8 → 2,3 с
Черновые токены предсказывает встроенная MTP-голова модели, основная проверяет их пачкой. Черновику хватает укороченного словаря — ошибку отсеет проверка.
+22 % на 8K, +43 % на 32K
Планировщик ведёт слоты одним батчем: четыре клиента получают одинаковую скорость, а не очередь с разбросом.
4 × 25,5 т/с против 12,4–25,7
Шаг декода захвачен в CUDA-граф. Residual с RMSNorm и подготовка внимания — по одному ядру, отбор top-k — за один проход.
короткий декод 51,6 → 53,9 т/с
Порт открывается только после загрузки весов, а страничный пул прогревается при старте. Первый запрос не платит за 8,2 ГиБ аллокаций.
первый запрос: 5,7 → 0,25 с
Цифры из журнала замеров проекта: RTX 3060 12 ГБ с Ornith-1.5-9B и RTX 4090 48 ГБ с Qwen3.8-27B.
Почему Rust
Сервер и движок написаны на Rust, горячие ядра — на CUDA. Всё собирается одной командой cargo в один исполняемый файл.
yforge.exe на Windows, yforge на Linux. Ни Python, ни pip, ни контейнеров: нужны только драйвер NVIDIA и CUDA-рантайм, модели — обычные GGUF-файлы.
Буферы CUDA, графы и слоты освобождаются по правилам владения, а не ручным free. Критерий релиза — длинные генерации в четырёх слотах без падений и утечек VRAM.
Снимки префикс-кеша уходят в фоновый поток, top-k сэмплера считается параллельно в rayon, HTTP — на tokio и axum. Общий буфер без синхронизации не пропустит компилятор.
Форк candle, в котором мы сами правим ядра: FlashAttention-2 со страничным split-KV, MMQ на Tensor Cores, Gated DeltaNet, MoE. llama.cpp для нас — эталон сверки, а не зависимость.
-m, -c, --ngl, --fa, логи в stdout и stderr. Флаг важнее переменной окружения, окружение — env-файла. Переезд с llama-server почти не трогает скрипты.
Клиенты
HTTP · Bearer-ключ · порт 18099
Планировщик слотов · префикс-кеш · сэмплер · горячая смена моделей
один процесс, без IPC
cudarc · Metal · CPU
Подключайте агентов и чаты, которые уже работают с OpenAI или Anthropic: достаточно сменить Base URL на http://<хост>:18099/v1.
POST /v1/chat/completionsOpenAI Chat CompletionsСтриминг SSE, tools и tool_choice, usage
POST /v1/responsesOpenAI ResponsesInput → output, стриминг SSE
POST /v1/messagesAnthropic MessagesСтриминг, tools, заголовок anthropic-version
GET /v1/modelsСписок моделейКонтекст, квант, слоты и действующие пресеты сэмплинга
Запрос к Chat Completions
curl http://localhost:18099/v1/chat/completions \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"model": "ornith-1.5-9b",
"stream": true,
"messages": [{"role": "user", "content": "Привет!"}]
}'
Ключи именованные: API_KEYS='[{"key": "…", "name": "codex"}]' — удобно раздавать клиентам и отзывать по одному.
Модели
Семейства Qwen 3.5, 3.6 и 3.8, Ornith и Gemma 4 в формате GGUF — от 4B до MoE 35B-A3B. Модель меняется без перезапуска: POST /admin/switch находит файлы в MODELS_DIR.
| Модель | Тип | Кванты | Стенд | Что проверено |
|---|---|---|---|---|
Qwen3.8-27Bсверка 13/13 |
Гибрид DeltaNet | Q8_0UD-IQ2_XXS |
RTX 4090 48 ГБ, RTX 3060 12 ГБ | Финальная сверка с llama.cpp, окно 256K, MTP |
Ornith-1.5-9Bпрод |
Гибрид DeltaNet | Q4_K_MQ6_KMTP Q8_0 |
RTX 3060 12 ГБ | Прод-профиль с окном 128K, пары с llama.cpp |
Qwen3.6-35B-A3B |
MoE, 3B активных | UD-IQ2_XXSUD-Q2_K_XL |
RTX 3060 12 ГБ | Эксперты в pinned RAM, горячие — в VRAM, окно 131K |
Qwen3.6-27B |
Гибрид DeltaNet | UD-Q2_K_XLIQ2_XXS |
RTX 3060 12 ГБ | Первая целевая модель: четыре слота, три API |
Qwen3.5-9B |
Гибрид DeltaNet | Q4_K_M |
RTX 3060 12 ГБ | Префикс-кеш с точками ветвления для агентов |
Qwen3.5-4B |
Гибрид DeltaNet, мультимодальная | Q4_K_MQ6_KQ8_0 |
RTX 3060 12 ГБ | Текст, изображения и видео, MTP, матрица против llama.cpp |
Gemma 4 E4B-it |
Трансформер | Q8_0Q4_K_M |
RTX 3060 12 ГБ | Пресеты сэмплинга семейства, длинный префил чанками |
Установка
Единственный артефакт — исполняемый файл yforge. Первая сборка CUDA-ядер занимает около 25 минут, повторные — секунды из кэша.
Драйвер NVIDIA с поддержкой CUDA 12.4+, CUDA Toolkit 13.2 с архива NVIDIA, Visual Studio 2022 Build Tools и Rust с MSVC-тулчейном.
winget install --id Git.Git -e
winget install --id Rustlang.Rustup -e
winget install --id Microsoft.VisualStudio.2022.BuildTools -e `
--override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
Движок и сервер кладутся рядом: сервер подключает движок относительным путём ../yttri-forge.
cd D:\Projects
git clone https://github.com/askidmobile/yttri-forge.git
git clone https://github.com/askidmobile/qwen36-server.git
CUDA_COMPUTE_CAP под вашу карту: 86 — RTX 30xx, 89 — RTX 40xx, 120 — RTX 50xx. Результат — target\release\yforge.exe.
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat" -arch=x64
set CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.2
set CUDA_COMPUTE_CAP=86
cd qwen36-server
cargo build --release --features cuda --bin yforge
Рабочий профиль для карты на 12 ГБ: окно 128K, int8-пул, CUDA-графы, один слот.
target\release\yforge.exe --model D:\Models\Ornith-1.5-9B-Q4_K_M.gguf ^
--api-key my-secret-key --ctx 131072 --slots 1 ^
--kv-pool q8 --cuda-graphs 1
Процесс завершается молча? Бинарь линкуется с cudart 12.4 — поставьте CUDA\v12.4\bin первым в PATH.
Процесс, запущенный из ssh-сессии, умирает вместе с ней — регистрируйте задачу планировщика с запуском при старте системы.
rem run-yforge.bat
set "PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin;%PATH%"
cd /d D:\Projects\qwen36-server
target\release\yforge.exe --env .env 1>>D:\Projects\logs\server.log 2>&1
rem регистрация и запуск задачи
schtasks /create /tn yforge /tr "cmd /c D:\Projects\run-yforge.bat" /sc onstart /ru SYSTEM
schtasks /run /tn yforge
Драйвер NVIDIA, CUDA Toolkit 12.4+, build-essential и Rust. Проверено на Ubuntu 24.04.
sudo apt install -y build-essential git
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Движок и сервер кладутся рядом: сервер подключает движок относительным путём ../yttri-forge.
git clone https://github.com/askidmobile/yttri-forge.git
git clone https://github.com/askidmobile/qwen36-server.git
CUDA_PATH обязателен: ядра FlashAttention линкуются со статическим cudart. CUDA_COMPUTE_CAP — 86, 89 или 120 под поколение карты.
export CUDA_PATH=/usr/local/cuda-12.8
export CUDA_COMPUTE_CAP=89
cd qwen36-server
cargo build --release --features cuda --bin yforge
Профиль финальной сверки: Qwen3.8-27B Q8_0 с окном 256K на карте 48 ГБ.
# prod.env
MODEL=/models/Qwen3.8-27B-Q8_0.gguf
API_KEYS='[{"key":"change-me","name":"primary"}]'
CTX=262144
CONTEXT_LIMIT=262144
GPU_ONLY=1
KV_POOL_Q8=1
KV_CACHE_DTYPE=q8
CUDA_GRAPHS=1
PGRAPH=on
./target/release/yforge --env prod.env
Задавайте и CTX, и CONTEXT_LIMIT: без второго окно молча ограничится 81 920 токенами.
Сервис поднимается при загрузке и перезапускается при сбое, логи — в journald.
# /etc/systemd/system/yforge.service
[Unit]
Description=Yttri Forge inference server
After=network.target
[Service]
ExecStart=/opt/yforge/yforge --env /opt/yforge/prod.env
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now yforge
journalctl -u yforge -f
macOS с Metal и чистый CPU — сборки для разработки: cargo build --release --features metal или без фич. Флаги --cuda-graphs, --kv-pool и MTP относятся к CUDA-пути.
Харнессы сравнения лежат в репозитории: final_verify.py поднимает оба движка с окном 256K и снимает те же 13 метрик.