Qwen 3.8 27B локально под Claude Code: пять конфигураций на двух RTX 5070 Ti

Всем привет, с вами Низамов Илья. Погонял модель unsloth/Qwen3.8-27B-NVFP4 с разными параметрами запуска. Главным параметром для меня изначально был максимальный размер контекста на двух видеокартах 5070 Ti по 16 ГБ. Использовать эту модель я хотел вместе с Claude Code, поэтому параллелизм подобрал небольшой и уменьшил батчи.

Ниже — пять рабочих конфигураций с замерами: четыре в vLLM и одна в llama.cpp, куда я добавил третью карту. Плюс разбор параметров: что каждый флаг реально даёт и какие эксперименты за этим стоят. В конце — как подключить всё это к Claude Code.

Содержание

Стенд и оговорки

GPU2× 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-torch10.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 14453.76 ток/с0.546 с583 ток/с
2. Рабочая, со всеми возможностями98 304110.61 ток/с0.325 с1 073 ток/с
3. MTP плюс контекст164 73678.37 ток/с0.371 с790 ток/с
4. Без MTP, честный fp8262 14450.85 ток/с0.419 с783 ток/с
5. llama.cpp, три карты, GGUF Q8262 14474.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-сервер для ИИ дома — как собиралась сама машина. Замеры скоростей на разных картах лежат в бенчмарках.

Частые вопросы