The Jerusalem PostEgypt approves military support for Saudi Arabia after Houthi attacks escalatePunchPolice arrest two over deaths of minors in BauchiInquirerUndas 2026: 4,790 police personnel to be deployed in SoccsksargenESPNThunder star Williams stretchered off after suffering significant leg injuryInquirer EntertainmentAndrea Brillantes bares diving mishap while shooting ‘Laya’ESPN DeportesReal Madrid, a 3 puntos del Barça: así va la tabla de LaLigaCollider'Deadpool's Director Is Presiding Over the Biggest Short Film Contest Ever [Exclusive]ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioAnime News NetworkRomelia War Chronicle ‒ Episode 4BBC NewsHow a movement of students brought down Serbia's VučićMintBrazil election 2026 results: What’s the net worth of Lula Da Silva as he gets re-elected as president for fourth term?Digital SpyUniversity Challenge contestants hit out at "absurd" BBC decision to edit out pride badges
The Daily Newsstand · Free, Always
Monday, October 26, 2026

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

Translate

Ссылки на все части:
Часть 1. Общий план
Часть 2. Запуск llama-cpp
Часть 3. Наполнение моделями
Часть 4. Запуск сервера генерации изображений
Часть 5. Open WebUI поверх llama.cpp
Часть 6. Конвертация моделей Hugging Face в GGUF
Часть 7. Финал. Офлайн-развёртывание

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

Для работы стека нужен интернет только на этапе сборки. После того как все 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 pipefail

SCRIPT_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" || true

WHEEL_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"
    fi
else
    warn "Список без torch пуст, пропускаю"
fi
rm -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"
    done
fi


# --- 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-offline
python3 -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.gz
cd 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 есть).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.