Перейти к содержанию

Перенос моделей Hugging Face в локальное S3 для запуска vLLM в закрытом контуре

Инструкция описывает загрузку модели с Hugging Face на машину с доступом в Интернет, перенос полного снапшота модели в закрытый контур и загрузку его в локальное S3-совместимое хранилище. После этого vLLM запускается внутри закрытого контура и получает модель из локального S3 либо из предварительно загруженного каталога на PVC.

Когда это пригодится

  • В закрытом контуре без доступа к сети Интернет необходимо запустить инференс модели через vLLM
  • Требуется контролируемая поставка моделей: фиксация версии, контроль целостности, исключение обращений к внешним ресурсам
  • Требуется развернуть модель в Kubernetes-кластере с GPU-нодами, где модель хранится в локальном S3

Основной сценарий:

                 INTERNET
          ┌───────────────────┐
          │ Internet Host     │
          │ Hugging Face      │
          │ huggingface_hub   │
          └─────────┬─────────┘
                    │ download
          ┌───────────────────┐
          │ Model Package     │
          │ config.json       │
          │ tokenizer.*       │
          │ *.safetensors     │
          │ generation_config │
          └─────────┬─────────┘
                    │ SHA256
          ┌───────────────────┐
          │ Transfer Package  │
          └─────────┬─────────┘
                    │ USB / SSD / контролируемый канал
          ┌───────────────────┐
          │ Air-gapped Host   │
          └─────────┬─────────┘
                    │ upload
          ┌───────────────────┐
          │ Local S3          │
          │ s3://models/...   │
          └─────────┬─────────┘
          ┌───────────────────┐
          │ Kubernetes / vLLM │
          │ model from S3     │
          └───────────────────┘

Обозначения в документе

Плейсхолдер Значение
<COMMIT-SHA> SHA-1 коммита модели в репозитории Hugging Face; фиксируется на шаге 1 и используется как immutable-префикс в S3
s3.example.local Адрес S3-совместимого хранилища в закрытом контуре
registry.example.local Адрес локального container registry в закрытом контуре
Qwen/Qwen3-8B Пример репозитория модели; заменяйте на вашу модель
vllm.example.local Адрес сервиса vLLM для проверки API

Предварительные требования

Internet-хост (машина с доступом к Hugging Face):

  • Python 3
  • Пакет huggingface_hub (CLI hf)
  • Токен Hugging Face для gated/private-моделей (для публичных моделей не требуется)
  • Опционально: GPU для полной проверки модели через vLLM до переноса

Закрытый контур:

  • S3-совместимое хранилище: endpoint, bucket, credentials
  • Kubernetes-кластер с GPU-нодами (см. GPU-оператор Metax)
  • vLLM-образ в локальном registry
  • StorageClass и PVC для хранения модели (см. Longhorn и Velero — контекст S3-хранилищ)
  • Достаточный объём VRAM и дискового пространства под модель

Быстрый путь: минимальный вариант для разовой поставки

Если требуется максимально простой сценарий без полных проверок — выполните следующее. Детальный сценарий с фиксацией commit SHA, inventory, контрольными суммами и промышленной схемой описан в шагах 1–5.

На Internet-хосте:

hf download \
    Qwen/Qwen3-8B \
    --revision <COMMIT-SHA> \
    --local-dir ./Qwen3-8B

Проверьте структуру:

find Qwen3-8B -type f | sort

Создайте контрольные суммы:

find Qwen3-8B \
    -type f \
    -exec sha256sum {} \; \
    > SHA256SUMS

Заархивируйте и посчитайте SHA-256 архива:

tar -cf Qwen3-8B.tar Qwen3-8B
sha256sum Qwen3-8B.tar > Qwen3-8B.tar.sha256

Перенесите архив на USB.

В закрытом контуре:

sha256sum -c Qwen3-8B.tar.sha256
tar -xf Qwen3-8B.tar

Загрузите в S3:

aws s3 sync \
    Qwen3-8B/ \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --endpoint-url https://s3.example.local

