Всем привет, с вами Низамов Илья. Погонял модель unsloth/Qwen3.8-27B-NVFP4 с разными параметрами запуска. Главным параметром для меня изначально был максимальный размер контекста на двух видеокартах 5070 Ti по 16 ГБ. Использовать эту модель я хотел вместе с Claude Code, поэтому параллелизм подобрал небольшой и уменьшил батчи.
Ниже — пять рабочих конфигураций с замерами: четыре в vLLM и одна в llama.cpp, куда я добавил третью карту. Плюс разбор параметров: что каждый флаг реально даёт и какие эксперименты за этим стоят. В конце — как подключить всё это к Claude Code.
Содержание
- Стенд и оговорки
- Конфигурация 1: максимальный контекст, 262 144 токена
- Конфигурация 2: рабочая, 96k и максимальная скорость
- Конфигурация 3: MTP, но контекста больше
- Конфигурация 4: убираем MTP и смотрим максимум
- Почему нельзя взять и контекст, и скорость сразу?
- Конфигурация 5: спасёт ли третья видеокарта?
- Подключаем к Claude Code
- Сводная таблица
- Что осталось непроверенным
Стенд и оговорки
| GPU | 2× RTX 5070 Ti, 16 303 Mb (15.47 Gb полезных), SM 12.0 |
| Третья карта | RTX 3090 — участвует только в разделе про llama.cpp |
| Межсоединение | PCIe, без NVLink |
| Модель | unsloth/Qwen3.8-27B-NVFP4 на Hugging Face, чекпойнт 21.81 Gb |
| GGUF-сборка | unsloth/Qwen3.8-27B-GGUF, квант UD-Q8_K_XL — для раздела про llama.cpp |
| Движки | vLLM 0.27.1, llama.cpp |
Материнская плата и процессор потребительские, линии PCIe раздаются урезанно. На скорость это влияет, насколько — честно оценю, когда переберусь на серверную платформу. 3090 в тестах vLLM не участвует: NVFP4 она не умеет, как и половину современных механизмов.
Если у вас та же модель работает иначе — присылайте параметры запуска: движок, точную сборку модели с Hugging Face, флаги. Запуск через обёртки вроде Ollama или LM Studio сравнивать не с чем: там либо другая модель, либо квант уровня Q2, либо часть слоёв уехала в оперативную память.
Еще бывает так, что вы скопировали команду, а у вас не запустилось и у меня тоже такое бывает, когда я перепроверяю статью на том же самом железе. Не знаю от чего это зависит, но обычно движок просит еще уменьшить контекст, в общем относитесь к данным командам как предельно возможным которые возможно не заведутся у вас.
Что означают эти три метрики?
Три метрики, и для агента вроде Claude Code они значат разное:
- Генерация, ток/с — сколько токенов в секунду модель пишет в ответ. Это то, что видно глазами, когда ответ печатается.
- TTFT — время до первого токена на коротком промпте. Отзывчивость: сколько агент «думает», прежде чем начать отвечать.
- Префилл, ток/с — скорость обработки входа. Для агента это главная метрика: каждый запрос Claude Code тащит системный промпт, содержимое файлов и историю. На промпте в 100 тысяч токенов разница между 1 073 и 354 ток/с — это полторы минуты против почти пяти.
Все замеры сняты батчем 1, без нагрузки: конфигурация и настраивалась под одного пользователя.
Конфигурация 1: максимальный контекст, 262 144 токена
Суммарно мы имеем 32 ГБ VRAM, и для запуска с максимальным контекстом можно использовать команду ниже. Но за счёт экстремального квантования кэша я бы её, наверное, не использовал в основной работе. Плюс модель запускается без vision-части, а это кому-то может быть важно.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0 \
CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1,2 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
--tensor-parallel-size 2 --gpu-memory-utilization 0.965 \
--kv-cache-dtype turboquant_4bit_nc --language-model-only \
--kv-cache-memory-bytes 2400000000 \
--max-num-seqs 8 --max-num-batched-tokens 1024 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
--enable-prefix-caching --max-model-len 262144| Метрика | Значение |
|---|---|
| Последовательная генерация | 53.76 ток/с |
| TTFT, короткий промпт | 0.546 с |
| Префилл | 583 ток/с |
Разбор параметров
--gpu-memory-utilization 0.965. Стандартные 0.95 оставляют на столе примерно четверть гигабайта. Выше 0.9689 подняться физически нельзя: свободно на карте 14.99 из 15.47 Gb. Значение 0.98 не стартует вообще, а подсказки самой vLLM в логе («увеличьте до 0.9841», в другом прогоне — до 0.9991) невыполнимы: они считают от полного объёма карты, а не от свободного. Прибавка от 0.95 к 0.965 — 8 192 токена.
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0. С версии 0.21 vLLM резервирует память под CUDA-графы заранее, на этапе профилирования. Переменная отключает оценку: графы остаются и захватываются как обычно, просто резерв не вычитается из бюджета KV. Освобождается 0.53 Gb — плюс 20 935 токенов. Плата тонкая: движок теперь берёт память под графы сверх запрошенного бюджета, физического запаса остаётся около 20 Mb.
--language-model-only. Модель мультимодальная: помимо 64 текстовых слоёв в ней 27 блоков vision-энкодера. Флаг убирает их: веса на карту падают с 10.67 до 10.24 Gb, доступный KV растёт с 2.64 до 3.31 Gb, потолок контекста — на 26 %. Прибавка к KV больше экономии на весах, потому что vision перестаёт участвовать ещё и в профилировании пиковых активаций.
--kv-cache-dtype turboquant_4bit_nc. Главный рычаг и главный компромисс. TurboQuant — это post-training квантование KV-кэша от Google Research: поворот вектора преобразованием Адамара, дальше квантование ключей по Ллойду-Максу и значений равномерно. Дообучения не требует, в vLLM 0.27.1 это готовый флаг с четырьмя пресетами.
Что стоит знать до включения:
- Заявленные «в шесть раз меньше памяти» считаются от FP16, а у нас KV уже в FP8. Честная дельта к текущему состоянию:
k8v4даёт ×1.32,4bit_nc— ×1.95,k3v4_nc— ×2.23,3bit_nc— ×2.59. - Качество платное, и цифры лежат в самом коде vLLM: рост перплексии +1.17 % у
k8v4, +2.71 % у4bit_nc, +10.63 % уk3v4_ncи +20.59 % у3bit_nc. Последний пресет на гибридных моделях лучше не трогать вовсе — там нет защиты граничных слоёв. - Сжимаются только 16 слоёв из 64: состояния mamba-слоёв TurboQuant не трогает. Поэтому отдача растёт вместе с длиной контекста — на 8k экономия около 31 % памяти на запрос, на 32k уже 43 %.
- Реализация в vLLM неполная: вторая ступень (QJL, однобитный остаток) выброшена намеренно — в комментарии к коду сказано, что несколько независимых групп нашли ухудшение качества внимания.
Вот из-за этой платы качеством я и не сажусь на четыре бита в постоянной работе. Осмысленность на коротких промптах и поиск «иголки» в четверти миллиона токенов конфигурация проходит, но перплексию я не мерил, а единственное замеченное отличие — на четырёх битах текст заметнее скатывается в перечисление, тогда как FP8 держит связный абзац.
--kv-cache-memory-bytes 2400000000. Самый неочевидный флаг: он не добавляет контекста, он возвращает память рантайму. Если отдать TurboQuant всё, что есть, сервер стартует, отвечает на короткие запросы и выглядит здоровым — а потом приходит длинный промпт, и движок умирает:
File ".../vllm/v1/attention/backends/turboquant_attn.py", line 857, in _continuation_prefill
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 204.00 MiB.
GPU 0 has a total capacity of 15.47 GiB of which 202.06 MiB is free.
...
vllm.v1.engine.exceptions.EngineDeadErrorДвухсот четырёх мегабайт не хватило при двухстах двух свободных. При поэтапном префилле TurboQuant распаковывает уже сохранённый префикс, и временный буфер под это растёт вместе с длиной префикса. Профилировщик памяти его не видит: профилирование идёт с крошечным KV-кэшем, где префикса ещё нет. 2.4 ГБ — чуть больше, чем нужно на 262 144 токена, остальное сознательно оставлено движку. С 2.2 ГБ проходит промпт на 235 тысяч токенов, с 2.4 ГБ — на 250 тысяч.
--max-num-seqs 8 и --max-num-batched-tokens 1024. Здесь честно: на этой конфигурации они не дают ни одного токена контекста — проверял, 2.64 Gb и те же цифры бит в бит при значениях 2 и 512. Раньше --max-num-seqs был решающим флагом, без которого сервер вообще не поднимался, но экономил он через профилировщик графов — а мы его только что отключили переменной окружения. Оставляю их, потому что стенд однопользовательский и большие батчи мне не нужны, а не потому, что они экономят память.
--enable-prefix-caching. Вот это для Claude Code важно: агент шлёт один и тот же системный промпт и одни и те же файлы запрос за запросом. Кэш префикса позволяет не считать их заново.
Эксперименты: как контекст рос с 8k до 262k
Отправная точка была скромной — 8 192 токена. Путь до полного контекста занял шесть флагов, и два из них дали ровно ноль. Ниже — та же лестница по шагам:
Отдельно про методику. Число GPU KV cache size в логе — не потолок контекста, оно показывает расклад для текущей длины и ошибается в полтора раза. Правильный способ: задать заведомо недостижимый --max-model-len и прочитать из ValueError строку estimated maximum model length — один запуск вместо бинарного поиска.
Конфигурация 2: рабочая, 96k и максимальная скорость
Далее берём уже более реальные параметры, которые на длинном контексте должны работать с качеством, приближенным к максимальному. Контекст, конечно, маловат — всего 96k, но модель загружается со всеми возможностями, --language-model-only не включаем.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0 \
CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1,2 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
--tensor-parallel-size 2 \
--speculative-config '{"method": "mtp", "num_speculative_tokens": 4}' \
--max-num-seqs 8 --max-num-batched-tokens 4096 \
--kv-cache-dtype fp8_e4m3 --gpu-memory-utilization 0.965 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
--enable-prefix-caching --max-model-len 98304| Метрика | Значение |
|---|---|
| Последовательная генерация | 110.61 ток/с |
| TTFT, короткий промпт | 0.325 с |
| Префилл | 1 073 ток/с |
Это лучшая конфигурация по всем трём метрикам сразу — и единственная, где модель поднимается целиком, вместе с vision.
Разбор параметров
--speculative-config '{"method": "mtp", "num_speculative_tokens": 4}'. В чекпойнте лежит MTP-слой (model_mtp.safetensors, 811 МБ) — черновая голова для спекулятивного декодирования. Она дёшево предсказывает следующие токены, основная модель проверяет их пачкой за один проход. Окупается уверенно: на отдельной серии замеров базовые 56.5 ток/с превращались в 84.4 на шаблонном коде и 77.4 на свободном тексте. Здесь, на 96k и с батчем 4096, вышло 110.61.
Сколько черновых токенов брать — вопрос не такой очевидный, как кажется. Слой MTP в модели один, и при num_speculative_tokens > 1 он прогоняется несколько раз подряд, о чём vLLM честно предупреждает в логе. Замеры это подтверждают: при двух токенах второй принимается лишь в 18–46 % случаев против 71–95 % у первого. Но итоговая скорость от этого не падает — за один шаг движка выдаётся больше токенов, и на свободном тексте два токена оказались даже быстрее одного (81.3 против 77.4 ток/с). На этой конфигурации четыре черновых токена дали лучший результат из всех пяти: на генерации кода, где текст предсказуем, длинный черновик принимается хорошо.
Плата — память: MTP добавляет 0.40 Gb весов (черновой слой лежит в bf16, он в списке исключений квантователя) и забирает 0.47 Gb доступного KV, плюс выравнивание слоёв — Add 3 padding layers, may waste at most 6.25% KV cache memory.
--max-num-batched-tokens 4096. Вот откуда вдвое более быстрый префилл, чем у первой конфигурации: 1 073 против 583 ток/с. Больше токенов в одном шаге предзаполнения — выше пропускная способность на входе. Ценой памяти, которой здесь хватает, потому что контекст ограничен 96k.
--kv-cache-dtype fp8_e4m3. Честные восемь бит, как записано в самом чекпойнте — форсить флагом не обязательно, значение подхватывается автоматически. Пишу явно, чтобы конфигурация читалась целиком.
Vision оставляем. Отказ от --language-model-only стоит примерно 26 % контекста, но модель поднимается со всеми возможностями. При 96k этот размен выглядит разумным: если контекста и так хватает, лучше не резать функциональность.
Конфигурация 3: MTP, но контекста больше
Хотим с MTP, но больше контекста — есть такая команда, но vision отключаем и режем батчи.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0 \
CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1,2 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
--tensor-parallel-size 2 --gpu-memory-utilization 0.965 \
--language-model-only --max-num-seqs 8 --max-num-batched-tokens 1024 \
--kv-cache-dtype fp8_e4m3 --mamba-ssm-cache-dtype bfloat16 \
--speculative-config '{"method": "mtp", "num_speculative_tokens": 2}' \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
--enable-prefix-caching --max-model-len 164736| Метрика | Значение |
|---|---|
| Последовательная генерация | 78.37 ток/с |
| TTFT, короткий промпт | 0.371 с |
| Префилл | 790 ток/с |
Разбор параметров
--mamba-ssm-cache-dtype bfloat16. Модель гибридная: из 64 слоёв только 16 — полноценное внимание, остальные 48 — linear attention (GDN/mamba) с состоянием фиксированного размера вместо растущего KV. Флаг переводит это состояние в bf16. Со спекулятивным декодированием он даёт около +3 % контекста — втрое больше, чем без него (+1.1 %): с MTP состояние свёрточной части крупнее, и его сжатие приносит больше. Заодно вдвое падает размер блока внимания, с 1 584 до 816 токенов. Качество на четырёх содержательных промптах и на поиске иголки в 160 тысячах токенов не пострадало.
Почему 164 736, а не круглое число. Это потолок, который vLLM назвал сам для этой комбинации флагов. Округлять вверх нельзя, и вот почему — см. следующий раздел про запрос, который висит навсегда.
Батчи обратно до 1024. Здесь память нужна контексту, а не префиллу, поэтому 790 ток/с против 1 073 у предыдущей конфигурации — прямое следствие возврата к маленькому батчу.
Как это выглядит в сравнении со всеми остальными:
Конфигурация 4: убираем MTP и смотрим максимум
Ну и убираем MTP и смотрим максимум. Хоть и запустилось на 256k, но не факт, что не поймаем OOM при его достижении.
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0 \
CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1,2 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
--tensor-parallel-size 2 --gpu-memory-utilization 0.965 \
--language-model-only --max-num-seqs 8 --max-num-batched-tokens 1024 \
--kv-cache-dtype fp8_e4m3 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
--enable-prefix-caching --max-model-len 262144| Метрика | Значение |
|---|---|
| Последовательная генерация | 50.85 ток/с |
| TTFT, короткий промпт | 0.419 с |
| Префилл | 783 ток/с |
Оговорка про OOM — не перестраховка, а арифметика. Раскладка 15.47 Gb на карту выглядит так:
| Статья расхода | Gb на карту |
|---|---|
| Недоступно (драйвер, контекст CUDA) | 0.48 |
| Веса + non-torch | 10.87 |
| Пиковые активации | 1.42 |
| CUDA-графы | 0.52 |
| Остаётся под KV-кэш | 2.64 |
С --language-model-only под KV освобождается около 3.3 Gb. А одной последовательности на 262 144 токена при восьмибитном KV нужно 4.09 Gb — это цифра из самой vLLM, она печатает её в отказе, когда считает потолок. Отсюда и вывод: сервер поднимается, короткие запросы работают, но по мере приближения к заявленной длине памяти не хватит. Держать такую конфигурацию как «до 200k» безопаснее, чем верить числу в конфиге.
Ловушка: запрос чуть короче лимита виснет навсегда
С этим я столкнулся на соседней конфигурации, и знать о ней стоит заранее. Запрос с промптом 166 465 токенов при --max-model-len 167776 (то есть внутри лимита) завис. Не упал, не вернул ошибку — завис: двадцать минут, ни одной новой строки в логе, Running: 0 reqs, Waiting: 0 reqs, загрузка GPU ноль. Сервер при этом живой, короткие запросы обрабатывает.
Арифметика такая: пул 167 776 токенов — это ровно 107 блоков по 1 568. Блок номер ноль vLLM резервирует как null-block, значит реально распределяемых 106 блоков, то есть 166 208 токенов. Запрос на 166 865 требует 107 блоков и не может быть запланирован никогда, а планировщик не считает это ошибкой — он просто ждёт.
Лечение: выравнивать --max-model-len по границе блока, отняв один блок. Тогда негабаритный запрос получает честный HTTP 400 мгновенно. Именно поэтому в третьей конфигурации стоит некруглое 164 736, а не 165 000.
Почему нельзя взять и контекст, и скорость сразу?
Напрашивается очевидное: включить TurboQuant ради контекста и MTP ради скорости. Связка стартует, отдаёт полные 262 144 токена и показывает высокую долю принятых черновых токенов. И генерирует мусор.
Воспроизводится детерминированно: две независимые конфигурации, четыре промпта, температура ноль. Контроли на месте — FP8 со спекуляцией отвечает нормально, четыре бита без спекуляции отвечают нормально. На полном контексте эта же связка убивает движок: CUDA error: an illegal memory access в сэмплере отбраковки, оба воркера, HTTP 500.
Причина нашлась в коде: у бэкенда TurboQuant стоит supports_spec_as_decode=False, поэтому порог переупорядочивания батча остаётся равным единице. Шаг спекуляции приходит с несколькими токенами в запросе, не проходит порог «это декод» и уходит в ветку префилла — с синтетическими длинами последовательностей. Формально код отрабатывает, считает неверно. Первые 3–6 токенов всегда правильные: самый первый шаг идёт вообще без черновика.
И главная ловушка: доля принятых драфт-токенов эту поломку не детектирует. На сломанной связке она равна 93.6 % против 78.4 % у исправной — модель печатает повторяющийся мусор, а мусор черновая голова предсказывает идеально.
Корректность спекулятивного декодирования проверяется чтением текста, а не метрикой.
Конфигурация 5: спасёт ли третья видеокарта?
Посмотрим, спасёт ли нас третья видеокарта 3090, которая не умеет NVFP4. И, конечно, vLLM не умеет делить на 3, 5, 7 и так далее: тензорный параллелизм требует, чтобы число KV-голов делилось на размер группы, а у этой модели их четыре — то есть допустимы 2 и 4, но не 3. Так что идём тестить llama.cpp и берём модель unsloth/Qwen3.8-27B-GGUF в кванте UD-Q8_K_XL — флаг -hf скачает её с Hugging Face сам.
CUDA_VISIBLE_DEVICES=0,1,2 ./build/bin/llama-server \
-hf unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL \
--host 0.0.0.0 --port 8000 \
--n-gpu-layers 99 --ctx-size 262144 --parallel 1 \
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn on \
--spec-draft-n-max 4 --spec-type draft-mtp \
--temp 0.7 --top-p 0.8 --top-k 20 --presence-penalty 1.5 --min-p 0.00 \
--reasoning off --split-mode tensor --tensor-split 0.4,0.3,0.3 \
--batch-size 4096 --ubatch-size 2048 --jinja| Метрика | Значение |
|---|---|
| Последовательная генерация | 74.85 ток/с |
| TTFT, короткий промпт | 1.052 с |
| Префилл | 354 ток/с |
Что дают эти флаги
--split-mode tensor --tensor-split 0.4,0.3,0.3. llama.cpp не связан ограничением на делители: он режет тензоры в заданной пропорции по трём картам. 3090 достаётся больше — у неё 24 ГБ против 16. Это и есть ответ на вопрос «спасёт ли третья карта»: памяти становится достаточно для полного контекста на восьмибитном кванте, без всякой экзотики вроде четырёхбитного KV.
--cache-type-k q8_0 --cache-type-v q8_0. Восьмибитный KV-кэш — аналог fp8 у vLLM, но квантование здесь блочное. На 262 144 токена без него памяти не хватило бы даже втроём.
--spec-type draft-mtp --spec-draft-n-max 4. llama.cpp умеет использовать тот же MTP-слой из GGUF как черновую модель. Настройка по смыслу совпадает с num_speculative_tokens: 4 у vLLM.
Что показывают цифры. Генерация — 74.85 ток/с, то есть быстрее, чем у обеих vLLM-конфигураций на том же контексте 262k (53.76 и 50.85). Но префилл — 354 ток/с против 583–783, а TTFT перевалил за секунду. Для чата это неважно, а для агента — критично: на промпте в 100 тысяч токенов llama.cpp потратит на предзаполнение около пяти минут, а vLLM на второй конфигурации — полторы.
Вывод, к которому я пришёл: третья карта решает вопрос памяти, но не делает сборку лучше для работы с кодом. Если бы задача была «читать длинные документы и отвечать», выбор был бы за llama.cpp.
Подключаем к Claude Code
Claude Code умеет ходить в любой сервер, совместимый с Anthropic API. Локальную модель подставляем переменными окружения — все три роли (opus, sonnet, haiku) указывают на одну и ту же модель:
ANTHROPIC_BASE_URL=http://10.10.1.32:8000 \
ANTHROPIC_API_KEY=dummy ANTHROPIC_AUTH_TOKEN=dummy \
ANTHROPIC_DEFAULT_OPUS_MODEL=unsloth/Qwen3.8-27B-NVFP4 \
ANTHROPIC_DEFAULT_SONNET_MODEL=unsloth/Qwen3.8-27B-NVFP4 \
ANTHROPIC_DEFAULT_HAIKU_MODEL=unsloth/Qwen3.8-27B-NVFP4 \
claudeКлючи нужны любые непустые — сервер их не проверяет, но клиент без них не стартует.
Для GGUF-варианта то же самое, плюс отключение телеметрии и заголовка атрибуции:
ANTHROPIC_BASE_URL=http://10.10.1.32:8000 \
ANTHROPIC_API_KEY=dummy ANTHROPIC_AUTH_TOKEN=dummy \
ANTHROPIC_DEFAULT_OPUS_MODEL=unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL \
ANTHROPIC_DEFAULT_SONNET_MODEL=unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL \
ANTHROPIC_DEFAULT_HAIKU_MODEL=unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL \
claude --settings '{"env":{"CLAUDE_CODE_ATTRIBUTION_HEADER":"0","CLAUDE_CODE_ENABLE_TELEMETRY":"0"}}' \
--model unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XLСо стороны llama.cpp при этом пригодится свой шаблон чата — встроенный в GGUF не всегда корректно разбирает вызовы инструментов, а без них агент бесполезен:
CUDA_VISIBLE_DEVICES=0,1,2 ./build/bin/llama-server \
-hf unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL --host 0.0.0.0 --port 8000 \
--n-gpu-layers 99 --ctx-size 262144 --parallel 1 \
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn on \
--spec-draft-n-max 4 --spec-type draft-mtp \
--temp 0.7 --top-p 0.8 --top-k 20 --presence-penalty 1.5 --min-p 0.00 \
--reasoning off --split-mode tensor --tensor-split 0.4,0.3,0.3 \
--batch-size 2048 --ubatch-size 1024 --jinja \
--chat-template-file /path/to/qwen38-fixed.jinjaОбратите внимание на --parallel 1 и урезанные батчи в обеих конфигурациях: сборка настроена под одного пользователя. Со стороны vLLM тому же служат --max-num-seqs 8 и небольшой --max-num-batched-tokens, а --tool-call-parser qwen3_coder и --reasoning-parser qwen3 разбирают вызовы инструментов и блоки рассуждений в формат, который клиент понимает.
Ещё одна мелочь, которую легко пропустить: min_p и logit_bias со спекулятивным декодированием не работают — vLLM предупреждает об этом в логе.
Сводная таблица
| Конфигурация | Контекст | Генерация | TTFT | Префилл |
|---|---|---|---|---|
| 1. Максимум контекста (turboquant 4 бита) | 262 144 | 53.76 ток/с | 0.546 с | 583 ток/с |
| 2. Рабочая, со всеми возможностями | 98 304 | 110.61 ток/с | 0.325 с | 1 073 ток/с |
| 3. MTP плюс контекст | 164 736 | 78.37 ток/с | 0.371 с | 790 ток/с |
| 4. Без MTP, честный fp8 | 262 144 | 50.85 ток/с | 0.419 с | 783 ток/с |
| 5. llama.cpp, три карты, GGUF Q8 | 262 144 | 74.85 ток/с | 1.052 с | 354 ток/с |
Что я из этого выбрал для работы с Claude Code: вторую. Она быстрее всех по всем метрикам, модель поднимается целиком, а 96 тысяч токенов на практике хватает — агент и так работает не одним гигантским промптом, а серией запросов с кэшем префикса. Третья идёт в дело, когда нужен большой разбор целиком. Первая осталась экспериментом: полный контекст на двух картах по 16 ГБ получить можно, но платить за него четырёхбитным кэшем в ежедневной работе я не готов.
Что осталось непроверенным
- Перплексию 4-битного KV не мерил: только осмысленность на нескольких промптах и поиск иголки. Цифра +2.71 % взята из кода vLLM, а не из моего замера.
- Промпт ровно на 262 144 токена не отправлял, максимум проверенного — 249 969.
- Четвёртая конфигурация на полном контексте не гонялась длинным промптом — там и ожидается OOM.
- Все замеры сняты батчем 1. Под несколькими одновременными запросами выигрыш спекулятивного декодирования обычно тает, на этом стенде не проверял.
- Качество ответов между конфигурациями формально не сравнивал: восьмибитный GGUF-квант и NVFP4 — это разные модели по точности, и разница в генерации может быть не только в скорости.
- Минимальный безопасный
--kv-cache-memory-bytesне искал, взял с запасом.
Если вы дочитали до этого места, у вас, скорее всего, есть свои карты и желание не платить за токены по подписке. Запуск локальной модели — половина дела; вторая половина в том, как встроить её в рабочий процесс: агенты, инструменты, поиск по своей базе знаний. Этому посвящён мой курс «ChatGPT и 1С» — там разбираем и локальные модели, и сборку агентов вокруг них на реальных задачах бизнеса.
Про соседние стенды я писал отдельно: Qwen 3.6 27B и 35B-A3B в NVFP4 — сравнение vLLM и SGLang на этих же картах, vLLM vs llama.cpp — что выбрать под задачу, RTX 5070 Ti vs RTX 3090 — стоит ли брать новое поколение, и GPU-сервер для ИИ дома — как собиралась сама машина. Замеры скоростей на разных картах лежат в бенчмарках.