Продолжаем разбираться с тем, как можно эффективно работать с большими языковыми моделями, используя доступное оборудование.
В этой части мы перейдём к организации распределённого инференса с помощью vLLM и обеспечим доступ к нему через Ray Serve. А ещё выясним, как запустить модель Gemma 3 в Ray-кластере и как проверить работу нашего OpenAI-совместимого эндпойнта с JWT-аутентификацией.
Если вы ещё не читали первую часть, стоит начать с неё. Там описано «железо» моей домашней лаборатории и процесс подготовки всего необходимого для развёртывания распределённого инференса с Ray Serve и vLLM.
Написание скрипта vLLM для шардирования, распределения вычислений и Ray Serve для вывода API
Начнём с ключевых концепций шардирования, параллелизации вычислений и принципов работы API.
Что такое vLLM и почему он выбран
vLLM — это фреймворк, который поддерживает параллельный инференс больших языковых моделей. Он предоставляет:
Нативную поддержку шардирования (tensor parallel): разбивает весовые тензоры модели между GPU в рамках одной ноды, позволяя каждому ускорителю обрабатывать свою часть вычислений одновременно.
Пайплайн-параллелизм (pipeline parallel): разбивает модель на последовательные блоки-слои и распределяет их по разным нодам: первые слои выполняются на GPU ноды А, следующие — на GPU ноды B и так далее. При этом пока нода А обрабатывает следующий запрос, нода B уже догоняет и завершает предыдущий — конвейер остаётся загруженным.
Интеграцию с NVIDIA Collective Communications Library (NCCL), которая обеспечивает сверхбыстрый обмен промежуточными активациями и градиентами между GPU и между нодами (в том числе по RDMA/InfiniBand), автоматически группируя все устройства в единый вычислительный пул.
Стриминговые ответы: отдаёт результат по мере генерации, как в OpenAI ChatCompletion API, чтобы клиенты видели ответ постепенно.
Вместе это даёт нам:
Объединённую видеопамять: все VRAM-карты работают как одно целое, суммируя свои ресурсы.
Постоянную загрузку: тензорные операции на каждой карте идут параллельно, а конвейерные этапы никогда не простаивают.
Горизонтальное масштабирование «из коробки»: независимо от того, 1 или 8 GPU на ноде и каково общее количество нод, vLLM + NCCL автоматически развернут любую большую модель сразу на десятках карт.
Распределение инференса и параметры шардирования
В конфиге vLLM задаём:
Tensor parallel size — сколько «кусочков» весов распределять по GPU внутри узла.
Pipeline parallel size — на сколько стадий дробим модель по нодам.
gpu_memory_utilization и cpu_offload_gb — параметры, которые помогают эффективно расходовать видеопамять и часть объёма оперативной памяти хоста.
Так мы балансируем память и производительность под свой кластер.
Что такое Ray Serve
Ray Serve — это компонент фреймворка Ray, который предоставляет микросервисную архитектуру для инференса:
Позволяет развернуть FastAPI-приложение и автоматически управлять его репликами.
Делает возможной горизонтальную масштабируемость — каждая реплика может быть на отдельном узле, если нужно.
Может эффективно работать с GPU, когда под каждый сервис зарезервирована часть видеокарт.
Мы используем Ray Serve, чтобы предоставить внешний HTTP API, совместимый с OpenAI-протоколами (ChatCompletion). Через этот API клиенты смогут делать запросы к модели, развёрнутой в vLLM.
Как это работает вместе
Внутри узла. Tensor Parallelism дробит модель на куски, загружает их на все локальные GPU и с помощью NCCL синхронизирует параметры.
Между узлами. Pipeline Parallelism превращает несколько серверов с GPU в конвейер: каждый узел обрабатывает свою «стадию» модели, передавая промежуточные данные дальше — так все карты работают без простоев.
Внешний интерфейс. Ray Serve оборачивает всю эту мощь в единый HTTP-сервис по OpenAI-протоколу. Он разбивает входящий запрос на нужные шард-группы, отправляет их на соответствующие GPU и ноды, а затем собирает полученные фрагменты в единый осмысленный ответ.
Описания скриптов
Дисклеймер: я не Python-разработчик, поэтому скрипты далеко не production-ready и написаны больше для ознакомительных целей.
работает с JWT — создание и проверка токенов, роль пользователя;
берёт загрузку пользователя (логин, роль, хэш пароля) из переменных окружения (например, USER_LIST);
предоставляет вспомогательные функции для проверки роли (admin/user) и времени жизни токена.
Этот модуль подключается внутри serve.py, чтобы проверять токены при обращении к эндпойнтам. Его реализация максимально упрощена (то же касается хранения логинов, паролей и ролей) и служит только демонстрационным примером.
Промежуточные итоги
Теперь у нас есть два скрипта для дальнейшего использования:
serve.py — основной сервер, где Ray Serve запускает FastAPI-приложение с vLLM под капотом;
auth.py — вспомогательная логика JWT-аутентификации, используемая в серверном скрипте.
Благодаря этим двум модулям мы получаем распределённый инференс, шардирование больших языковых моделей и удобный API для взаимодействия с ними. Теперь нужно собрать скрипты в Docker-образ и развернуть всё в KubeRay.
Настройка KubeRay
В этом разделе мы разберёмся, как подготовить Docker-образ, поместить его в Registry и развернуть Ray Cluster с нужными параметрами. Это поможет организовать распределённый инференс, использовать CephFS или другое хранилище, настраивать CPU- и GPU-ресурсы и так далее. В примерах я беру за основу официальный Docker-образ Ray, но вы можете легко заменить его на любой другой совместимый базовый образ.
Подготовка Docker-образа и загрузка в Registry
1. Базовый образ Ray:
Берём официальный образ Ray соответствующей версии, например rayproject/ray:2.44.0-py310-cu124. Он уже содержит нужные компоненты Ray, CUDA-библиотеки и Python 3.10.
2. Добавляем скрипты и пакеты:
Помещаем serve.py, auth.py (упаковывая всё в zip-архив).
Устанавливаем vLLM, httpx, PyJWT и другие зависимости.
3. Сборка:
Пишем Dockerfile (dockerfile.ray), в котором описываем добавление zip-файла и установку pip-зависимостей.
Собираем образ с помощью Make (Makefile) командой:
make package-container
4. Загрузка образа:
Пушим результат в нужный Registry — DockerHub, GitLab Registry и так далее:
Убеждаемся, что Kubernetes-узлы или кластер имеют доступ к этому Registry. Кстати, в Deckhouse уже есть возможность добавить внутренний registry, а также авторизацию для него.
Настройка KubeRay Cluster
Для удобства описываем Ray Cluster с помощью Helm-чарта и values-файла. Основная идея такая:
head (головной узел) — содержит Ray Head Pod с установленным Ray Dashboard, Autoscaler, если он нужен, и обеспечивает взаимодействие с worker'ами. Для обеспечения режима HA есть отдельная опцияgcsFaultToleranceOptions — подробнее о её работе расскажу чуть позже.
worker — 1+ подов, каждый из которых может работать на CPU или GPU.
additionalWorkerGroups — дополнительные группы worker'ов с другими конфигурациями (например, меньше памяти, более слабый GPU).
Ниже приведён пример ap-values.yaml с основными настройками.
rayVersion — указывает, какая версия Ray используется Autoscaler'ом, если включён in-tree Autoscaling;
serviceAccountName — ServiceAccount, который нужно дать поду. Он может понадобиться для доступа к PV, Secret и так далее;
envFrom — берём переменные из Secret auth-config. Это логины, пароли, JWT-ключи;
resources — запросы и лимиты CPU/RAM для head-пода. В примере: лимит 6 CPU, 8GiB RAM;
volumes/ volumeMounts — примонтированный CephFS (через PVC model-cache-pvc) на путь /data/model-cache, чтобы модель была доступна для head.
HA-режим GCS (GCS Fault Tolerance)
Чтобы защитить метаданные Ray-кластера от единственной точки отказа (GCS Head), мы включили опциональный режим репликации через Redis:
enableGcsFT: true — кастомный флаг, добавленный вручную, который заставляет Helm-шаблон сгенерировать секцию gcsFaultToleranceOptions. На момент написания статьи этого флага ещё нет в официальном чарте RayCluster.
gcsFT.redisAddress: redis:6379 — адрес нашего Redis-сервиса, где будут храниться дубли GCS-метаданных.
gcsFT.redisSecret.name/key — Kubernetes Secret с учётными данными доступа к Redis, чтобы пароль не «плавал» в YAML.
Принцип работы
При старте Ray-кластера GCS head теперь дублирует свои служебные данные — информацию о запущенных задачах и состояние акторов — не только во внутреннее хранилище, но и в Redis. Если оригинальный head-под выходит из строя, резервный head подхватывает сохранённые в Redis метаданные и продолжает обслуживание без потери состояния.
Можно добавить сколько угодно дополнительных групп, каждая — со своими ресурсами, лейблами, affinities и так далее. Если пока не хотим их запускать, ставим disabled: true, как в примере.
Развёртывание Ray Application. Запуск модели Gemma 3 и тестирование API
Gemma 3 — одна из передовых LLM от Google, доступных на Hugging Face. Она умеет работать с очень длинными текстами (до 128K токенов) и спроектирована так, чтобы эффективно использовать память и быстро отвечать. Этот пример подходит для демонстрации работы, но вы можете использовать аналогичные шаги для развёртывания других доступных в открытом доступе LLM.
В этом разделе разберёмся, как выбрать и запустить модель в Ray-кластере, а также как проверить работу нашего OpenAI-совместимого эндпойнта с JWT-аутентификацией.
Запуск модели через Ray Application
Для запуска модели в Ray-кластере мы используем Ray Application. Данный механизм указывает Ray Serve, какие файлы брать, какую модель загружать и под какими настройками параллелизации (tensor/pipeline) работать.
Пример JSON-запроса к Ray Dashboard
Отправляем POST-запрос на https://ray-dashboard.k8s.example.com/api/serve/applications/ с телом:
Указывает Ray, что в архиве (working_dir) находится Python-модуль serve.py, в котором определён объект model.
Именно он развёртывается как Ray Serve Deployment.
2. name: "Gemma-3-12b"
Имя приложения в Ray Dashboard, чтобы легко отличать его от других.
3. route_prefix: "/"
Базовый путь, по которому будет доступен сервис при наличии Ingress или сервисов.
4. autoscaling_config
min_replicas, initial_replicas, max_replicas задают политику масштабирования. В примере — 1 реплика без автоскейла.
5. deployments
Здесь описан VLLMDeployment с num_replicas = 1. Это класс/обёртка vLLM в serve.py.
deployment_ready_timeout_s = 1200 даёт время (20 минут) на инициализацию модели. Это полезно при больших загрузках.
6. runtime_env
working_dir: "file:///home/ray/serve.zip" говорит Ray, где лежит код — скрипты serve.py, auth.py.
env_vars: задаёт переменные окружения для vLLM:
"MODEL_ID" — название модели на Hugging Face, здесь "google/gemma-3-12b-it";
"TENSOR_PARALLELISM" и "PIPELINE_PARALLELISM" — регламентируют шардирование и конвейерную параллельность ( "TENSOR_PARALLELISM": "1" — на каждом узле 1 GPU, "PIPELINE_PARALLELISM": "2" — 2 GPU всего в кластере);
"MODEL_NAME" — отображается в ответах API как название модели;
"MAX_MODEL_LEN" — максимальная длина обрабатываемой последовательности в токенах, можно задать в чарте Ray Cluster;
"MAX_NUM_SEQS" — максимальное число одновременных сессий/запросов к модели;
"ENABLE_ENFORCE_EAGER" — принудительное включение eager-режима в vLLM для улучшения детерминированности и дебага;
"CPU_OFFLOAD_GB" — объём оперативной памяти (GB), выделяемой под офлоад части вычислений с GPU на CPU;
"DTYPE" — выбор формата данных (bfloat16/float32 и т.д.) для инференса, можно задать в чарте Ray Cluster;
"VLLM_USE_V1" — переключает движок на новую архитектуру vLLM V1 с единым диспетчером задач, менеджером KV-кэша и другими оптимизациями, включёнными по умолчанию;
"GPU_MEMORY_UTIL" — доля доступной видеопамяти (98 %) для использования моделью, можно задать в чарте Ray Cluster.
Результат деплоя и вид в Ray Dashboard
После успешного запроса Ray:
Извлекает файлы из serve.zip и создаёт Ray Serve Application под именем Gemma-3-12b.
В разделе Deployments появляется VLLMDeployment. vLLM инициализируется, подгружая Gemma-3-12b из Hugging Face.
При желании можно просмотреть логи, где будет видно, как vLLM скачивает вес модели и запускает инференс.
С этого момента модель Gemma 3 готова к приёму запросов на OpenAI-совместимые эндпойнты (например, /v1/chat/completions), используя конфигурацию параллелизации (tensor/pipeline), указанную в env_vars.
Тестирование API и аутентификации
1. Создание пользователей
В Kubernetes создаём Secret (auth.yaml) с JWT-ключом, параметрами истечения токена, а также списком пользователей (логины, роли, хэши паролей).
В переменных окружения ALICE_HASHED_PASSWORD и так далее хранится уже «соль + хэш» пароля.
2. Генерация пароля и хэша
Используем Python-скрипт gen_pwd.py, где с помощью secrets.token_hex и hashlib.sha256 вычисляем соли и хэши паролей. Затем записываем хэшированный пароль в значение строки PASSWORD для конкретного пользователя, например ALICE_HASHED_PASSWORD. Соль записываем в строку JWT_KEY. Это нужно, чтобы auth.py мог сравнивать введённые данные при аутентификации.
В качестве демонстрации у нас для всех паролей и ключа JWT одинаковая соль, но в реальных условиях, конечно же, так делать не нужно.
3. Авторизация и получение JWT
Выполняем запрос на /token, передавая username / password в формате form-data:
Получаем ответ, по которому видим, что ИИ успешно справился с задачей — хотя другие модели с трудом отвечали на этот вопрос (это не шутка):
{ "id": "chatcmpl-1745243015.727946", "object": "chat.completion", "created": 1745243015, "model": "Gemma-3-12b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "В марте 2025 года будет **четыре** четверга. Вот даты: * 6 марта * 13 марта * 20 марта * 27 марта" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 23, "completion_tokens": 46, "total_tokens": 69 } }
Вот для сравнения модель Qwen3-14B, которая рассуждала 48 сек., но где-то просчиталась
Мониторинг в Ray Dashboard
В Ray Dashboard видим загруженность GPU: скорость ~40 token/s в зависимости от конкретной модели и конфигурации.
Честно признаюсь, скрин логов взят со старой версии Ray и для модели Dolphin, которая использует Engine V0. В новой версии V1 можно сделать вывод логов по эндпойнту /metrics, но я это ещё не настроилТак выглядит сам Ray Cluster во время работы
А логи отображают процесс инференса, можно анализировать ошибки или задержки.
Пришло время для тестов
Чтобы понять, как быстро и надёжно работает модель, я провёл два вида тестов с помощью LLMPerf — один на скорость и нагрузку, другой на корректность ответов под нагрузкой.
Как я тестировал
Тест скорости и нагрузки:
Скрипт token_benchmark_ray.py.
Менял число параллельных запросов (1, 4, 8, 16, 20), длину контекста (от 512 до 32 768 токенов) и способ сэмплинга (greedy, Top-K, Top-P, их сочетание).
Для каждого случая я запускал от 30 до 100 запросов и фиксировал:
время на токен (inter_token_latency_s);
время до первого токена (ttft_s);
общую задержку (end_to_end_latency_s);
скорость (throughput_token_per_s);
p50/p95/p99 в каждой метрике.
Тест стабильности и точности:
Скрипт llm_correctness.py.
Отправлял до 300 запросов при 20 параллельных сессиях и смотрел, стали ли ответы «уезжать» от ожидаемого или уходить в ошибку. Фиксировал error_rate и mismatch_rate.
Особенности окружения
CUDA Graph для Gemma-3-12B пришлось отключить (ENABLE_ENFORCE_EAGER=true), иначе модель падала. На Dolphin3.0 с графом производительность была ? 40 t/s.
Если у вас есть идеи, как вернуть CUDA Graph для Gemma-3 или другие способы ускорить эту модель, напишите, пожалуйста, в комментариях!
Тесты шли через внешний OpenAI API, поэтому в задержку добавлялся сетевой оверхед.
Все тесты на одной и той же конфигурации с постоянными GPU и RAM, чтобы сравнения были честными.
Результаты тестирования
Базовая производительность: модель выдаёт новый токен каждые ? 0,116 с, скорость генерации ? 10 tok/s, время до первого токена ? 3,2 с.
Параллелизм: оптимально использовать до 8 одновременных сессий — при дальнейшем росте latency начинает расти непропорционально.
Контекст: безопасный предел — до ? 2?000–4?000 токенов; при больших окнах существенно падает скорость (до 6 tok/s на 8?192 токенах и ниже) и возрастает риск ошибок.
Сэмплинг: выбор Top-K или Top-P даёт незначительное замедление (< 0,005 с/токен), но может улучшить качество генерации.
Надёжность: при 20 concurrent запросах error_rate = 0 %, mismatch_rate < 1 % — модель отвечает стабильно, без падений.
Все подробные результаты и исходные скрипты доступны в репозитории.
Промежуточные итоги
Успешно подключили Gemma 3 из Hugging Face, развернув её в Ray-кластере с vLLM «под капотом».
API /v1/chat/completions проверен — он выдаёт JSON-ответ, совместимый с OpenAI-протоколом.
JWT-аутентификация ограничивает доступ к этому эндпойнту, пользователи хранятся в Secret.
Провели ряд тестов производительности с помощью LLMPerf. Средняя межтокеновая задержка составила ?0,116 с, а throughput — ?10 т/с, что наглядно подтверждает стабильность и корректность работы инференса. Не так быстро, как хотелось бы, но я продолжаю исследовать возможности улучшить результат.
В следующей части
Итак, мы протестировали API. Дальше хочется подключить удобный интерфейс. В следующей, заключительной, статье мы рассмотрим, как развернуть OpenWebUI — бесплатный веб-интерфейс для взаимодействия с LLM. Подробно разберём его основные возможности и настройки, а также посмотрим, как связать OpenWebUI с нашим Ray-кластером, чтобы общаться с моделью через OpenAI-совместимый API.