Проверьте содержимое:

aws s3 ls \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --recursive \
    --endpoint-url https://s3.example.local

После этого модель можно использовать для vLLM deployment.

Ограничения быстрого пути

В быстром пути не фиксируются commit SHA модели, inventory и контрольные суммы файлов до переноса, не проверяется повторно целостность после загрузки в S3. Для воспроизводимой и проверяемой поставки используйте шаги 1–5.

Шаг 1. Скачивание снапшота модели на Internet-хосте

1.1. Подготовка Internet-хоста

Проверьте Python и установите huggingface_hub:

python3 --version
python3 -m pip install -U huggingface_hub

Проверьте CLI. В современных версиях huggingface_hub основным CLI является hf:

huggingface-cli --help
hf --help

1.2. Авторизация в Hugging Face

Для публичной модели авторизация может не потребоваться. Для gated/private-моделей необходим токен:

hf auth login

Введите Hugging Face Access Token. Проверьте авторизацию:

hf auth whoami

1.3. Создание рабочего каталога

export WORKDIR=/opt/model-transfer
mkdir -p "$WORKDIR"/{models,manifests,checksums,logs,scripts}

Результат:

/opt/model-transfer/
├── models/
├── manifests/
├── checksums/
├── logs/
└── scripts/

1.4. Фиксация модели: repository, revision, commit SHA

Не используйте только имя репозитория (Qwen/Qwen3-8B) как идентификатор поставки — ветка main может измениться. Фиксируйте три параметра:

Параметр Значение
Repository Qwen/Qwen3-8B
Revision main
Commit SHA <COMMIT-SHA>

Commit SHA однозначно идентифицирует состояние модели.

1.5. Получение информации о модели

Посмотрите, какие файлы будут скачаны, и оцените размер:

hf download Qwen/Qwen3-8B --dry-run

Для конкретного revision:

hf download \
    Qwen/Qwen3-8B \
    --revision <COMMIT-SHA> \
    --dry-run

1.6. Скачивание снапшота целиком

Скачивайте снапшот целиком, а не только веса

vLLM требуется как минимум config.json + веса + tokenizer. В зависимости от модели могут понадобиться generation_config.json, tokenizer_config.json, special_tokens_map.json, chat_template.jinja, индекс весов (model.safetensors.index.json) и custom-код модели. Правильная единица поставки — весь снапшот (Model Release), а не отдельные *.safetensors.

hf download \
    Qwen/Qwen3-8B \
    --revision <COMMIT-SHA> \
    --local-dir "$WORKDIR/models/Qwen3-8B"

Используйте --local-dir, а не стандартный cache ~/.cache/huggingface/: модель сразу лежит в самостоятельном каталоге, который можно проверить, заархивировать, посчитать checksum и перенести.

Почему нельзя скачать только *.safetensors Распространённая ошибка — «мне нужны веса модели, скачаю `*.safetensors`». Для `vLLM` этого недостаточно: без `config.json` и `tokenizer` модель не загрузится. Правильная единица поставки — Model Release, а не `model.safetensors`.

1.7. Проверка снапшота на Internet-хосте

Проверьте структуру и размер:

find "$WORKDIR/models/Qwen3-8B" -type f | sort
du -sh "$WORKDIR/models/Qwen3-8B"

Пример вывода:

42G    /opt/model-transfer/models/Qwen3-8B

Проверьте обязательные файлы — конфигурацию, токенизатор и веса:

test -f "$WORKDIR/models/Qwen3-8B/config.json"
find "$WORKDIR/models/Qwen3-8B" -maxdepth 1 -type f | grep tokenizer
find "$WORKDIR/models/Qwen3-8B" \
    -type f \
    \( -name '*.safetensors' -o -name '*.bin' -o -name '*.pt' \)

Посмотрите содержимое config.json — особенно поля architectures, model_type, torch_dtype, hidden_size, num_hidden_layers, num_attention_heads:

jq '{ architectures, model_type, torch_dtype, hidden_size, num_hidden_layers, num_attention_heads }' \
    "$WORKDIR/models/Qwen3-8B/config.json"

Убедитесь, что снапшот корректно загружается через Transformers. Установите зависимости:

python3 -m pip install transformers torch

Выполните тест офлайн-загрузки:

from transformers import AutoTokenizer, AutoConfig

path = "/opt/model-transfer/models/Qwen3-8B"

config = AutoConfig.from_pretrained(
    path,
    local_files_only=True
)

tokenizer = AutoTokenizer.from_pretrained(
    path,
    local_files_only=True
)

print(config)
print(tokenizer)

local_files_only=True

Параметр local_files_only=True гарантирует, что библиотека не попытается скачать недостающие файлы из Интернета. Используйте его во всех офлайн-проверках.

Если на Internet-хосте есть GPU, выполните полный тест vLLM до переноса:

vllm serve \
    /opt/model-transfer/models/Qwen3-8B \
    --served-model-name qwen3-8b

Зафиксируйте версии окружения (vLLM, CUDA, NVIDIA driver, PyTorch, архитектура модели и GPU) — они понадобятся для environment manifest.

Если Internet Host не имеет GPU Это не проблема: можно проверить tokenizer, config и файлы модели офлайн. Полный тест `vLLM` выполните уже в закрытом контуре.
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(
    "/opt/model-transfer/models/Qwen3-8B",
    local_files_only=True
)

print("Tokenizer OK")

1.8. Особенности отдельных моделей

  • Квантованные модели. Если модель квантована (AWQ, GPTQ, GGUF, FP8, INT8), явно отразите это в manifest: Qwen3-8B и Qwen3-8B-AWQ — разные артефакты, нельзя считать их одной моделью.
  • Custom-код. Некоторые модели требуют дополнительных Python-файлов в снапшоте (modeling_xxx.py, configuration_xxx.py, tokenization_xxx.py). Эти файлы должны быть перенесены вместе со снапшотом.

trust_remote_code

Флаг --trust-remote-code означает выполнение кода, поставляемого вместе с моделью. В защищённом контуре включайте его только для заранее проверенных и утверждённых моделей.

  • Python-зависимости. Custom-код может требовать пакеты (einops, sentencepiece, protobuf, tiktoken и др.). В air-gapped среде они тоже должны быть доступны — при необходимости перенесите их в локальный PyPI/package repository.
  • Токенизатор. Проверьте наличие файлов токенизатора — в зависимости от модели это tokenizer.json, tokenizer.model, vocab.json, merges.txt, special_tokens_map.json, tokenizer_config.json:
find "$WORKDIR/models/Qwen3-8B" -maxdepth 1 -type f | grep -Ei 'token|vocab|merges|spiece'
  • Chat template. Для instruct/chat-моделей важен chat_template.jinja (или chat template внутри tokenizer_config.json). Без него модель может технически загрузиться, но поведение chat API будет отличаться от ожидаемого. Chat template — часть Model Release.

Шаг 2. Фиксация версии и подготовка transfer package

2.1. Получение commit SHA

Зафиксируйте commit SHA модели через Python:

from huggingface_hub import model_info

info = model_info(
    "Qwen/Qwen3-8B"
)

print(info.sha)

Или для конкретной revision:

from huggingface_hub import model_info

info = model_info(
    "Qwen/Qwen3-8B",
    revision="main"
)

print(info.sha)

Сохраните результат в manifests/model-revision.txt:

repository=Qwen/Qwen3-8B
revision=main
commit=<COMMIT-SHA>

2.2. Inventory и контрольные суммы

Создайте inventory всех файлов модели:

find \
    "$WORKDIR/models/Qwen3-8B" \
    -type f \
    -printf '%P\n' \
    | sort \
    > "$WORKDIR/manifests/model-files.txt"

Посчитайте SHA-256 каждого файла:

cd "$WORKDIR/models/Qwen3-8B"
find . \
    -type f \
    -exec sha256sum {} \; \
    > "$WORKDIR/checksums/model-SHA256SUMS"

Проверьте контрольные суммы:

cd "$WORKDIR/models/Qwen3-8B"
sha256sum -c "$WORKDIR/checksums/model-SHA256SUMS"

Все файлы должны вернуть OK.

Сохраните размер и количество файлов:

du -sh "$WORKDIR/models/Qwen3-8B" > "$WORKDIR/manifests/model-size.txt"
find "$WORKDIR/models/Qwen3-8B" -type f | wc -l > "$WORKDIR/manifests/model-file-count.txt"

2.3. Model manifest

Пример manifests/model.yaml:

model:
  name: Qwen3-8B
  repository: Qwen/Qwen3-8B
  revision: main
  commit: "<COMMIT-SHA>"

files:
  count: 42
  size_bytes: 123456789012

format:
  weights: safetensors

source:
  type: huggingface
  url: https://huggingface.co/Qwen/Qwen3-8B

target:
  storage: s3
  bucket: models
  prefix: qwen/Qwen3-8B/<COMMIT-SHA>/

2.4. Сборка transfer package

Рекомендуемая структура пакета:

Qwen3-8B-<COMMIT-SHA>/
├── README.md
├── manifest.yaml
├── model/
│   ├── config.json
│   ├── generation_config.json
│   ├── tokenizer.json
│   ├── tokenizer_config.json
│   ├── model-00001-of-00004.safetensors
│   └── ...
├── manifests/
│   ├── model-files.txt
│   ├── model-revision.txt
│   ├── model-size.txt
│   └── environment.txt
└── checksums/
    └── model-SHA256SUMS

Соберите пакет:

mkdir -p "$WORKDIR/package"
cp -r "$WORKDIR/models/Qwen3-8B" "$WORKDIR/package/model"
cp -r "$WORKDIR/manifests" "$WORKDIR/package/"
cp -r "$WORKDIR/checksums" "$WORKDIR/package/"

Положите в пакет README.txt с информацией о модели:

Model: Qwen3-8B
Hugging Face repository: Qwen/Qwen3-8B
Revision: main
Commit: <COMMIT-SHA>
Download date: 2026-08-31
Weight format: safetensors
Total size: XX GB
Files: XX
SHA256: checksums/model-SHA256SUMS

2.5. Создание архива и его SHA-256

tar \
    -czf \
    Qwen3-8B-<COMMIT-SHA>-airgap.tar.gz \
    -C "$WORKDIR/package" \
    model \
    manifests \
    checksums

gzip для больших моделей

Сжатие safetensors обычно даёт небольшой выигрыш — веса уже не являются хорошо сжимаемыми текстовыми данными. Для моделей на десятки-сотни гигабайт используйте обычный tar без сжатия:

tar \
    -cf \
    Qwen3-8B-<COMMIT-SHA>-airgap.tar \
    -C "$WORKDIR/package" \
    model \
    manifests \
    checksums

Можно переносить каталог и без архива, если транспорт это позволяет.

Создайте SHA-256 архива:

sha256sum \
    Qwen3-8B-<COMMIT-SHA>-airgap.tar \
    > \
    Qwen3-8B-<COMMIT-SHA>-airgap.tar.sha256

2.6. Перенос через USB

cp Qwen3-8B-<COMMIT-SHA>-airgap.tar /media/usb/
cp Qwen3-8B-<COMMIT-SHA>-airgap.tar.sha256 /media/usb/
sync
umount /media/usb

Шаг 3. Проверка и распаковка в закрытом контуре

3.1. Проверка SHA-256 архива

После подключения USB в закрытом контуре:

sha256sum -c Qwen3-8B-<COMMIT-SHA>-airgap.tar.sha256

Ожидаемый результат:

Qwen3-8B-<COMMIT-SHA>-airgap.tar: OK

НЕ ПРОДОЛЖАТЬ ИМПОРТ

