- BrainTools - https://www.braintools.ru -

27B-модель на четырёх V100 в 2026 году: форк vLLM, INT4 на Volta и контекст 262K

У нас есть четыре Tesla V100 2017 года, и на них в проде работает плотная Qwen3.8-27B с окном 262 тысячи токенов. Официальный vLLM на этих картах 4-битную модель даже не запускает. Рассказываю, на каком форке это работает, куда утекает память [1], что показал бенчмарк MoE против dense и почему имя модели нельзя менять никогда.

Зачем вообще V100

Эта модель — zubr в ZuBrIQ: единственная наша модель, запросы к которой не покидают Беларусь. Промпт, документ и ответ остаются на нашем железе в Минске. Для банков, госсектора и всех, кто работает с персональными данными, это обязательное условие, а не опция.

Железо — 4× Tesla V100 SXM2 по 16 ГБ, всего 64 ГБ. Новые карты в Беларусь привезти сложно и дорого, а эти уже стоят в стойке. Задача — выжать из них максимум.

Проблема в том, что V100 — это архитектура Volta, compute capability 7.0 (SM70). На ней нет:

  • bfloat16 — а большинство современных моделей распространяются именно в нём;

  • FP8 — ни для весов, ни для KV-кеша;

  • 4-битных ядер в официальном vLLM — он отвечает min capability 75, current 70 и не стартует.

Стек: форк, systemd и никакого Docker

Мы используем 1Cat-vLLM [2] — форк vLLM для SM70 под лицензией Apache-2.0. В нём есть 4-битные ядра для Volta (Marlin и TurboMind), бэкенд внимания [3] FLASH_ATTN_V100 и CUDA Graph. У нас версия 1.2.1 на Python 3.12 и драйвере 580.

Модель работает в LXC-контейнере на Proxmox с пробросом всех четырёх GPU. vLLM запущен нативно через systemd, без Docker: так проще с драйверами и пробросом устройств.

Первая грабля всплыла ещё до запуска модели. Форк ставится пакетом 1cat-vllm, а CLI vLLM спрашивает version("vllm") и падает с PackageNotFoundError. Мы чиним это однократно в setup.sh:

# 1. фиктивные метаданные пакета «vllm»
mkdir -p "$SP/vllm-1.2.1.dist-info"
printf 'Metadata-Version: 2.1nName: vllmnVersion: 1.2.1n' > "$SP/vllm-1.2.1.dist-info/METADATA"
# 2. хардкод версии в CLI
for f in "$SP/vllm/entrypoints/cli/main.py" "$SP/vllm/entrypoints/cli/run_batch.py"; do
  [ -f "$f" ] && sed -i 's/importlib.metadata.version("vllm")/"1.2.1"/g' "$f"
done
# 3. сбросить старый байткод, иначе подхватится прежний .pyc
find "$SP/vllm" -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

Модель и квант

Мы взяли cyankiwi/Qwen3.8-27B-AWQ-INT4 [4]. Это плотная 27B с гибридным вниманием (linear и full в пропорции 3:1), мультимодальная. В INT4 веса весят около 21 ГБ — примерно 5,3 ГБ на карту при tensor parallel 4.

Две детали, без которых модель не стартует:

  1. Это не классический AWQ. Квант сохранён в формате compressed-tensors W4A16 (group 128, symmetric). На SM70 он идёт через те же ядра TurboMind, что и AWQ, но флаг для него выключен по умолчанию. Без VLLM_SM70_COMPRESSED_TENSORS_TURBOMIND=1 схема требует capability 75, и модель не грузится.

  2. dtype модели — bfloat16. На Volta его нет, поэтому --dtype float16 обязателен.

Юнит целиком:

[Service]
Environment=VLLM_SM70_COMPRESSED_TENSORS_TURBOMIND=1
ExecStart=/opt/vllm-env/bin/vllm serve /opt/models/Qwen3.8-27B-AWQ-INT4 
  --served-model-name zubr zubr-2 qwen3.8-27b 
  --tensor-parallel-size 4 
  --dtype float16 
  --gpu-memory-utilization 0.50 
  --kv-cache-memory-bytes 4600000000 
  --max-model-len 262144 
  --max-num-seqs 24 
  --enable-auto-tool-choice 
  --tool-call-parser qwen3_xml 
  --reasoning-parser qwen3 
  --host 0.0.0.0 --port 80
