В прошлой статье мы подняли vLLM как быстрый inference-сервер для растущей команды. Но vLLM — не единственный производственный движок для инференса LLM, и в некоторых сценариях его достойная альтернатива — Text Generation Inference (TGI), проект команды Hugging Face. Оба решения решают одну задачу быстрого параллельного инференса, но по-разному расставляют приоритеты: TGI теснее интегрирован с экосистемой Hugging Face Hub и удобнее там, где модели и токенизаторы уже привычно берутся именно оттуда, а конфигурация деплоя должна быть максимально простой и предсказуемой.

Рассматриваем развёртывание TGI на том же сервере srv-ai01 (сеть 192.168.10.0/24, домен itpages.lab.local), либо как замену vLLM, либо как второй inference-бэкенд для конкретных моделей, которые лучше поддержаны именно в TGI.

Чем TGI отличается от vLLM на практике

TGI использует собственный механизм эффективного батчинга (аналог continuous batching у vLLM) и поддерживает Flash Attention, quantization через bitsandbytes, GPTQ и AWQ, а также спекулятивное декодирование для ускорения генерации на некоторых моделях. Разница в пропускной способности между TGI и vLLM на одинаковом железе и модели обычно небольшая и зависит от конкретной версии и архитектуры модели — ни один из движков не является безусловным победителем по производительности во всех сценариях, поэтому выбор чаще определяется удобством эксплуатации и совместимостью с конкретными моделями, а не голыми цифрами бенчмарков.

Практическое отличие, которое стоит учитывать: TGI исторически строже относится к лицензиям моделей на Hugging Face Hub и требует явного принятия условий использования (gated models) через токен доступа, что для корпоративного использования скорее плюс — так проще контролировать, какие модели с какими лицензионными условиями фактически развёрнуты в компании.

Ещё TGI традиционно быстрее подхватывает поддержку самых свежих архитектур моделей сразу после их публикации, поскольку разрабатывается той же командой, что поддерживает библиотеку transformers — если компания планирует часто пробовать новые модели сразу после релиза, это может быть заметным практическим плюсом в пользу TGI. vLLM в этом смысле тоже развивается очень активно и почти всегда догоняет за считанные дни, но именно для самых первых часов после публикации новой модели TGI исторически чуть впереди.

Развёртывание TGI в Docker Compose

Добавь сервис tgi в docker-compose.yml на srv-ai01 рядом с уже работающими ollama, vllm и openwebui:

  tgi:
    image: ghcr.io/huggingface/text-generation-inference:latest
    container_name: tgi
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - /srv/ai/models-hf:/data
    environment:
      - HF_TOKEN=hf_xxxxxxxxxxxxxxxx
    command:
      - --model-id
      - mistralai/Mistral-7B-Instruct-v0.3
      - --quantize
      - awq
      - --max-total-tokens
      - "8192"
      - --max-input-length
      - "4096"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - ai-net

Обрати внимание, что порт 8080 контейнера TGI пересекается с портом Open WebUI из наших предыдущих статей — на реальном сервере стоит либо развести порты (например, вынести TGI на 8081), либо развёртывать TGI и vLLM попеременно для тестирования, а не держать оба движка постоянно активными одновременно, если видеопамяти на одной карте не хватает на две загруженные модели сразу.

OpenAI-совместимый режим и подключение к Open WebUI

Начиная с относительно новых версий TGI поддерживает эндпоинт /v1/chat/completions, совместимый с форматом OpenAI, что упрощает подключение к Open WebUI точно так же, как в случае с vLLM:

  openwebui:
    environment:
      - OPENAI_API_BASE_URL=http://tgi:80/v1
      - OPENAI_API_KEY=not-needed-for-tgi

Если используемая версия TGI ещё не поддерживает OpenAI-совместимый режим, можно обращаться к нативному эндпоинту /generate напрямую, но тогда потребуется либо ждать обновления Open WebUI с поддержкой нативного API TGI, либо писать тонкий прокси-адаптер, транслирующий запросы между форматами — на практике проще держаться актуальной версии образа, где совместимость уже реализована из коробки.