Если checksum не совпадает — не продолжайте импорт. Повторите перенос или разберитесь с причиной повреждения до загрузки в S3.

3.2. Распаковка

mkdir -p /opt/model-import
tar -xf Qwen3-8B-<COMMIT-SHA>-airgap.tar -C /opt/model-import

Результат:

/opt/model-import/
├── model/
├── manifests/
└── checksums/

3.3. Повторная проверка checksum файлов

cd /opt/model-import/model
sha256sum -c ../checksums/model-SHA256SUMS

Все файлы должны пройти проверку.

Шаг 4. Загрузка модели в локальное S3

4.1. Подготовка AWS CLI и credentials

Пример параметров:

  • Endpoint: https://s3.example.local
  • Bucket: models

Проверьте наличие AWS CLI:

aws --version

Настройте credentials:

aws configure

Укажите:

AWS Access Key ID:
AWS Secret Access Key:
Default region:

Для S3-совместимого хранилища endpoint задаётся отдельно через --endpoint-url.

4.2. Проверка доступа к S3

aws s3 \
    --endpoint-url https://s3.example.local \
    ls

Проверьте bucket:

aws s3 \
    --endpoint-url https://s3.example.local \
    ls \
    s3://models/

4.3. Если используется внутренний CA

Если S3 использует сертификат внутреннего CA — не отключайте проверку TLS.

Не используйте --no-verify-ssl в production

Флаг --no-verify-ssl отключает проверку TLS. В production вместо него установите внутренний CA:

export AWS_CA_BUNDLE=/etc/pki/ca-trust/source/anchors/internal-ca.crt

(или соответствующий путь для конкретной ОС). После этого проверьте доступ:

aws s3 \
    --endpoint-url https://s3.example.local \
    ls

4.4. Выбор immutable prefix

Рекомендуемый префикс:

s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/

Не перезаписывайте существующую модель

Не загружайте новую версию поверх s3://models/qwen/Qwen3-8B/. Используйте immutable prefix <COMMIT-SHA> — это позволяет хранить несколько релизов модели (Model Release 1/2/3) и выполнять rollback.

Почему в S3 лучше использовать commit SHA Хранение только по имени модели (`s3://models/qwen/Qwen3-8B/`) не позволяет отличить версии. Префикс с commit SHA — неизменяемый артефакт. Для нескольких моделей структура выглядит так:
s3://models/
├── qwen/
│   ├── Qwen3-8B/
│   │   ├── <COMMIT-1>/
│   │   └── <COMMIT-2>/
│   │
│   └── Qwen3-32B/
│       └── <COMMIT-1>/
├── meta-llama/
│   └── Llama-3.3-70B-Instruct/
│       └── <COMMIT>/
└── mistralai/
    └── Mistral-Small/
        └── <COMMIT>/
При необходимости можно вести логический alias `current`:
qwen/Qwen3-8B/
├── abc123/
├── def456/
└── current/   # current → def456
`current` — логический alias на утверждённую версию, а не единственное хранилище модели.

4.5. Загрузка модели в S3

aws s3 sync \
    /opt/model-import/model/ \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --endpoint-url https://s3.example.local

AWS CLI рекурсивно загрузит весь каталог.

4.6. Проверка содержимого S3

aws s3 ls \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --recursive \
    --endpoint-url https://s3.example.local

Должны присутствовать config.json, tokenizer.json, tokenizer_config.json, *.safetensors и остальные файлы модели.

Проверьте, что количество файлов совпадает. Локально:

find /opt/model-import/model -type f | wc -l

В S3:

aws s3 ls \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --recursive \
    --endpoint-url https://s3.example.local \
    | wc -l

Количество должно совпадать.

Проверьте размер. Локально:

du -sb /opt/model-import/model

В S3:

aws s3 ls \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    --recursive \
    --summarize \
    --human-readable \
    --endpoint-url https://s3.example.local

Сравните общий размер.

4.7. Строгая проверка: повторная SHA-256 из S3

Количество файлов и размер — недостаточная проверка: содержимое могло измениться. Скачайте модель из S3 во временный каталог и сверьте SHA-256:

mkdir -p /tmp/model-verification
aws s3 sync \
    s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
    /tmp/model-verification/ \
    --endpoint-url https://s3.example.local
cd /tmp/model-verification
sha256sum -c /opt/model-import/checksums/model-SHA256SUMS

Если все файлы вернули OK — модель успешно перенесена в S3.

ETag ≠ MD5

Не используйте ETag == MD5 как универсальную проверку целостности: для multipart-загрузки ETag обычно не является простым MD5 всего файла. Источник истины — собственные SHA-256 из checksums/model-SHA256SUMS.

Шаг 5. Подготовка PVC и запуск vLLM

5.1. Рекомендуемая схема: initContainer + PVC

Для production Kubernetes модель загружается из S3 в PVC через initContainer:

Local S3
   │ aws s3 sync
initContainer
PVC /models
vLLM

S3 используется как master storage, PVC — как локальный cache. Это удобно для моделей размером 10–500 ГБ.

Не используйте emptyDir для больших моделей

При использовании emptyDir модель исчезает при пересоздании Pod — при каждом рестарте пришлось бы скачивать модель заново:

Pod restart
emptyDir lost
100 GB download
vLLM start

Используйте PVC как cache: S3 → PVC → vLLM.

Альтернатива: скачать модель на PVC заранее Можно один раз вручную скопировать модель из S3 на PVC (без initContainer). Этот вариант проще, но обновление модели требует ручных операций.

5.2. Credentials и права доступа

Не прописывайте AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY непосредственно в Deployment. Создайте Secret:

kubectl create secret generic s3-model-credentials \
    --from-literal=AWS_ACCESS_KEY_ID='...' \
    --from-literal=AWS_SECRET_ACCESS_KEY='...' \
    -n inference

Отдельный пользователь S3

