Операторы упрощают жизнь, но они доступны не всегда. В этой статье я покажу практичный способ развернуть большие языковые модели (Large Language Models, LLM) на OpenShift без использования операторов OpenShift AI или NVIDIA. Подход строится на llama.cpp как лёгком движке вывода (inference runtime) и запускает квантованную модель в формате GGUF — granite-4.0-h-350m-Q4\_K\_M.gguf — что обеспечивает эффективный вывод при минимальном числе зависимостей.
Эта статья не претендует на замену production-решений. Такие платформы, как OpenShift AI, использующие под капотом KServe и предоставляющие богатый набор оптимизированных сред выполнения для разных AI-фреймворков, лучше подходят для корпоративных развёртываний. Описываемый подход ориентирован на разработку, эксперименты и среды, где доступны только нативные ресурсы Kubernetes и OpenShift.
Почему именно эта модель?
Выбор модели в данной схеме определяется прежде всего инфраструктурными ограничениями и простотой развёртывания, а не размером модели или точностью. Выбранная модель — granite-4.0-h-350m-Q4\_K\_M.gguf — является квантованной GGUF-моделью, что делает её оптимальной для сред, полностью полагающихся на вывод на CPU.
Благодаря совместимости с llama.cpp и бэкендом GGML модель эффективно работает без GPU-ускорения, сохраняя приемлемую производительность и умеренное потребление памяти. Квантование существенно уменьшает объём модели, позволяя ей работать в рамках ресурсов скромного кластера OpenShift без GPU-узлов.
Это делает модель удобным выбором для разработки, тестирования и нетребовательных рабочих нагрузок вывода — особенно в кластерах, где добавление GPU или специализированных операторов невозможно.
Процедура развёртывания
Развёртывание намеренно устроено просто: используются только нативные ресурсы OpenShift и Kubernetes. Процесс разбит на три основных шага:
-
Сборка образа контейнера для обслуживания модели Сначала создаётся образ контейнера, который обслуживает LLM с помощью llama.cpp. Для компиляции среды выполнения и эффективной упаковки квантованной модели применяется многоэтапная сборка Docker (multi-stage build), результатом которой становится компактный и переносимый образ. Такой подход сокращает размер образа, повышает воспроизводимость сборки и исключает лишние зависимости времени выполнения. Детали процесса сборки рассматриваются в следующем разделе.
-
Развёртывание модели на OpenShift После подготовки образа создаётся стандартный Deployment Kubernetes, запускающий сервер модели. В манифесте указываются требования к ресурсам, аргументы контейнера и базовая конфигурация для старта inference-сервиса llama.cpp. Никаких пользовательских ресурсов и операторов не требуется — только стандартный Deployment.
-
Публикация и проверка endpoint модели Наконец, развёртывание публикуется с помощью Kubernetes Service и ingress (а также, при желании, Route OpenShift), открывая доступ к API модели. После появления endpoint можно отправить тестовые запросы и убедиться, что модель работает корректно.
Выполнив эти шаги, вы получите полноценный inference endpoint LLM на OpenShift, пригодный для разработки, тестирования или сред, где операторные решения недоступны.
Сборка образа контейнера для обслуживания модели
Чтобы финальный образ был безопасным, компактным и не содержал лишних зависимостей сборки, используется многоэтапная сборка Docker. Этот подход разделяет получение модели и её обслуживание, гарантируя, что в финальный образ попадёт только то, что строго необходимо во время выполнения.
Этап 1: Builder
Первый этап отвечает исключительно за загрузку и проверку артефактов модели.
-
Базовый образ:
registry.access.redhat.com/ubi9/python-311 -
Назначение: безопасно получить квантованные веса модели.
-
Процесс:
-
Установить Python-пакет
huggingface-hub. -
Выполнить вспомогательный скрипт
download_model.pyдля загрузки нужного.gguf-файла. -
Проверить целостность загруженного файла, убедившись в отсутствии повреждений или подмены.
-
На этом этапе присутствуют Python-инструменты и зависимости, но ни одна из них не переносится в финальный образ.
Этап 2: Runtime (финальный образ)
Второй этап формирует финальный запускаемый образ, который будет развёрнут на OpenShift.
-
Базовый образ:
ghcr.io/ggml-org/llama.cpp:server -
Назначение: запустить inference-движок LLM с помощью llama.cpp.
-
Процесс:
-
Скопировать только нужный
.gguf-файл модели из этапа builder. -
Отбросить все зависимости сборки: Python-пакеты, кэши pip и промежуточные файлы.
-
Задать точку входа контейнера для запуска процесса
llama-server.
-
Изолируя загрузку модели в этапе builder и сохраняя runtime-образ минимальным, эта схема повышает безопасность, уменьшает размер образа и соответствует лучшим практикам OpenShift для контейнерных рабочих нагрузок.
FROM registry.access.redhat.com/ubi9/python-311:latest as builder
USER root
# Установка библиотеки huggingface
RUN pip install huggingface-hub
WORKDIR /build
# Копирование python-скрипта
COPY download_model.py .
# Запуск загрузки (результирующий файл окажется в /build/granite-4.0-h-350m-Q4_K_M.gguf)
RUN python download_model.py
FROM ghcr.io/ggml-org/llama.cpp:server
# Копирование ТОЛЬКО файла модели из этапа builder
COPY --from=builder /build/granite-4.0-h-350m-Q4_K_M.gguf /model.gguf
# Открытие порта API
EXPOSE 8080
# Команда запуска сервера с указанной моделью
# --host 0.0.0.0: слушать на всех интерфейсах
# --port 8080: порт, совместимый с OpenAI
CMD ["-m", "/model.gguf", "--host", "0.0.0.0", "--port", "8080"]
Скрипт download\_model.py отвечает за безопасное и эффективное получение конкретного файла модели:
from huggingface_hub import hf_hub_download
import shutil
import os
# Определяем репозиторий модели и конкретный файл
repo_id = "ibm-granite/granite-4.0-h-350m-GGUF"
filename = "granite-4.0-h-350m-Q4_K_M.gguf"
print(f"Downloading {filename} from {repo_id}...")
# Загружаем файл в локальный кэш
model_path = hf_hub_download(
repo_id=repo_id,
filename=filename,
local_dir=".", # Загрузить в текущий каталог
local_dir_use_symlinks=False # Сохранить как реальный файл, не симлинк
)
Здесь используется официальная Python-библиотека Hugging Face. Это надёжнее, чем curl или wget: библиотека автоматически обрабатывает аутентификацию (при необходимости), перенаправления и верификацию.
podman build -t granite-api .
Развёртывание модели на OpenShift
После сборки образа следующий шаг — развернуть его с помощью стандартного Deployment Kubernetes. Это сохраняет схему простой и не требует зависимостей от пользовательских ресурсов или операторов.
Конфигурация развёртывания
A. Deployment
-
Replicas: 1 Развёртывание стартует с одной репликой, чего достаточно для разработки и тестирования. Нагрузку можно масштабировать горизонтально по мере необходимости; однако масштабирование LLM требует доработки балансировки нагрузки для сохранения согласованности сессий (одинаковый KV-кэш).
-
Ресурсы: Запросы и лимиты ресурсов задаются для предсказуемого планирования и предотвращения ресурсного голодания:
-
Requests: CPU: 1, Memory: 1Gi
-
Limits: CPU: 4, Memory: 2Gi
-
Эта конфигурация даёт достаточный запас для CPU-вывода, ограничивая нагрузку в пределах доступных ресурсов кластера.
-
-
Проверки работоспособности (Health Checks): Пробы настраиваются для повышения надёжности и корректного поведения сервиса в OpenShift:
-
Readiness Probe: гарантирует, что модель полностью загружена и готова обслуживать запросы, прежде чем под будет добавлен в endpoints сервиса.
-
Liveness Probe: отслеживает доступность API и перезапускает контейнер, если inference-сервер перестаёт отвечать.
-
Эта модель развёртывания хорошо подходит для кластеров OpenShift только с CPU и обеспечивает надёжную основу для лёгких inference-нагрузок LLM.
apiVersion: apps/v1
kind: Deployment
metadata:
name: granite-deployment
labels:
app: granite-api
spec:
replicas: 1 # Количество запускаемых подов
selector:
matchLabels:
app: granite-api
template:
metadata:
labels:
app: granite-api
spec:
containers:
- name: granite-container
image: granite-api:latest # Замените на имя вашего образа
imagePullPolicy: IfNotPresent
args:
- "-m"
- "/model.gguf"
- "--host"
- "0.0.0.0"
- "--port"
- "8080"
- "--ctx-size"
- "8192"
ports:
- containerPort: 8080
name: http
# Ресурсы: скорректированы для модели 350M с контекстом 8192
resources:
requests:
memory: "4Gi"
cpu: "2"
limits:
memory: "8Gi"
cpu: "4"
# Проверки работоспособности
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
oc apply -f granite-deploy.yaml
Публикация endpoint модели
После запуска развёртывания API модели необходимо сделать доступным для обращения и тестирования. Это реализуется на двух уровнях: внутренний Service и внешний Route.
B. Service
Service предоставляет стабильный внутренний endpoint для сервера модели и абстрагирует от динамической природы IP-адресов подов.
-
Тип: ClusterIP
-
Функция: выступает стабильной внутренней точкой доступа для развёртывания, позволяя другим подам или компонентам OpenShift взаимодействовать с API модели, не зная IP конкретных подов.
C. Route
Route открывает внешний доступ к Service через роутер OpenShift.
-
Функция: делает API модели доступным по публичному URL.
-
Безопасность: используется Edge TLS termination — трафик шифруется между клиентом и роутером OpenShift, а конфигурация бэкенда остаётся простой.
apiVersion: v1
kind: Service
metadata:
name: granite-service
spec:
selector:
app: granite-api
ports:
- name: http
protocol: TCP
port: 80 # Порт, на котором другие поды обращаются к сервису
targetPort: 8080 # Реальный порт контейнера
type: ClusterIP
---
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: granite-route
spec:
to:
kind: Service
name: granite-service
weight: 100
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
oc apply -f granite-expose.yaml
Проверка работоспособности
После создания Service и Route остаётся убедиться, что модель доступна и отвечает корректно. Сервер llama.cpp предоставляет API, совместимый с OpenAI, что позволяет проверить вывод простым HTTP-запросом.
Замените <YOUR-ROUTE-URL> на имя хоста, сгенерированное Route OpenShift, и выполните следующую команду:
curl https://<YOUR-ROUTE-URL>/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "granite-350m",
"messages": [ { "role": "user", "content": "How many pyramids are in Egypt?" } ] }'
При успешном развёртывании API вернёт JSON-ответ с ответом, сгенерированным моделью. Это подтверждает, что модель загружена корректно, inference-сервер работает, а endpoint доступен через Route OpenShift.
Заключение
В статье показан лёгкий и практичный способ развернуть большую языковую модель на OpenShift без специализированных операторов и GPU-ускорения. Используя квантованную GGUF-модель с llama.cpp, многоэтапную сборку контейнера и стандартные ресурсы Kubernetes, можно запустить LLM-вывод даже в ограниченных средах только с CPU.
Хотя такая схема хорошо подходит для разработки, экспериментов и кластеров с ограниченными операторами, она не заменяет production-платформы вроде OpenShift AI и KServe, предлагающие расширенное управление моделями, масштабирование и наблюдаемость. Зато она даёт гибкую альтернативу там, где простота, переносимость или минимальные зависимости — главный приоритет.
В итоге этот подход наглядно показывает: для запуска LLM на OpenShift не всегда нужен сложный инструментарий — при правильном выборе модели и среды выполнения базовых примитивов Kubernetes вполне достаточно, чтобы начать работу.