Руководство по сборке локального AI-стека. Часть 7. Финал. Офлайн-развёртывание

—

от автора

Руководство по сборке локального AI-стека

Офлайн-развёртывание: локальная репа и перенос

Для работы стека нужен интернет только на этапе сборки. После того как все Python-пакеты будут скачаны в offline_packages/, проект можно перенести на машину без сети и развернуть из локальной репы. Эта часть — про то, как собрать «снимок» зависимостей и восстановить стек на чистой Debian 12.

Зачем это нужно

Стандартный pip install open-webui требует интернета и разрешает зависимости заново — сегодня поставит одну версию, завтра другую. Для офлайн-машины это неприемлемо: pip либо упадёт с «no internet», либо (если кэш частичный) поставит несовместимые версии.

Локальная репа (offline_packages/) решает обе проблемы:

  • Фиксация версий — requirements.lock содержит точный список пакетов (transformers==5.5.4, torch==2.11.0+cpu и т.д.), а не «свободные» диапазоны.

  • Работа без сети — все .whl лежат локально, pip install --no-index --find-links=./offline_packages не обращается к PyPI.

  • Воспроизводимость — на любой машине с теми же ОС/Python получится идентичное окружение.

Что попадает в репу: только Python-пакеты. Модели, бинарники llama.cpp/stable-diffusion.cpp, скрипты, HTML — переносятся отдельно (через tar всей папки проекта). offline_packages/ — это дополнение, а не замена «папки проекта целиком».

Как работает make_offline_packages.sh

Скрипт делает снимок текущего venv и скачивает все пакеты в offline_packages/:

  1. pip freeze > requirements.lock — фиксирует точные версии всего, что стоит в venv.

  2. pip download -r requirements.lock — скачивает wheels для каждого пакета.

  3. --only-binary=:all: — гарантирует, что не скачаются .tar.gz (sdist). Если для какого-то пакета нет wheel — скрипт сообщит об этом сейчас, а не на офлайн-машине.

  4. --extra-index-url https://download.pytorch.org/whl/cpu — ключевой момент: torch берётся с CPU-индекса (версия +cpu), а не с PyPI (CUDA-версия с nvidia-*).

  5. Fallback на sdist — если для какого-то пакета wheel нет, скрипт скачает .tar.gz и предупредит, что на целевой машине понадобится gcc, python3-dev, а для tokenizers — rustc/cargo.

  6. Проверка — сравнивает число файлов с числом пакетов в requirements.lock. Если меньше — предупреждает.

Почему --extra-index-url, а не --index-url. С --index-url pip ищет только на PyTorch-индексе и не находит open-webui (его там нет). С --extra-index-url — ищет везде: open-webui на PyPI, torch на PyTorch-индексе. При этом torch==2.11.0+cpu (суффикс +cpu) однозначно указывает на CPU-версию, и CUDA не подтянется.

Про «no wheel» fallback. В 99% случаев для стека open-webui + torch на Debian 12 x86_64 все пакеты имеют готовые wheels. Fallback нужен как страховка: лучше узнать о sdist сейчас (когда есть интернет и можно доставить python3-dev), чем на офлайн-машине получить error: command 'gcc' failed.

Полный код make_offline_packages.sh

