Руководство по сборке локального AI‑стека
Ссылки на все части:
Часть 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/:
pip freeze > requirements.lock— фиксирует точные версии всего, что стоит вvenv.pip download -r requirements.lock— скачивает wheels для каждого пакета.--only-binary=:all:— гарантирует, что не скачаются.tar.gz(sdist). Если для какого‑то пакета нет wheel — скрипт сообщит об этом сейчас, а не на офлайн‑машине.--extra-index-urlhttps://download.pytorch.org/whl/cpu— ключевой момент:torchберётся с CPU‑индекса (версия+cpu), а не с PyPI (CUDA‑версия сnvidia-*).Fallback на sdist — если для какого‑то пакета wheel нет, скрипт скачает
.tar.gzи предупредит, что на целевой машине понадобитсяgcc,python3-dev, а дляtokenizers—rustc/cargo.Проверка — сравнивает число файлов с числом пакетов в
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/
Что исключать и почему:
Исключение |
Причина |
|---|---|
|
Пересоздаётся на целевой машине из |
|
Содержит ключи. Если переносите — сессии WebUI сохранятся, но ключи «уедут» на новую машину. Безопаснее сгенерировать заново через |
|
Пользователи, история чатов. Если хотите «чистое развёртывание» — не переносите, пусть создастся с нуля. Если хотите сохранить историю — переносите, но тогда и |
Что НЕ исключать:
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, если бинарников нет.
Подводные камни
Разная версия Python.
offline_packages/содержит wheels с тегомcp311(Python 3.11, стандарт для Debian 12). Если на целевой машине Python 3.12 — часть wheels не подойдёт. Решение: та же ОС и версия Python (Debian 12 → 3.11).Разная архитектура.
offline_packages/содержит wheels дляx86_64. На ARM (Raspberry Pi) они не подойдут. Решение: собирать репу на целевой архитектуре.systemd-runв LXC/контейнере.run_llama_mmap.shиспользуетsystemd-run --user --scopeдля лимита памяти. В LXC/Docker без user session это может не работать — скрипт откатится на запуск «без лимита», но модель может съесть всю RAM. Решение: либо запускать на хосте (не в контейнере), либо перенести лимиты вsystemdюниты (мы решили не делать).systemd-runтребуетlogind. На headless‑сервере безlogind(или в SSH‑сессии безpam_systemd)systemd-run --userупадёт. Решение:sudo loginctl enable-linger $USER, чтобы user‑сессия жила после логаута, или запускатьllama-serverбез лимита вручную.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есть).
redfox0
Почему Debian 12, а не 13?
AntonovPsy Автор
Просто им пользуюсь (пока).
redfox0
Debian 12 EOL, хотя ещё LTS до 2028-06-30
AntonovPsy Автор
Я минималист (то есть, немного ленивый) пока работает не трогаю )