Потоковая генерация и Server-Sent Events

TGI поддерживает потоковую отдачу токенов через Server-Sent Events, что критично для отзывчивости чат-интерфейса — пользователь видит ответ по мере генерации, а не ждёт полного завершения. Проверить, что стриминг работает корректно, можно прямым запросом к API:

curl -N http://192.168.10.50:8081/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "mistralai/Mistral-7B-Instruct-v0.3", "stream": true, "messages": [{"role":"user","content":"Расскажи о преимуществах TGI"}]}'

Флаг -N в curl отключает буферизацию вывода, и ты должен увидеть, как ответ приходит частями, кусок за куском, а не одним блоком в конце. Если ответ приходит целиком сразу — вероятно, между TGI и клиентом стоит промежуточный прокси, который буферизует ответ, и стоит проверить настройки nginx (в частности, директиву proxy_buffering off для этого location).

Выбор между TGI и vLLM: практический критерий

Однозначного правильного ответа тут нет, но есть практичный подход: разверни оба движка параллельно на тестовом окружении с одной и той же моделью, прогони одинаковый нагрузочный тест (как описано в статье про vLLM) и сравни реальные цифры пропускной способности и задержки именно на тех моделях, которые компания планирует использовать постоянно. Разница в производительности между версиями обоих проектов меняется от релиза к релизу, поэтому полагаться на общие рейтинги из интернета годичной давности не стоит — тестируй на актуальных версиях образов и на своём железе.

# benchmark.sh — простой сравнительный тест TGI и vLLM
for engine in "tgi:8081" "vllm:8000"; do
  echo "=== $engine ==="
  time (for i in $(seq 1 15); do
    curl -s -o /dev/null -w "%{time_total}\n" \
      http://192.168.10.50:${engine#*:}/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{"model":"test","messages":[{"role":"user","content":"Explain RAID levels briefly"}]}' &
  done; wait)
done

Такой простой скрипт с параллельными запросами через фоновые процессы shell даёт грубую, но достаточную для практического решения картину: суммарное время выполнения пятнадцати параллельных запросов на каждом движке. Для более точного и воспроизводимого бенчмарка у обоих проектов есть собственные утилиты нагрузочного тестирования (vllm bench и text-generation-inference benchmark), но для внутреннего сравнения на конкретной модели и конкретном железе даже простого curl-скрипта обычно достаточно, чтобы принять обоснованное решение.

Ресурсы и параллельная эксплуатация нескольких движков

Если решено оставить и vLLM, и TGI на srv-ai01 постоянно — например, TGI под одну специализированную модель, а vLLM под основную универсальную, — важно явно разграничить видеопамять между ними на уровне конфигурации, а не полагаться на то, что оба процесса сами договорятся о распределении ресурсов GPU. Для vLLM это параметр gpu-memory-utilization из предыдущей статьи, для TGI — аналогичный контроль достигается ограничением через —max-total-tokens и размер батча, поскольку прямого параметра доли видеопамяти в TGI нет, память резервируется исходя из размера модели и указанных лимитов токенов.

nvidia-smi --query-gpu=memory.used,memory.total --format=csv -l 5

Команда выше в цикле каждые пять секунд показывает текущее потребление видеопамяти — полезно запустить её в отдельном терминале во время первого совместного старта TGI и vLLM на одной карте, чтобы сразу увидеть, укладываются ли оба процесса в физический объём памяти GPU без взаимных конфликтов и OOM-ошибок.

Дополнительный практический критерий — команда поддержки. Если в компании уже есть опыт работы с экосистемой Hugging Face (transformers, datasets, токенизаторы), TGI будет восприниматься естественнее и проще в отладке. Если приоритет — максимальная гибкость в настройке батчинга и явный контроль над очередью запросов, vLLM даёт больше рычагов управления через параметры командной строки, которые мы разбирали в предыдущей статье.

Квантование в TGI: bitsandbytes, GPTQ и AWQ