#!/usr/bin/env bash# Скачивает все зависимости из текущего venv в offline_packages/.# Требуется интернет. После этого папку можно перенести на офлайн-машину.## Usage: ./make_offline_packages.sh# Env:#   VENV_DIR=./venv          путь к venv (по умолчанию)#   OUT_DIR=./offline_packages  куда качать#   PIP_EXTRA_INDEX_URL      для CPU-torch (по умолчанию PyTorch CPU)set -euo pipefailSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"VENV_DIR="${VENV_DIR:-$SCRIPT_DIR/venv}"OUT_DIR="${OUT_DIR:-$SCRIPT_DIR/offline_packages}"REQ_FILE="$SCRIPT_DIR/requirements.lock"TORCH_INDEX="${PIP_EXTRA_INDEX_URL:-https://download.pytorch.org/whl/cpu}"RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }[ -d "$VENV_DIR" ] || error "venv не найден: $VENV_DIR"PIP="$VENV_DIR/bin/pip"[ -x "$PIP" ] || error "pip не найден: $PIP"mkdir -p "$OUT_DIR"# --- 1. Фиксируем список пакетов ---info "Генерирую $REQ_FILE из текущего окружения...""$PIP" freeze > "$REQ_FILE"PKG_COUNT=$(wc -l < "$REQ_FILE")info "Пакетов в списке: $PKG_COUNT"# --- 2. Скачиваем всё, кроме torch (сначала только wheels) ---info "Скачиваю пакеты в $OUT_DIR (wheels)..."#grep -v -i '^torch' "$REQ_FILE" > "$REQ_FILE.tmp" || trueWHEEL_FAILED=()if [ -s "$REQ_FILE" ]; then    if ! "$PIP" download \            --dest "$OUT_DIR" \            --only-binary=:all: \            --no-cache-dir \            --extra-index-url "$TORCH_INDEX" \            -r "$REQ_FILE"; then        warn "Не все пакеты имеют wheel. Определяю виновников..."        # Пытаемся по одному, чтобы найти те, у кого нет wheel        while IFS= read -r pkg; do            [ -z "$pkg" ] && continue            if ! "$PIP" download \                    --dest "$OUT_DIR" \                    --only-binary=:all: \                    --no-cache-dir \                    "$pkg" >/dev/null 2>&1; then                WHEEL_FAILED+=("$pkg")            fi        done < "$REQ_FILE"    fielse    warn "Список без torch пуст, пропускаю"firm -f "$REQ_FILE.tmp"# --- 2b. Fallback: sdist для пакетов без wheel ---if [ ${#WHEEL_FAILED[@]} -gt 0 ]; then    warn "Пакеты без wheel: ${WHEEL_FAILED[*]}"    warn "Скачиваю их как sdist (сборка потребуется на целевой машине):"    warn "  - нужен gcc (build-essential)"    warn "  - нужен python3-dev (заголовки Python)"    warn "  - для tokenizers нужен rustc/cargo"    for pkg in "${WHEEL_FAILED[@]}"; do        "$PIP" download --dest "$OUT_DIR" --no-cache-dir "$pkg" || \            warn "  не удалось скачать $pkg"    donefi# --- 4. Проверка результата ---WHL_COUNT=$(find "$OUT_DIR" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' \) | wc -l)SIZE=$(du -sh "$OUT_DIR" | cut -f1)info "Скачано файлов: $WHL_COUNT"info "Размер $OUT_DIR: $SIZE"if [ "$WHL_COUNT" -lt "$PKG_COUNT" ]; then    warn "Файлов меньше, чем пакетов ($WHL_COUNT < $PKG_COUNT)."    warn "Возможно, часть пакетов не имеет wheel под вашу платформу."    warn "Проверьте: pip download может ругаться на отсутствие wheel."fi# --- 5. Инструкция по офлайн-установке ---echo ""info "Для установки на офлайн-машине:"echo "    python3 -m venv venv"echo "    ./venv/bin/pip install --no-index --find-links=$OUT_DIR -r $REQ_FILE"echo ""info "Готово."

Ключевые моменты

requirements.lock — фиксация версий

pip freeze > requirements.lock

Это снимок текущего venv. Если завтра вы обновите open-webui и снова запустите make_offline_packages.sh, requirements.lock перезапишется новыми версиями, а offline_packages/ — новыми wheels. Старые файлы останутся (не удаляются), но pip install --no-index возьмёт те, что указаны в requirements.lock.

Для воспроизводимости храните requirements.lock вместе с проектом (он в git/архиве) и не запускайте make_offline_packages.sh без необходимости.

Размер и состав

Для текущего стека (open-webui + torch + llama.cpp зависимости):

  • ~244 файла, ~730 МБ.

  • torch-2.11.0+cpu — ~200 МБ (самый большой).

  • nvidia-* отсутствуют — это главный индикатор, что CUDA не подтянулась.

Если размер больше 2 ГБ — почти наверняка попала CUDA-версия torch. Проверка:

ls offline_packages/ | grep -i nvidia    # должно быть пустоls offline_packages/torch-*.whl          # должно быть +cpu в имени

Проверка перед переносом

1. Тестовая установка в чистый venv:

rm -rf /tmp/test-offlinepython3 -m venv /tmp/test-offline/tmp/test-offline/bin/pip install \    --no-index \    --find-links=./offline_packages \    -r requirements.lock/tmp/test-offline/bin/python -c "import torch; print(torch.__version__)"rm -rf /tmp/test-offline

Должно вывести 2.11.0+cpu. Если упадёт с No matching distribution — какого-то пакета не хватает, pip download его не включил (редкий случай, но бывает для пакетов с platform-specific wheels).

2. pip check в тестовом окружении:

/tmp/test-offline/bin/pip check

Должно быть пусто. Если есть конфликты — requirements.lock содержит несовместимые версии (например, fla требует transformers>=5.6.2, а в lock — 5.5.4). Решение: убрать конфликтующий пакет из venv, pip freeze > requirements.lock заново, пересобрать репу.

Упаковка и перенос

1. Собрать репу (на машине с интернетом):

./make_offline_packages.sh

Появится offline_packages/ (~730 МБ) и requirements.lock.

2. Упаковать весь проект в архив:

cd ..tar czf llama_server_offline.tar.gz \    --exclude='llama_server/venv' \    --exclude='llama_server/.secrets' \    --exclude='llama_server/open-webui-data' \    llama_server/

Что исключать и почему:

Исключение

Причина

venv/

Пересоздаётся на целевой машине из offline_packages/. Переносить 2–3 ГБ venv бессмысленно — он привязан к путям и версии Python.

.secrets

Содержит ключи. Если переносите — сессии WebUI сохранятся, но ключи «уедут» на новую машину. Безопаснее сгенерировать заново через init_secrets.sh.

open-webui-data/

Пользователи, история чатов. Если хотите «чистое развёртывание» — не переносите, пусть создастся с нуля. Если хотите сохранить историю — переносите, но тогда и .secrets тоже (иначе сессии не расшифруются).

Что НЕ исключать:

  • offline_packages/ — это и есть локальная репа.

  • requirements.lock — фиксация версий.

  • models/ — модели (тяжёлые, но нужны).

  • llama.cpp/ и stable-diffusion.cpp/ — исходники + build/bin/ (бинарники можно перенести, но лучше пересобрать на целевой машине, если архитектура CPU отличается).

  • web/, все скрипты, README.md.

3. Перенести архив на целевую машину (флешка, scp, rsync — что удобно).

4. Развернуть на целевой машине (Debian 12 x86_64, без интернета):

tar xzf llama_server_offline.tar.gzcd llama_server# Системные пакеты (нужен локальный apt-репозиторий или они уже стоят)sudo apt install -y cmake build-essential libopenblas-dev \    python3 python3-venv python3-pip git curl wget jq# Python-окружение из локальной репыpython3 -m venv venv./venv/bin/pip install --no-index --find-links=./offline_packages -r requirements.lock# Секреты (генерируются заново)./init_secrets.sh# Запуск./run_webui.sh

По шагам:

  • python3 -m venv venv — быстро, без сети.

  • pip install --no-index — читает только offline_packages/, не обращается к PyPI. Если видите Downloading — что-то не так (например, --no-index забыт).

  • ./init_secrets.sh — создаст .secrets с новыми ключами.

  • ./run_webui.sh — при первом запуске может спросить статический IP, поставить llama.cpp, если бинарников нет.

Подводные камни

  1. Разная версия Python. offline_packages/ содержит wheels с тегом cp311 (Python 3.11, стандарт для Debian 12). Если на целевой машине Python 3.12 — часть wheels не подойдёт. Решение: та же ОС и версия Python (Debian 12 -> 3.11).

  2. Разная архитектура. offline_packages/ содержит wheels для x86_64. На ARM (Raspberry Pi) они не подойдут. Решение: собирать репу на целевой архитектуре.

  3. systemd-run в LXC/контейнере. run_llama_mmap.sh использует systemd-run --user --scope для лимита памяти. В LXC/Docker без user session это может не работать — скрипт откатится на запуск «без лимита», но модель может съесть всю RAM. Решение: либо запускать на хосте (не в контейнере), либо перенести лимиты в systemd юниты (мы решили не делать).

  4. systemd-run требует logind. На headless-сервере без logind (или в SSH-сессии без pam_systemd) systemd-run --user упадёт. Решение: sudo loginctl enable-linger $USER, чтобы user-сессия жила после логаута, или запускать llama-server без лимита вручную.

  5. open-webui-data/ и .secrets. Если переносите оба — сессии WebUI продолжат работать на новой машине (ключ тот же). Если перенести только open-webui-data/ без .secrets — WebUI сгенерирует новый WEBUI_SECRET_KEY, и все пользователи разлогинятся. Если переносить только .secrets без open-webui-data/ — нет пользователей, но ключи валидны для новых.

Итог

Что даёт:

  • Полная воспроизводимость: те же версии пакетов на любой машине.

  • Никаких обращений к PyPI во время установки.

  • Нет риска, что завтра open-webui обновится и сломает совместимость.

  • Размер архива: ~730 МБ (репа) + модели (несколько ГБ, у меня около 70 Гб) + исходники.

Что НЕ решает:

  • Системные пакеты (apt) — их нужно доставить отдельно (локальный apt-репозиторий или apt-offline). Для Debian 12 базовый набор (cmake, python3-venv, git) обычно уже стоит, но на чистой системе придётся доставить.

  • Бинарники llama.cpp/stable-diffusion.cpp — либо переносятся в архиве, либо пересобираются на целевой машине (если cmake/gcc есть).

ссылка на оригинал статьи https://habr.com/ru/articles/1099552/