Restart=always
TimeoutStartSec=600

Холодный старт с компиляцией и захватом CUDA-графов занимает 4–5 минут, поэтому TimeoutStartSec=600.

Куда утекает память

Веса не вычитаются из бюджета KV

В обычном vLLM бюджет KV-кеша считается так: total × gpu_memory_utilization − веса − активации. В форке загрузчик для SM70 сообщает размер весов почти нулём, и они из бюджета не вычитаются. В итоге при --gpu-memory-utilization 0.50 процесс занимал не 8, а почти 14 ГБ на карту, и соседям не оставалось места.

Решение — задать KV явно через --kv-cache-memory-bytes. --gpu-memory-utilization после этого работает только как проверка свободной памяти при старте.

Размер KV посчитали не на глаз, а от факта. Движок сам сообщил, что окну 131 072 токена нужно 2,08 ГиБ, — это около 17,1 КБ на токен. Для окна 262 144 нужно примерно 4,4 ГиБ. Взяли 4,6 ГБ — это около 268 тысяч токенов с небольшим запасом.

Бюджет считается по самой тесной карте. Если на одной из GPU живёт что-то ещё, например сервис эмбеддингов, KV ограничен свободной памятью именно на ней.

CUDA-графы съедают память вне учёта

Летом у нас стояло --max-num-seqs 96: свип показал, что это потолок пропускной способности. Но CUDA-графы захватываются под каждый размер батча, и на 96 последовательностей они съедали около 5 ГБ сверх бюджета. Мы снизили до 24 — пропускная способность почти не просела, а память вернулась.

Два инстанса на одних картах

До сентября рядом с основной моделью работала отдельная Qwen3-VL-4B для зрения [5]. Квантов для неё на SM70 не нашлось: VL-кванты в compressed-tensors требовали SM75+. Поэтому она шла в fp16 с --enforce-eager.

CI перезапускал оба юнита с разницей в секунды. Пик инициализации двух процессов разом превышал 16 ГБ на карту, и один воркер ловил OOM. Хуже того, основной инстанс после этого зависал на NCCL watchdog. systemd показывал active, а порт был закрыт: снаружи модель «работала», но не отвечала.

Мы вылечили это сериализацией старта: второй юнит ждал health первого.

ExecStartPre=-/bin/sh -c 'timeout 600 sh -c "until curl -sf -m 2 http://localhost:80/v1/models >/dev/null; do sleep 5; done"'

В сентябре проблема ушла совсем. Qwen3.8-27B видит изображения сама, и на нашем тесте с чтением таблиц она справилась в 3 случаях из 4, а отдельная 4B — в 0 из 3. Мы погасили 4B, освободили около 8 ГБ и подняли окно основной модели со 131 072 до 262 144 токенов.

MoE против dense: бенчмарк

До 18 сентября под этим именем работала MoE Qwen3.6-35B-A3B: 35 млрд параметров, около 3 млрд активных. Мы прогнали обе модели одним скриптом vllm bench serve на одном боксе с теми же соседями. Нагрузка — случайные промпты 512 токенов на вход и 256 на выход, 300 запросов, 24 одновременных клиента.

Метрика

Qwen3.6-35B-A3B (MoE)

Qwen3.8-27B (dense)

Генерация, ток/с

200,2

218,5 (+9 %)

Время прогона, с

383,6

351,6

TTFT, медиана

0,59 с

1,31 с

TTFT, P99

2,3 с

14,2 с

TPOT, медиана

111,6 мс

95,9 мс (−14 %)

TPOT, P99

126,0 мс

139,9 мс

Мы ждали, что плотная модель окажется кратно медленнее. Не оказалась: на генерации она быстрее на 9 %, каждый токен печатается на 14 % быстрее. Похоже, MoE-ядра на SM70 просто не вылизаны.