TGI поддерживает несколько разных механизмов квантования, и они не взаимозаменяемы — выбор зависит от того, в каком формате изначально опубликована модель. Флаг —quantize принимает несколько значений, и практическая разница между ними существенна:

  • bitsandbytes — квантование на лету прямо при загрузке модели в исходном формате, не требует заранее квантованной версии весов, но даёт чуть менее предсказуемый выигрыш в скорости по сравнению с моделью, квантованной заранее.
  • gptq — требует модель, уже опубликованную в формате GPTQ (обычно с суффиксом -GPTQ в названии репозитория на Hugging Face), даёт стабильный прирост скорости и предсказуемое потребление памяти.
  • awq — аналогично требует заранее квантованную версию (-AWQ), обычно чуть лучше сохраняет качество ответов на моделях с большим разбросом весов.
  tgi:
    command:
      - --model-id
      - TheBloke/Mistral-7B-Instruct-v0.3-GPTQ
      - --quantize
      - gptq
      - --max-total-tokens
      - "8192"

Если для нужной модели нет готовой квантованной версии на Hugging Face, а самостоятельно квантовать её нет ресурсов или времени, bitsandbytes — самый практичный выбор для старта: он работает с любой моделью в исходном формате без предварительной подготовки, ценой чуть менее агрессивного сжатия по сравнению с GPTQ или AWQ.

Health-check и обновление модели без простоя

TGI отдаёт отдельный эндпоинт для проверки готовности, который стоит завести в docker-compose.yml так же, как для остальных сервисов стека — это позволяет системе мониторинга и самому Docker понимать, что контейнер поднялся, но модель ещё не полностью загружена в память, а не считать его сразу готовым к приёму трафика:

  tgi:
    ...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 20s
      timeout: 10s
      retries: 5
      start_period: 120s

Параметр start_period увеличен до 120 секунд не случайно — загрузка модели среднего размера с диска в видеопамять при старте контейнера может занимать одну-две минуты, и без увеличенного start_period Docker может ошибочно посчитать контейнер нездоровым ещё до завершения загрузки весов и начать его перезапускать в бесконечном цикле.

Для перехода на новую версию модели без ощутимого простоя для пользователей практично поднять новый контейнер TGI с другой моделью на отдельном порту, прогнать на нём тот же набор тестовых вопросов, что описан в статье про vLLM, и только после успешной проверки переключить переменную OPENAI_API_BASE_URL в Open WebUI на новый адрес, а старый контейнер с прежней моделью оставить работающим ещё какое-то время как запасной вариант на случай отката:

docker compose up -d tgi-v2
# проверка нового контейнера, затем переключение Open WebUI
docker compose stop tgi
docker compose rm -f tgi

Такой параллельный запуск требует немного больше видеопамяти на время перехода, зато исключает окно полной недоступности сервиса, которое неизбежно возникает при простом docker compose restart на единственном рабочем контейнере.

Частые ошибки и как их избежать

  • Конфликт портов при одновременном запуске нескольких inference-серверов. Ollama, vLLM и TGI по умолчанию используют разные порты, но при копировании конфигурации легко забыть об этом и получить ошибку старта контейнера. Явно прописывай и документируй порты каждого сервиса в docker-compose.yml.
  • Использование gated-моделей без принятого соглашения на Hugging Face. Если модель требует принятия лицензии на сайте Hugging Face, а токен доступа не связан с аккаунтом, принявшим условия, контейнер TGI упадёт с ошибкой авторизации при попытке скачать веса. Проверяй статус доступа к модели заранее, до попытки развёртывания на проде.
  • Буферизация ответа на уровне nginx. Если забыть отключить proxy_buffering для потоковых эндпоинтов, пользователи будут видеть ответ одним блоком вместо плавной генерации, что создаёт впечатление, будто сервис завис. Проверяй стриминг отдельно от общей работоспособности API.

Что дальше

Свой AI-сервер редко ограничивается одной настройкой — вот что обычно идёт следом: