Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера. ai.. ai. gpu.. ai. gpu. Kubernetes.. ai. gpu. Kubernetes. machinelearning.. ai. gpu. Kubernetes. machinelearning. mlops.. ai. gpu. Kubernetes. machinelearning. mlops. nvidia.
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 1

Всем привет! В этой статье я расскажу, как мы запускаем большие языковые модели на Kubernetes-платформе Nova AI. Разобьем материал на две части: сначала посмотрим, с помощью чего это реализовано (архитектура и компоненты), а затем — что это позволяет делать (сценарии использования и практические кейсы).

Архитектура платформы

Платформа логически разделена на два уровня: инфраструктурный и софтверный.

Инфраструктурный уровень отвечает за компоненты, которые обеспечивают работу с устройствами. В данном случае речь идет о GPU — видеокартах. Компоненты этого уровня позволяют использовать видеокарты внутри подов для выполнения задач. В текущем релизе это NVIDIA GPU Operator, и уровень будет пополняться в дальнейшем.

Софтварный уровень — это весь софт, все продукты и компоненты, которые покрывают ML-цикл. В текущем релизе это:

  • JupyterHub — среда разработки

  • Apache Airflow — автоматизация задач

  • MLflow — трекинг экспериментов

  • KServe — inference моделей

  • Kuberay — распределенные вычисления

Начало работы с платформой

Для начала работы нужен Kubernetes-кластер Nova версии 7.3.0 и выше, а также два манифеста — по одному для каждого уровня.

Манифест инфраструктурного уровня

Этот манифест описывает компоненты для взаимодействия с физическим оборудованием:

apiVersion: apps.nova-platform.io/v1alpha1

kind: MLInfrastructure

metadata:

  name: nova-ml-infrastructure 

spec:

  nvidiaGpuOperator: 

      mig: 

          enabled: true 

          strategy: single 

          configRef: mig-config 

      timeSlicing: 

          enabled: true 

          configRef: time-slicing-config
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 2

После применения манифеста через kubectl apply Nova AI разворачивает все необходимые компоненты для взаимодействия с GPU.

Манифест софтверного уровня

Софтварный манифест сложнее, потому что содержит больше компонентов. Ключевой принцип — модульность: платформа не навязывает лишнего. Если нужен только Airflow для сценариев автоматизации, можно поставить только этот компонент:

apiVersion: apps.nova-platform.io/v1alpha1

kind: MLCluster

metadata:

  name: nova-ml-cluster

spec:

  mlBaseDomain: "apps.test.platform"

  storageClass: longhorn-storage-single-replica

  jupyterHub:

    enabled: true

  airFlow:

    enabled: true

    logVolumeSize: 5G

  mlFlow:

    enabled: true

  kubeRay:

    enabled: true

  kserve:

    enabled: true

  postgreSQL:

    enabled: true

  minIO:

    enabled: true

    driveCount: 1

    driveSize: 25Gi
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 3

Остальные компоненты не будут развернуты и не будут загружать лишние ресурсы.

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

externalDatabase:

        enabled: true

        host: "psql-postgresql.nova-postgresql.svc"

        port: 5432

        database: mlflow

        userDatabase: mlflow_auth

        starVaultUserSecretPath: "nova-secrets/data/credentials/external-integrations#pg_password"

        starVaultPassSecretPath: "nova-secrets/data/credentials/external-integrations#pg_password"

    externalS3:

        enabled: true

        host: "minio.nova-minio.svc"

        port: 443

        bucketRegion: "ru-nova-1"

        bucket: "ml"

        starVaultUserSecretPath: "nova-secrets/data/credentials/external-integrations#root_user"

        starVaultPassSecretPath: "nova-secrets/data/credentials/external-integrations#root_password"
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 4

Также можно переопределять storage-классы, чтобы не завязываться на одном типе хранилища.

При полной конфигурации развертывание всей платформы на развернутом кластере занимает от 7 до 15 минут. 

Кстати, в этом же манифесте представлена интеграция одного из ML-компонентов с StarVault – российским хранилищем секретов, которое также разрабатываем мы :) 

Компоненты платформы

NVIDIA GPU Operator

Это компонент инфраструктурного уровня, который обеспечивает работу с видеокартами из подов. Пользователь может написать свой сервис, использующий GPU для обучения, запустить его, указать в описании deployment, что хочет использовать GPU — и GPU автоматически туда подтянется.

GPU Operator состоит из нескольких подкомпонентов: Device Plugins, Runtimes, модуль мониторинга, автоматическое обновление драйверов и гибкая конфигурация механизмов взаимодействия с GPU.

Ключевой функционал оператора — возможность делить GPU на более атомарные юниты. Это решает важную проблему: когда мы просто работаем с GPU без деления, у нас есть одна GPU в worker-ноде, и мы можем привязать к ней только один под. Еще хуже, когда легкая модель откусывает 10% ресурсов видеокарты, а 90% простаивает.

Time Slicing — более старая технология, которая делит GPU на виртуальные слайсы. Kubernetes видит их не как одно устройство, а как несколько. Минусы: псевдопараллелизм (задачи не выполняются параллельно) и отсутствие изоляции по видеопамяти — если один под перегрузит память, упадут обе нагрузки.

MIG (Multi-Instance GPU) работает начиная с архитектуры Ampere (примерно с видеокарт A30). Это полноценные независимые инстансы — виртуальные микро-GPU с полной изоляцией по памяти и настоящим параллелизмом. Задачи выполняются каждая в своем адресном пространстве и не затрагивают друг друга.

Что касается объединения нескольких GPU в единое пространство — это делается на другом уровне: на уровне железа через NVLink или на уровне софта, когда в под прокидываются две видеокарты и софт внутри распределяет задачи. Для распределенных вычислений между серверами есть Kuberay.

JupyterHub

Многопользовательская среда разработки. Дает единую точку входа, где пользователи могут заказывать ноутбуки нужного размера. Один пользователь может создать несколько ноутбуков, решать в них задачи и удалять, когда работа закончена.

Интеграция с StarVault обеспечивает единый механизм аутентификации. Сам ноутбук — это под внутри Kubernetes. В планах вынести конфигурацию ресурсов на сторону пользователя: выбор CPU, RAM и количества GPU через интерфейс.

Apache Airflow

Универсальный компонент для автоматизации задач — применим не только в ML/AI, но и в BI-системах и любых сферах, требующих автоматизации. Позволяет писать задачи в Python-скриптах, а Airflow контролирует их выполнение по расписанию, по триггеру или вручную.

Процессы описываются в DAG (направленных ациклических графах). DAG — это pipeline, в котором описываются задачи. Когда наступает время выполнения, автоматически запускается под внутри Kubernetes, задача выполняется, результат возвращается.

Все DAG’и — это клон Git-репозитория в Gitea. Пользователь пишет DAG в своем репозитории, делает commit, он прилетает в Gitea, и Airflow автоматически обновляет DAG из этого репозитория.

MLflow

Сервис для трекинга метрик моделей и экспериментов. Нужен, чтобы пользователи не потерялись в том, что они наделали — при обучении моделей возникает огромное количество вариаций параметров, десятки экспериментов с разными параметрами и эпохами обучения.

MLflow позволяет хранить все в едином месте: трекинг метрик и экспериментов, хранение и версионирование моделей, упаковку модели в оболочку для деплоя. Эксперимент — это один инстанс обучения модели. Три раза запустили обучение — три эксперимента. В каждом можно посмотреть метрики, конфигурацию, прогрессию метрик в разрезе эпох.

Можно сравнивать эксперименты между собой и тегировать их. Например, тег success: true можно прикрутить к автоматизации в Airflow: Airflow наблюдает за тегами, видит, что появились модели с тегом success, и автоматически деплоит их в Production.

MLflow позволяет складывать артефакты в S3-хранилище: файлы модели для деплоя, графики (например, Confusion Matrix), логи и визуализации.

KServe

Компонент для inference — вывода моделей в продакшн. Как показывает практика, в России компании только идут к тому, чтобы обучать и дообучать свои модели. Пока больший процент спроса именно на inference — взаимодействие с готовыми моделями, которые запихнули в контейнеры.

KServe — стандарт в отрасли, если речь идет про промышленный инференс. Компонент является универсальным сервисом для деплоя почти любой модели. Работает через унифицированный интерфейс: заполняете манифест, указываете модель, откуда ее взять, и выбираете inference backend.

Из коробки KServe поставляет огромное количество inference-бэкендов: vLLM (для больших языковых моделей), ONNX (универсальный формат), PyTorch, TensorFlow, Triton Inference Server от NVIDIA и другие. За счет этого можно инферить практически любую модель. Например, большие языковые модели инферятся на vLLM, но vLLM не поддерживает ряд embedder-моделей — в этом случае можно выбрать ONNX. Весь прикол в вариативности.

Помимо стандартных бэкендов, KServe позволяет конфигурировать дополнительные. Если понадобится что-то специфичное, можно написать свой адаптер. Доступные бэкенды можно посмотреть через kubectl get clusterservingruntimes.

Что касается безопасности — это пользовательская история. Можно использовать OAuth2 Proxy для авторизации, ролевую модель для ограничения доступа, Ingress с аннотациями. Если нужна кастомизация пода (например, sidecar-контейнер с OAuth2 Proxy), это можно сделать в манифесте InferenceService.

Kuberay

Компонент для распределенных вычислений. Kuberay позволяет создавать кластеры внутри кластеров. Под капотом: в основе лежит Kubernetes, внутри которого создается Ray Cluster с Head-нодами (следят за выполнением задач) и Worker-нодами (выполняют задачи).

Задачи могут шардироваться и разделяться на несколько частей, которые отправляются на разные worker-ноды и выполняются параллельно. Kuberay особенно полезен для распределенного обучения моделей, обработки больших объемов данных, параллельного inference и задач, которые можно разбить на независимые части. Если GPU находятся на разных серверах, Kuberay распределяет обучение модели между ними.

Сопровождающие компоненты

Это модули, которые нужны для функционирования основных компонентов. Можно использовать как внешние сервисы (PaaS в облаке), так и внутренние (развернутые в Kubernetes). Если пользователь не хочет заморачиваться, платформа автоматически поднимает необходимые компоненты.

PostgreSQL хранит метаданные сервисов: какой ноутбук создан для какого пользователя в JupyterHub, информация о DAG’ах в Airflow, информация об экспериментах в MLflow. Это только метаданные, не датасеты и не данные моделей. PostgreSQL выбран как единственная база данных, которая поддерживается всеми компонентами.

Gitea — внутренний Git-сервис. Хранит DAG’и для Airflow, скрипты обучения и т.д. Пользователь делает commit в Gitea, и Airflow автоматически подтягивает изменения. Можно использовать внешний Git.

MinIO — S3-совместимое хранилище для артефактов. Хранит файлы моделей из MLflow, артефакты экспериментов, веса моделей. MLflow использует MinIO как основное хранилище. Можно использовать внешний S3.

Практические кейсы использования

Теперь давайте разберем реализацию основных задач, с которыми сталкиваются компании, желающие уверенно начать и масштабировать количество работающих LLM и ML-пайплайнов. 

Кейс 1: Быстрый Inference через KServe

Задача: Быстро задеплоить модель для общения с ней.

Манифест описывается согласно стандартной документации KServe:

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: qwen-model
  namespace: nova
spec:
  predictor:
    model:
      modelFormat:
        name: huggingface  # Используем Hugging Face backend
      storageUri: "hf://Qwen/Qwen2.5-1.5B"  # Модель Qwen 2.5 на 1.5B параметров
      resources:
        limits:
          nvidia.com/gpu: 1  # Запрашиваем 1 GPU
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 5

Указываем backend (Hugging Face, под капотом использует vLLM), модель (Qwen 2.5 на 1.5 миллиарда параметров) и ресурсы (1 GPU). Применяем манифест через kubectl apply.

После этого KServe подхватывает манифест, начинает разворачивать под с моделью, скачивает образ контейнера и модель, загружает модель в видеопамять. Время развертывания — примерно 4 минуты. Мир машинного обучения — это про ожидание: образы весят много, модели весят много.

Для оптимизации времени развертывания можно складывать файлы моделей в PVC и тянуть их напрямую из хранилища внутри Kubernetes:

spec:
  predictor:
    model:
      storageUri: "pvc://model-storage/qwen-2.5"  # Из PVC

После развертывания можно отправить запрос к модели:

curl -X POST http://qwen-model.nova.svc.cluster.local/v1/completions 
  -H "Content-Type: application/json" 
  -d '{
    "prompt": "Как варить гречку?",
    "max_tokens": 100
  }'

Если модель не поднимается на GPU T4 с ошибкой CUDA out of memory — T4 слабенькая для этой модели. Решение: взять более мощную карту или урезать модель, изменив параметр dtype:

spec:
  predictor:
    model:
      args:
        - --dtype=float16  # Вместо float32
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 6

Преимущества KServe: универсальность (поддержка множества бэкендов), скорость (быстро описали манифест — быстро применили — модель быстро поднялась), гибкость (выбор бэкенда под задачу). Минус: нестандартные endpoint’ы, не всегда совпадают с OpenAI API. Доступ к модели можно вытащить наружу через Ingress или NodePort, это обычный HTTP API, можно настроить OAuth2 Proxy для аутентификации.

После запуска кстати можно кстати поднять в нашей же платформе тот же OpenWebUI и подключить к ней LLM, далее опубликовать через ingress – и вот у вас уже безопасная LLM, доступная для всех сотрудников компании, можно отправлять любые конфиденциальные данные в нее и не переживать за их сохранность.

Кейс 2: Распределенный Inference через Kuberay

Задача: Развернуть модель распределенно. Это когда часть LLM выполняется на GPU одного узла, а вторая часть – на другом GPU второго узла. Это стандартный способ доутилизировать оставшиеся ресурсы GPU нашего кластера, чтобы выполнить больше задач. Покупать GPU на десятки миллионов ради 10% загрузки — сомнительное удовольствие.

Схема похожа на KServe, но есть принципиальные отличия. Вместо одного пода мы описываем кластер внутри кластера, в котором все будет работать.

Описание Ray Cluster:

apiVersion: ray.io/v1
kind: RayCluster
metadata:
  name: ray-cluster
  namespace: nova
spec:
  # Конфигурация Head-ноды
  headGroupSpec:
    rayStartParams:
      dashboard-host: '0.0.0.0'
    template:
      spec:
        containers:
        - name: ray-head
          image: rayproject/ray:latest
          resources:
            limits:
              cpu: "2"
              memory: "4Gi"

  # Конфигурация Worker-нод
  workerGroupSpecs:
  - replicas: 3
    minReplicas: 1
    maxReplicas: 5
    groupName: gpu-workers
    rayStartParams: {}
    template:
      spec:
        containers:
        - name: ray-worker
          image: rayproject/ray:latest
          resources:
            limits:
              nvidia.com/gpu: 1
              cpu: "4"
              memory: "8Gi"
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 7

Мы описываем Head-ноду (управляющий узел кластера) и Worker-ноды (рабочие узлы с GPU).

Описание RayService для модели:

apiVersion: ray.io/v1
kind: RayService
metadata:
  name: llm-service
  namespace: nova
spec:
  serviceUnhealthySecondThreshold: 900
  deploymentUnhealthySecondThreshold: 300

  serveConfigV2: |
    applications:
      - name: llm
        import_path: serve_model:deployment
        runtime_env:
          working_dir: "https://github.com/example/model-serving.git"
        deployments:
          - name: LLMDeployment
            num_replicas: 3
            ray_actor_options:
              num_gpus: 1
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 8

Создается сервис на базе Ray Cluster, модель разворачивается на нескольких worker-нодах, запросы распределяются между репликами.

Преимущества Kuberay: распределенные вычисления (задачи распределяются между несколькими GPU), масштабируемость (можно добавлять/убирать worker-ноды), отказоустойчивость (если один worker падает, задачи перераспределяются), эффективное использование ресурсов (GPU на разных серверах работают как единое целое).

Когда использовать Kuberay? Большие модели, которые не помещаются на одну GPU. Высоконагруженные сервисы с большим количеством запросов. Распределенное обучение моделей. Параллельная обработка данных.

Кейс 3: Полный ML Pipeline с Airflow и MLflow

Задача: Автоматизировать процесс обучения, трекинга и деплоя модели.

Архитектура решения: Gitea (DAG код) → Airflow (Обучение) → MLflow (Трекинг) → KServe (Deploy).

Написание DAG для обучения:

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime
import mlflow

def train_model():
    """Функция обучения модели"""
    import torch
    from transformers import AutoModelForSequenceClassification, AutoTokenizer

    # Начинаем MLflow эксперимент
    mlflow.start_run()

    # Параметры обучения
    params = {
        'learning_rate': 0.001,
        'epochs': 10,
        'batch_size': 32
    }
    mlflow.log_params(params)

    # Обучение модели (упрощенно)
    model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased')

    # Логируем метрики
    for epoch in range(params['epochs']):
        # ... процесс обучения ...
        train_loss = 0.5 - epoch  0.02  # Пример
        accuracy = 0.7 + epoch  0.02     # Пример

        mlflow.log_metric('train_loss', train_loss, step=epoch)
        mlflow.log_metric('accuracy', accuracy, step=epoch)

    # Сохраняем модель
    mlflow.pytorch.log_model(model, "model")

    # Добавляем тег успешности
    mlflow.set_tag("success", "true")

    mlflow.end_run()

def check_and_deploy():
    """Проверяем успешные модели и деплоим"""
    # Ищем эксперименты с тегом success=true
    experiments = mlflow.search_runs(filter_string="tags.success='true'")

    if not experiments.empty:
        # Берем последний успешный эксперимент
        latest_run = experiments.iloc[0]
        model_uri = f"runs:/{latest_run.run_id}/model"

        # Деплоим через KServe (применяем манифест)
        # ... код деплоя ...
        print(f"Deployed model from run {latest_run.run_id}")

# Определяем DAG
with DAG(
    'ml_training_pipeline',
    start_date=datetime(2024, 1, 1),
    schedule_interval='@daily',  # Каждый день
    catchup=False
) as dag:

    train_task = PythonOperator(
        task_id='train_model',
        python_callable=train_model
    )

    deploy_task = PythonOperator(
        task_id='check_and_deploy',
        python_callable=check_and_deploy
    )

    # Определяем порядок выполнения
    train_task >> deploy_task
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 9

После написания DAG делаем коммит в Gitea. Airflow автоматически синхронизируется с Gitea, обнаруживает новый DAG и добавляет его в список доступных пайплайнов.

В интерфейсе Airflow видим новый DAG ml_training_pipeline, можем запустить вручную или дождаться расписания. В интерфейсе MLflow видим новый эксперимент, можем посмотреть метрики в реальном времени, сравнить с предыдущими, скачать артефакты.

Если модель успешно обучилась (тег success=true), задача check_and_deploy находит ее в MLflow и автоматически создает InferenceService в KServe — модель деплоится в продакшн.

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

Кейс 4: Разработка в JupyterHub с последующим деплоем

Задача: Разработать модель в ноутбуке, обучить ее и задеплоить.

Переходим в JupyterHub через роутдэшборд, аутентифицируемся, заказываем ноутбук с нужными ресурсами (CPU: 4 cores, RAM: 16 GB, GPU: 1x NVIDIA T4, Storage: 50 GB).

В ноутбуке пишем код:

# Установка зависимостей
!pip install transformers torch mlflow

# Импорты
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import mlflow

# Настройка MLflow
mlflow.set_tracking_uri("http://mlflow.nova.svc.cluster.local")
mlflow.set_experiment("sentiment-analysis")

# Загрузка данных
# ... код загрузки данных ...

# Обучение модели
with mlflow.start_run():
    model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased')
    tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')

    # Параметры
    params = {'learning_rate': 0.001, 'epochs': 5}
    mlflow.log_params(params)

    # Обучение
    for epoch in range(params['epochs']):
        # ... код обучения ...
        mlflow.log_metric('loss', loss, step=epoch)
        mlflow.log_metric('accuracy', accuracy, step=epoch)

    # Сохранение модели
    mlflow.pytorch.log_model(model, "model")

    # Сохранение токенизатора
    tokenizer.save_pretrained("./tokenizer")
    mlflow.log_artifacts("./tokenizer", artifact_path="tokenizer")

Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 10

Сохраняем модель в PVC для быстрого доступа:

model.save_pretrained("/mnt/models/sentiment-analysis")
tokenizer.save_pretrained("/mnt/models/sentiment-analysis")
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 11

Создаем манифест для деплоя:

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: sentiment-analysis
  namespace: nova
spec:
  predictor:
    pytorch:
      storageUri: "pvc://model-storage/sentiment-analysis"
      resources:
        limits:
          nvidia.com/gpu: 1
          memory: "4Gi"
        requests:
          cpu: "1"
          memory: "2Gi"
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 12

Применяем манифест и тестируем:

# Применяем манифест
kubectl apply -f sentiment-analysis.yaml

# Ждем готовности
kubectl wait --for=condition=Ready inferenceservice/sentiment-analysis -n nova

# Тестируем
curl -X POST http://sentiment-analysis.nova.svc.cluster.local/v1/models/sentiment-analysis:predict 
  -H "Content-Type: application/json" 
  -d '{
    "instances": [
      {"text": "This product is amazing!"}
    ]
  }'
Как мы запускаем LLM on-prem в Kubernetes и выжимаем максимум из GPU-кластера - 13

Преимущества подхода: быстрая итерация (разработка в привычной среде Jupyter), доступ к GPU (можно обучать модели прямо в ноутбуке), интеграция с MLflow (автоматический трекинг экспериментов), простой деплой (из ноутбука сразу в продакшн).

Заключение

Nova AI — это платформа, которая покрывает полный цикл работы с ML/LLM-моделями и ресурсами GPU: от разработки в JupyterHub до деплоя через KServe, с автоматизацией через Airflow и трекингом экспериментов в MLflow.

Ключевые преимущества: 

  • модульность (устанавливайте только то, что нужно)

  • гибкость (используйте внешние или внутренние сервисы)

  • знакомые инструменты (компоненты, с которыми работают ML-инженеры)

  • полный цикл ML (от разработки до продакшна)

  • простота входа (развертывание продуктивного кластера за день и начало работы)

  • масштабируемость (от одного GPU до распределенных кластеров).

Пишите в комментариях, что вам интересно узнать из нашей практики еще, постараюсь раскрыть это в следующих статьях.

Автор: NVekesser

Источник

Rambler's Top100