Для vLLM/model downloader не используйте административный S3-аккаунт. Создайте отдельного пользователя vllm-model-reader с правами только s3:GetObject и s3:ListBucket на models/qwen/Qwen3-8B/*. Запретите PutObject и DeleteObject.

Модель — immutable-артефакт: после утверждения s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ vLLM имеет доступ только на чтение (READ, не WRITE). Это разделяет процессы загрузки (Model ingestion) и обслуживания (Model serving) модели.

5.3. Манифест Deployment

Простой манифест для проверки работоспособности:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen3-8b
  namespace: inference
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-qwen3-8b
  template:
    metadata:
      labels:
        app: vllm-qwen3-8b
    spec:
      initContainers:
        - name: model-download
          image: registry.example.local/tools/aws-cli:latest
          command:
            - /bin/sh
            - -c
            - |
              aws s3 sync \
                s3://models/qwen/Qwen3-8B/<COMMIT-SHA>/ \
                /models/Qwen3-8B \
                --endpoint-url https://s3.example.local
          envFrom:
            - secretRef:
                name: s3-model-credentials
          volumeMounts:
            - name: model
              mountPath: /models
      containers:
        - name: vllm
          image: registry.example.local/vllm/vllm-openai:<VERSION>
          command: ["vllm", "serve"]
          args:
            - /models/Qwen3-8B
            - --served-model-name
            - qwen3-8b
          env:
            - name: HF_HUB_OFFLINE
              value: "1"
          volumeMounts:
            - name: model
              mountPath: /models
      volumes:
        - name: model
          persistentVolumeClaim:
            claimName: qwen3-8b-model

Запрос GPU-ресурсов (resources.limits) задаётся в соответствии с установленным GPU-оператором — см. GPU-оператор Metax.

Не используйте latest в production

Образ registry.example.local/tools/aws-cli:latest может измениться в любой момент. В production используйте конкретную версию (registry.example.local/tools/aws-cli:<VERSION>), а лучше — digest (registry.example.local/tools/aws-cli@sha256:<DIGEST>).

Отличия от минимального варианта:

  1. Образы initContainer и vLLM зафиксированы по digest:
image: registry.example.local/tools/aws-cli@sha256:<DIGEST>
  1. Credentials берутся из Secret s3-model-credentials (см. шаг 5.2), у S3-пользователя только права на чтение.
  2. HF_HUB_OFFLINE=1 в контейнере vLLM (см. шаг 5.5).
  3. Сетевой доступ ограничен NetworkPolicy: разрешены только S3, DNS и внутренние сервисы, Интернет запрещён:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: vllm-egress
  namespace: inference
spec:
  podSelector:
    matchLabels:
      app: vllm-qwen3-8b
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: <S3_CIDR>   # адреса локального S3
      ports:
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector: {}  # DNS
      ports:
        - protocol: UDP
          port: 53

Подставьте реальные CIDR под вашу инфраструктуру.

5.4. Запуск vLLM

После того как модель появилась на PVC (/models/Qwen3-8B), vLLM запускается обычным образом:

vllm serve \
    /models/Qwen3-8B \
    --served-model-name qwen3-8b

В Kubernetes аргументы задаются в манифесте Deployment (см. шаг 5.3).

5.5. Офлайн-режим и сетевая изоляция

vLLM должен запускаться без доступа к huggingface.co, github.com, pypi.org, docker.io. Если модель полностью локальная, vllm serve /models/Qwen3-8B не должен обращаться к Hugging Face.

HF_HUB_OFFLINE=1

Переменная HF_HUB_OFFLINE=1 запрещает Hugging Face Hub client обращаться в Интернет за отсутствующими файлами. Задайте её в контейнере:

env:
  - name: HF_HUB_OFFLINE
    value: "1"

Это особенно полезно для тестирования air-gapped-развёртывания.

Не полагайтесь только на переменные окружения: ограничьте сетевой доступ namespace/Pod через NetworkPolicy (см. шаг 5.3, промышленный вариант).

Промышленный вариант: рекомендуемая архитектура

Для production рекомендуемая архитектура:

                 INTERNET
          ┌──────────────────┐
          │ Model Collector  │  huggingface_hub
          └────────┬─────────┘
          ┌──────────────────┐
          │ Model Validator  │  commit SHA, SHA256, inventory, tokenizer, config
          └────────┬─────────┘
          ┌──────────────────┐
          │ Transfer Package │
          └────────┬─────────┘
                USB
          ┌──────────────────┐
          │ Airgap Importer  │
          └────────┬─────────┘
          ┌──────────────────┐
          │ Local S3         │  model releases
          └────────┬─────────┘
            read-only access
          ┌──────────────────┐
          │ Model Downloader │  initContainer
          └────────┬─────────┘
          ┌──────────────────┐
          │ PersistentVolume │
          └────────┬─────────┘
          ┌──────────────────┐
          │ vLLM (GPU)       │
          └────────┬─────────┘
          OpenAI-compatible API

Главная идея: Hugging Face — внешний источник модели, S3 — внутренний master storage, PVC — локальный cache для inference, а vLLM не должен знать о существовании Интернета.

Единица поставки — Model Release, а не отдельный файл весов:

Qwen3-8B Model Release
├── Hugging Face repository
├── Commit SHA
├── Model configuration
├── Tokenizer
├── Chat template
├── Weight files
├── Weight index
├── Generation configuration
├── Custom code
├── Python dependencies
├── File inventory
├── SHA256
└── Validation information

После импорта модель живёт в S3 с immutable prefix:

s3://models/
└── qwen/
    └── Qwen3-8B/
        └── <COMMIT-SHA>/
            ├── config.json
            ├── tokenizer.json
            ├── tokenizer_config.json
            ├── model-00001-of-00004.safetensors
            ├── ...
            └── model.safetensors.index.json

Сохраните также environment manifest — сведения об окружении, на котором модель проверялась:

Python: 3.x
huggingface_hub: x.y.z
transformers: x.y.z
torch: x.y.z
vLLM: x.y.z
CUDA: 12.x
NVIDIA Driver: xxx.xx

Это позволит через год ответить на вопрос: «На каком окружении эта модель была проверена?»

Проверка результата

После загрузки модели в S3 и запуска Deployment выполните итоговые проверки.

  1. Целостность архива и файлов — шаги 3.1 и 3.3: sha256sum -c для архива и для файлов модели.
  2. Повторная SHA-256 из S3 — шаг 4.7: скачайте модель из S3 и сверьте контрольные суммы.
  3. Запуск vLLM:
vllm serve /models/Qwen3-8B --served-model-name qwen3-8b
  1. Проверка API vLLM. Список моделей:
curl http://vllm.example.local/v1/models

Ожидаемый ответ:

{
  "data": [
    {
      "id": "qwen3-8b"
    }
  ]
}

Тестовый запрос:

curl http://vllm.example.local/v1/chat/completions \
    -H 'Content-Type: application/json' \
    -d '{
      "model": "qwen3-8b",
      "messages": [
        {
          "role": "user",
          "content": "Hello"
        }
      ]
    }'
  1. Тест air-gap: запустите vLLM, убедитесь, что модель загружается, полностью запретите Интернет, перезапустите Pod и убедитесь, что vLLM снова успешно стартует. Если это работает — deployment действительно независим от Интернета.
Internet OFF
Pod restart
S3 local
Model
vLLM READY

Частые ошибки

Симптом Вероятная причина Решение
vLLM не загружает модель, ошибка на старте Перенесены только веса, отсутствует tokenizer или config.json Скачивайте снапшот целиком (шаг 1.6), проверяйте обязательные файлы (шаг 1.7)
vLLM обращается в Интернет при старте Не задан HF_HUB_OFFLINE, нет NetworkPolicy Задайте HF_HUB_OFFLINE=1 и ограничьте сеть (шаг 5.5)
В S3 новая версия модели перезаписала старую Нарушение immutable: загрузка без префикса <COMMIT-SHA> Всегда используйте immutable prefix (шаг 4.4)
Проверка целостности по ETag даёт неверный результат Для multipart-upload ETag ≠ MD5 всего файла Используйте собственные SHA-256 (шаг 4.7)
initContainer не скачивает модель Неверные credentials, endpoint или права пользователя S3 Проверьте Secret, --endpoint-url, права s3:GetObject/s3:ListBucket (шаг 5.2)
Модель скачивается заново при каждом рестарте Pod Используется emptyDir вместо PVC Используйте PVC как cache (шаг 5.1)
Не воспроизводимый образ initContainer Используется latest-тег Зафиксируйте версию или digest образа (шаг 5.3)
Модель с custom code не запускается в закрытом контуре Не перенесены Python-зависимости Перенесите зависимости в локальный PyPI (шаг 1.8)

Приложения

Контрольный чек-лист перед переносом

  • Hugging Face repository зафиксирован
  • Revision зафиксирован
  • Commit SHA получен
  • Snapshot скачан целиком
  • config.json присутствует
  • tokenizer присутствует
  • все weight-файлы присутствуют
  • index.json присутствует, если необходим
  • generation_config проверен
  • chat template проверен
  • custom code проверен
  • Python dependencies определены
  • модель локально загружается
  • file inventory создан
  • SHA256 создан
  • transfer package создан
  • SHA256 package создан

Контрольный чек-лист после переноса

  • SHA256 transfer package совпадает
  • SHA256 файлов модели совпадает
  • количество файлов совпадает
  • размер модели совпадает
  • manifest проверен
  • S3 bucket доступен
  • модель загружена в правильный prefix
  • количество S3-объектов совпадает
  • размер S3-объектов совпадает
  • повторная SHA256-проверка прошла
  • S3 prefix сделан immutable
  • credentials имеют только необходимые права
  • PVC создан
  • model downloader работает
  • vLLM стартует
  • vLLM не обращается в Интернет
  • API /v1/models работает
  • inference request работает