Хук: 18 tok/s → 100 tok/s за 3 недели

Первый запуск vLLM на 2×3090 дал нам 18 tok/s. Через 3 недели — 75. Разница — 4 параметра в конфиге и 2 дня отладки. Вот эти 4 параметра, и вот что пошло не так по пути.

Первый запуск (и почему он был плохой)

vllm serve Qwen3.8-27B-AWQ-MTP --tensor-parallel-size 2

18 tok/s. TTFT 4.2s. «Работает, но это не то».

Ошибки первого запуска:

  1. Не настроили MTP — модель с MTP-головами гонялась в обычном режиме, 1 токен за итерацию.
  2. Не настроили KV-cache — контекст 32K по умолчанию, 256K не заявлен.
  3. OMP_NUM_THREADS по умолчанию — CPU-многопоточность создавала overhead, а не ускорение.

Итерация 1: MTP-спекулятивное декодирование

--speculative-config '{"method": "mtp", "num_speculative_tokens": 3}'

18 → 45 tok/s (×2.5). Первый ощутимый скачок.

Проблема: MTP в vLLM 0.27.1 — нестабильный. Wild write bug (PR #50021): после нескольких часов нагрузки процессорный буфер уходил в мусор, и vLLM крашился. Наш vLLM крашнулся на 3-й день. Пришлось патчить.

Решение: upgrade на 0.28.0 — баг закрыт. (NVFP4 и TRT-LLM из 0.28.0 нам не нужны: это Hopper/Blackwell-фичи, а у нас Ampere.)

Итерация 2: KV-cache и max-model-len

--max-model-len 262144

256K контекст. Проблема: на 24GB×2 KV-cache под 256K съедает ~18GB. Осталось 6GB для весов. Не влезает.

Решение:

--max-num-seqs 2

Лимит параллельных запросов. Мы жертвуем параллельностью ради контекста. Для агента это ок — он генерирует последовательно. (Почему это честный компромисс, а не «обход» — в части 5.)

Итерация 3: OMP_NUM_THREADS и async scheduling

OMP_NUM_THREADS=1
--disable-async-scheduling

Почему OMP_NUM_THREADS=1 (не 8, не 16): на 3090 (Ampere) GPU упирается в память, и CPU-многопоточность в CUDA-рантайме создаёт overhead, а не ускорение. Мы замерили: 1 тред — быстрее, чем 8.

Почему --disable-async-scheduling: async scheduling конфликтует с MTP-декодированием — планировщик «опережает» спекулятивные токены, и acceptance rate падает.

Результат: TTFT 4.2s → 1.8s.

Итерация 4: mamba-cache-mode

--mamba-cache-mode align

Почему: по умолчанию vLLM использует auto, который на 3090 даёт suboptimal memory layout для гибридного KV-хранилища. align выравнивает блоки — +5 tok/s.

Финальный конфиг (полный)

vllm serve Qwen3.8-27B-AWQ-MTP \
  --tensor-parallel-size 2 \
  --max-model-len 262144 \
  --max-num-seqs 2 \
  --speculative-config '{"method": "mtp", "num_speculative_tokens": 3}' \
  --mamba-cache-mode align \
  --disable-async-scheduling \
  --gpu-memory-utilization 0.92
# OMP_NUM_THREADS=1

Бенчмарк: до/после

ПараметрПервый запускФинальный
Tok/s (decode)1875
TTFT (8K)4.2s1.8s
Контекст32K256K
Параллельность8 seqs2 seqs
VRAM usage42GB46GB

×4.2 по decode-скорости. И обратите внимание на параллельность: мы сознательно ушли с 8 последовательностей на 2. Это не деградация — это расчёт (см. часть 5).

Что мы НЕ делали (и почему)

  • NVFP4 — не поддерживается на Ampere.
  • TRT-LLM — не нужен, vLLM достаточно.
  • TensorRT — overhead больше, чем gain.
  • 8 параллельных запросов — для агента 2 достаточно. Гонять 8 — значит жертвовать контекстом и TTFT ради throughput, который нам не нужен.

Смежные материалы: Tensor Parallelism, AWQ, PagedAttention.

Уроки тюнинга

  1. Начинайте с baseline, а не с «лучшего конфига из интернета». Наш первый конфиг — дефолтный, и именно он показал, где боль.
  2. Каждая итерация — одна переменная. MTP → KV-cache → CPU → cache-mode. Иначе не понять, что дало эффект.
  3. Версия рантайма — часть тюнинга. 2 недели на нестабильном 0.27.1 — это потерянное время. Проверяйте, что вы патчите баги, а не живёте с ними.

Именно так мы работаем в проектах по тюнингу vLLM: baseline → итерации по одной переменной → нагрузочное тестирование → отчёт с метриками до/после. Если ваш инференс «работает, но медленно» — обсудим проект.

← Как мы выбрали Qwen3.8-27B из 12 моделей: бенчмарки, MTP и почему 27B — sweet spot Мы написали балансировщик для 3 реплик vLLM: почему NGINX не работает для LLM →