Платим первым токеном: медиана 1,31 с против 0,59 с, хвост P99 — 14,2 с против 2,3 с. Причина понятна: на prefill плотная модель прогоняет по промпту все 27 млрд параметров, а MoE считала около 3 млрд.

Честная оговорка: мы замерили скорость, а не ум. Качество проверено только смоук-запросами, сравнения 3.6 и 3.8 по качеству мы не делали.

Имя модели — вечное

18 сентября, меняя веса, мы убрали старое имя qwen3.6-35b — оно врало про модель. Через несколько минут наш чат-бот поддержки начал на каждое сообщение отвечать «подождите оператора».

Бот ходил в vLLM напрямую, мимо шлюза, по старому имени. Он получал 404, а в режиме «только локальная модель» любая ошибка [6] означает эскалацию на оператора.

С тех пор правило такое:

  • первое имя в --served-model-name — вечное: zubr. Его зовут все клиенты, и оно не меняется при смене весов;

  • версионные имена (zubr-2, qwen3.8-27b) описывают конкретные веса и могут меняться;

  • vLLM возвращает в поле model ответа запрошенный алиас, а не настоящие веса. Источник истины — поле root в /v1/models.

Следующая смена весов не потребует правки ни одного конфига у клиентов — только юнита.

Парсеры: инструменты и рассуждения

  • --tool-call-parser qwen3_xml — Qwen3.5 и новее вызывают инструменты в XML-формате, и hermes их не разбирает.

  • --reasoning-parser qwen3 мы добавили только в октябре. Без него рассуждения модели при enable_thinking приходили прямо в content вместе с тегом </think>, и переключатель «Размышления» в чате не работал. С парсером они идут отдельным полем reasoning_content.

Деплой

Веса лежат во внутреннем MinIO. ExecStartPre юнита забирает их оттуда, а если их там нет — скачивает с Hugging Face и заливает в MinIO. Если веса уже на диске, шаг ничего не делает. Прежняя модель остаётся на диске как откат: вернуть её — значит откатить один коммит в юните.

Деплой — GitLab CI на push в main: git pull на боксе, обновление юнита, systemctl restart vllm. Правки только документации рестарт не вызывают.

Что в итоге

  • 4× V100 2017 года тянут плотную 27B в INT4 с окном 262K и зрением, около 218 ток/с генерации на 24 параллельных запросах.

  • Главные грабли — учёт памяти: веса не вычитаются из бюджета KV, CUDA-графы живут вне него, два процесса нельзя инициализировать одновременно.

  • Имя модели для клиентов не меняется никогда; версия весов — это отдельное имя.

Эту модель можно попробовать бесплатно — это zubr в ZuBrIQ, тариф Free, без карты. По API она доступна по OpenAI-совместимому адресу:

curl https://api.zubriq.ai/v1/chat/completions 
  -H "Authorization: Bearer $ZUBRIQ_API_KEY" 
  -H "Content-Type: application/json" 
  -d '{"model": "zubr", "messages": [{"role": "user", "content": "Привет! Где ты работаешь?"}]}'

В первой статье цикла [7] — как на этой модели и GLM-5.3 работает Claude Code. В следующей — как мы маскируем персональные данные до того, как запрос уйдёт во внешнюю модель. Если вы тоже держите модели на V100, делитесь в комментариях, на каком стеке и с какими цифрами.

Автор: ZubikIT

Источник [8]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/38533

URLs in this post:

[1] память: http://www.braintools.ru/article/4140

[2] 1Cat-vLLM: https://github.com/1CatAI/1Cat-vLLM

[3] внимания: http://www.braintools.ru/article/7595

[4] cyankiwi/Qwen3.8-27B-AWQ-INT4: https://huggingface.co/cyankiwi/Qwen3.8-27B-AWQ-INT4

[5] зрения: http://www.braintools.ru/article/6238

[6] ошибка: http://www.braintools.ru/article/4192

[7] первой статье цикла: https://habr.com/ru/articles/1104826/

[8] Источник: https://habr.com/ru/articles/1104832/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1104832

www.BrainTools.ru

Rambler's Top100