Руководство по сборке локального AI-стека
Часть 4. Запуск сервера генерации изображений
На этом шаге мы соберём stable-diffusion.cpp, разберёмся, почему для картинок нужен не один файл, а три, разложим компоненты по папкам и запустим sd-server на порту 8888 с собственным веб-интерфейсом. После этого у вас будет генератор изображений, доступный из LAN.
Что нужно понять до начала
stable-diffusion.cpp (sd.cpp) — это «младший брат» llama.cpp для диффузионных моделей. Он построен на том же ggml и умеет:
-
Работать на CPU — с теми же
mmap,--offload-to-cpu,systemd-run. -
Загружать модели в трёх форматах:
.gguf(квантованные),.safetensors(полная точность),.ckpt/.pt(старый PyTorch). -
Использовать разные архитектуры: SD 1.5, SDXL, FLUX, Qwen-Image, Z-Image, Mage-Flow, Wan и другие.
Ключевое отличие от LLM: для генерации изображений почти никогда не достаточно одного файла. Модель состоит из трёх компонентов, и все три нужно явно указать при запуске.
Три компонента диффузионной модели
|
Компонент |
Что делает |
Флаг |
Размер (типично) |
|---|---|---|---|
|
Diffusion model (denoiser) |
Основная модель: «проявляет» изображение из шума |
|
2–12 ГБ |
|
VAE |
Декодирует латентное представление в реальные пиксели |
|
150–350 МБ |
|
Text encoder (LLM) |
Превращает промпт в вектор, понятный модели |
|
1.5–7 ГБ |
Почему так. Диффузионная модель работает не с пикселями, а с латентами — сжатым представлением изображения (например, 64×64×16 для картинки 512×512). VAE переводит латенты в пиксели (и обратно, если нужен img2img). Текстовый энкодер — это отдельная LLM (Qwen, T5, UMT5, Qwen3-VL), которая «понимает» промпт и передаёт его модели.
Что это значит на практике:
-
Нельзя запустить только
.gguf-модель — без VAE и LLMsd.cppупадёт с ошибкой «VAE not loaded» или «text encoder not loaded». -
VAE и LLM можно переиспользовать между моделями одной архитектуры. Например,
z_image_turboиFLUXиспользуют один и тот же VAE (ae.safetensorsот FLUX). АQwen3-4B-Instructподходит для Z-Image, но не для Mage-Flow (там нуженQwen3VL-4B). -
Размер на диске = модель + VAE + LLM. Для Z-Image: ~3.8 ГБ (модель) + ~330 МБ (VAE) + ~2.5 ГБ (Qwen3-4B) ≈ 6.6 ГБ.
Структура папок
models/├── Qwen3-4B-Instruct-2507-Q4_K_M.gguf <- LLM для Z-Image (общая с чатом)├── Qwen3VL-4B-Instruct-Q4_K_M/ <- LLM для Mage-Flow (vision-модель)│ └── Qwen3VL-4B-Instruct-Q4_K_M.gguf│└── media/ <- diffusion-модели ├── z_image_turbo-Q4_K.gguf <- diffusion ├── mage_flow_turbo_bf16.safetensors <- diffusion (полная точность) └── vae/ ├── Z-Image/ │ └── ae.safetensors <- VAE для Z-Image (от FLUX) └── mage/ └── mage_flow_vae_bf16.safetensors <- VAE для Mage-Flow
Правила раскладки:
-
Diffusion-модели — в
models/media/(плоско, скрипт находит по расширению). -
VAE — в
models/media/vae/<модель>/, чтобы не путаться. Скрипт находит по явному пути вMODEL_MAP. -
Text encoder (LLM) — в
models/(общая папка с чат-моделями). Один и тот же файл используется и для чата, и для генерации картинок — экономия места. -
Tokenizer (только для некоторых моделей, например MOSS) — можно положить рядом с VAE и указать через
--tokenizerвEXTRA_FLAGS.
Подсказки что и где скачать есть в репозитории сервера: https://github.com/leejet/stable-diffusion.cpp — клик на название модели и откроется справка со ссылками.
Модели: что оставлено и почему
В процессе тестирования были опробованы пять диффузионных моделей. Осталось две — остальные удалены по объективным причинам.
Оставлены
1. z_image_turbo-Q4_K.gguf (Z-Image Turbo, ~3.8 ГБ)
-
Скорость: ~11 минут на картинку 576×1024.
-
Сильные стороны: отличная композиция, точная передача цвета/одежды, хорошее разделение планов. Рабочая лошадка для быстрых итераций.
-
Слабые стороны: не интегрирует источники света (голубые камни не светятся), guohua-стиль «уваливается» в гуашь.
-
Когда использовать: 3D-сцены.
2. mage_flow_turbo_bf16.safetensors (Mage-Flow Turbo, ~8.2 ГБ)
-
Скорость: ~7 минут на картинку (4 шага, но модель тяжелее).
-
Сильные стороны: лучший epic-жанр (дракон, кристаллы, свет — единственная модель, которая справилась), канон в anime. Хорошо держит композицию.
-
Слабые стороны: guohua — «коллаж», 3D — теряет средний план.
-
Когда использовать: повседневная генерация, финальные epic-сцены, anime-постеры.
Удалены (и почему)
3. qwen-image-2.1-turbo-Q4_K_M.gguf (~6 ГБ)
-
Тесты: anime — «паспортный анфас» (скучная композиция), 3D — лучший свет, но потерян средний план, epic — дракон «как трафарет», guohua — OOM при лимите 6 ГБ (VAE-декод ест ~5 ГБ).
-
Скорость: ~15-20 минут на картинку (в 3 раза медленнее Z-Image).
-
Почему удалена: медленно, слабая композиция, OOM в половине случаев. Единственный плюс — свет в 3D-сценах, но это не оправдывает 15 минут ожидания.
4. flux3-image-klein-Q4_0.gguf (~4.5 ГБ)
-
Тесты: anime — лучший «постер» (динамика, эмоции), guohua — канонический (направление штрихов, туман, прозрачность), 3D — артефакты (балка слилась со стулом), epic — «плавающие острова» вместо «летающих».
-
Скорость: ~33 минуты на картинку (в 3 раза медленнее Z-Image).
-
Почему удалена: 33 минуты — это не «чуть медленнее», а другой режим работы. Для 10 итераций промпта FLUX съест 5.5 часов, Z-Image — 50 минут. Качество в guohua/aниме выше, но не настолько, чтобы оправдать ожидание.
5. MOSS-Image-Base (~22 ГБ, не тестировали)
-
Почему удалена: 20+ шагов на модели 22 ГБ — генерация займёт дольше часа на CPU. Даже не запускали.
Итог: оставлены две модели, которые покрывают 90% основных задач и работают довольно быстро и сравнительно качественно (Mage-Flow, Z-Image). Если понадобится guohua — можно вернуть FLUX, но запускать его «на ночь» или под конкретный заказ.
Ключевые опции sd-server / sd-cli
—diffusion-model, —vae, —llm — три обязательных флага
--diffusion-model models/media/z_image_turbo-Q4_K.gguf--vae models/media/vae/Z-Image/ae.safetensors--llm models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf
Без любого из них — ошибка загрузки. Проверить, что все три на месте, можно командой:
ls -lh models/media/*.gguf models/media/vae/*/*.safetensors models/Qwen3-4B*.gguf
—use-mmap — экономия RAM
--use-mmap
Аналог --load-mode mmap в llama.cpp: модель не грузится в RAM целиком, страницы подгружаются по требованию. Важно: в отличие от LLM, где mmap даёт большой выигрыш (модель 20 ГБ на 10 ГБ RAM), для diffusion-моделей VAE-декод всё равно требует много анонимной памяти (~5 ГБ на пике). То есть mmap помогает с загрузкой весов, но не спасает от OOM при высоком разрешении.
Почему VAE ест память. Декодирование латентов в пиксели — это одна большая свёртка, которая создаёт промежуточные тензоры размером с итоговое изображение, умноженное на число каналов. Для 1024×1024 это ~50 МБ на канал × 128 каналов × несколько слоёв = гигабайты. Отсюда и пик ~5 ГБ.
—offload-to-cpu — для гибридных систем
--offload-to-cpu
В sd.cpp это аналог «выгрузить всё на CPU» для систем, где есть GPU, но он слабый. Для чисто CPU-инференса флаг не нужен (всё и так на CPU), но и не вредит. Скрипт добавляет его автоматически, если находит в --help.
—steps и —cfg-scale — параметры сэмплинга
--steps 8 --cfg-scale 1.0 # для turbo-моделей--steps 20 --cfg-scale 7.0 # для base-моделей
Ключевое правило: параметры зависят от модели, а не от вашего желания.
|
Модель |
|
|
Почему |
|---|---|---|---|
|
Z-Image Turbo |
8 |
1.0 |
Дистиллирована под 8 шагов, CFG «запечён» в веса |
|
Mage-Flow Turbo |
4 |
1.0 |
Дистиллирована под 4 шага |
|
Z-Image (base) |
28–50 |
5.0–7.0 |
Полная модель, требует много шагов и CFG |
|
FLUX.3 Klein |
16 |
1.0 |
Distilled |
|
Qwen-Image Turbo |
8 (через |
1.0 |
Специальное расписание сигм |
Подсказки по параметрам можно подсмотреть в примерах, которые приведены в репозитории сервера: https://github.com/leejet/stable-diffusion.cpp — кликните нужную модель и смотрите параметры в примерах.
Что будет, если перепутать. Если дать turbo-модели 30 шагов — качество не улучшится, а может ухудшиться (траектория денойзинга рассчитана на крупные шаги). Если дать base-модели 8 шагов и CFG 1.0 — получите размытый кадр (реальный случай с Z-Image base: «намек на изображение, жёсткий песок»). Параметры хранятся в MODEL_MAP скрипта, менять их «на лету» в UI — плохая идея.
—sampling-method — алгоритм сэмплинга
--sampling-method euler # по умолчанию, стандарт--sampling-method heun # чуть чище, но медленнее--sampling-method euler_a # больше вариативности
Для 95% случаев euler — правильный выбор. Остальные — для экспериментов.
—sigmas — для Qwen-Image и подобных
--sigmas qwen-image-2.1-turbo # пресет из 8 значений--sigmas 1.0,0.978,0.954,...,0.0 # вручную
Некоторые модели требуют фиксированного расписания сигм вместо --steps. Количество сигм = количество шагов. Если указать --sigmas и --steps одновременно — движок использует сигмы и проигнорирует шаги. Пресет qwen-image-2.1-turbo — это просто строковый псевдоним, он подставляется в список чисел (см. docs/ sd.cpp).
-s -1 — случайный seed
-s -1
-1 означает «новый seed на каждый запрос» — каждый раз новая картинка. Если нужна воспроизводимость — задайте конкретное число (-s 42).
Веб-интерфейс web/index.html
UI для sd-server — это собственная статическая страница (не родной UI sd.cpp), которую мы сделали в Material Dark стиле. Она:
-
Автоматически определяет имя модели через
GET /sdapi/v1/options(полеsd_model_checkpoint). -
Даёт поле промпта, выбор размера (группировка по соотношениям: 1:1, 16:9, 9:16, 4:3, 3:2, до 2048px) и кнопку «Сгенерировать».
-
Показывает статус («Работаю… (N шагов)» -> «Готово» / «Ошибка»).
-
Выводит картинку с кнопкой «Скачать PNG».
-
Адаптивна — на телефоне всё в один столбец.
Чего в UI нет (и почему):
-
Выбора модели —
sd-serverкэширует одну модель, смена требует перезапуска. Имя показывается, но не переключается. -
Настройки шагов/CFG — они заданы в
MODEL_MAPпод конкретную модель. Если дать пользователю крутить шаги, он неизбежно сломает turbo-модель. -
Прогресс-бара в реальном времени — эндпоинт
/v1/images/generationsсинхронный, сервер отвечает только когда готово. Индикатор «Работаю…» просто висит. Для прогресса нужен асинхронный/sdcpp/v1/img_gen+ polling, но это избыточно для первой итерации.
Код запуска run_sd_media.sh
#!/usr/bin/env bash# Генерация изображений через stable-diffusion.cpp.# По умолчанию запускается веб-сервер (sd-server) с UI.# Для одиночной генерации через CLI — флаг -c/--cli.## Usage:# ./run_sd_media.sh # сервер с моделью по умолчанию# ./run_sd_media.sh -m z_image_turbo-Q4_K # сервер с явной моделью# ./run_sd_media.sh --port 8081 --host 127.0.0.1 # другой порт/адрес# ./run_sd_media.sh -c "a cat on the moon" # CLI-генерация# ./run_sd_media.sh -c -m z_image_turbo-Q4_K -W 1024 -H 768 "a cat"set -euo pipefail# --- Конфигурация ---SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"SD_DIR="$SCRIPT_DIR/stable-diffusion.cpp"MEDIA_MODELS_DIR="$SCRIPT_DIR/models/media"TEXT_MODELS_DIR="$SCRIPT_DIR/models"SD_BUILD_BIN="$SD_DIR/build/bin"# Поддерживаемые форматы весов (порядок = приоритет при неоднозначности).# Используется в нормализации -m и в сканировании MEDIA_MODELS_DIR.SUPPORTED_EXTS=(gguf safetensors ckpt pt pth)SERVER_PORT="${SERVER_PORT:-8888}"SERVER_HOST="${SERVER_HOST:-0.0.0.0}"SERVER_HTML="${SERVER_HTML:-$SCRIPT_DIR/web/index.html}"MEM_MAX="${MEM_MAX:-6G}" # Жёсткий лимит памяти для сервераMEM_HIGH="${MEM_HIGH:-5G}" # Мягкий лимит (активное вытеснение)# --- 1. Разбор аргументов ---MODE="server"MODEL_ARG=""PROMPT_ARG=""STEPS_OVERRIDE=""CFG_OVERRIDE=""WIDTH_ARG=""HEIGHT_ARG=""PORT_ARG=""HOST_ARG=""HTML_ARG=""usage() { cat <<EOFUsage: $(basename "$0") [OPTIONS] ["prompt"]Режимы: (по умолчанию) запуск веб-сервера sd-server -c, --cli режим CLI (одна генерация)Модель: -m, --model NAME имя модели (с .gguf, .safetensors, .ckpt, .pt, .pth или без)CLI-режим (-c): --steps N переопределить число шагов --cfg-scale X переопределить CFG scale -W, --width N ширина (требует -H) -H, --height N высота (требует -W)Сервер (по умолчанию): --port N порт (по умолчанию $SERVER_PORT) --host HOST адрес (по умолчанию $SERVER_HOST) --html PATH путь к index.html (по умолчанию $SERVER_HTML)Прочее: -h, --help эта справкаEOF}while [ $# -gt 0 ]; do case "$1" in -c|--cli) MODE="cli"; shift ;; -m|--model) MODEL_ARG="$2"; shift 2 ;; --steps) STEPS_OVERRIDE="--steps $2"; shift 2 ;; --cfg-scale) CFG_OVERRIDE="--cfg-scale $2"; shift 2 ;; -W|--width) WIDTH_ARG="$2"; shift 2 ;; -H|--height) HEIGHT_ARG="$2"; shift 2 ;; --port) PORT_ARG="$2"; shift 2 ;; --host) HOST_ARG="$2"; shift 2 ;; --html) HTML_ARG="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; --) shift; [ $# -gt 0 ] && PROMPT_ARG="$*"; break ;; -*) echo "[!] Неизвестный флаг: $1"; exit 1 ;; *) if [ -z "$PROMPT_ARG" ]; then PROMPT_ARG="$1"; else PROMPT_ARG="$PROMPT_ARG $1"; fi; shift ;; esacdone[ -n "$PORT_ARG" ] && SERVER_PORT="$PORT_ARG"[ -n "$HOST_ARG" ] && SERVER_HOST="$HOST_ARG"[ -n "$HTML_ARG" ] && SERVER_HTML="$HTML_ARG"# Валидация -W/-H (только для CLI)if [ "$MODE" = "cli" ]; then if { [ -n "$WIDTH_ARG" ] && [ -z "$HEIGHT_ARG" ]; } || { [ -z "$WIDTH_ARG" ] && [ -n "$HEIGHT_ARG" ]; }; then echo "[!] -W и -H нужно указывать вместе."; exit 1 fi if [ -n "$WIDTH_ARG" ]; then [[ "$WIDTH_ARG" =~ ^[1-9][0-9]*$ ]] || { echo "[!] -W: положительное целое"; exit 1; } [[ "$HEIGHT_ARG" =~ ^[1-9][0-9]*$ ]] || { echo "[!] -H: положительное целое"; exit 1; } fifi# Нормализация имени модели: если расширение не указано — подбираем по факту наличия файла.# Поддерживаемые форматы: .gguf, .safetensors, .ckpt, .pt, .pth (см. docs stable-diffusion.cpp).if [ -n "$MODEL_ARG" ]; then # Есть ли у имени уже известное расширение? HAS_KNOWN_EXT=0 for ext in "${SUPPORTED_EXTS[@]}"; do if [[ "$MODEL_ARG" == *".$ext" ]]; then HAS_KNOWN_EXT=1 break fi done # Если расширения нет — ищем файл по приоритету if [ "$HAS_KNOWN_EXT" -eq 0 ]; then for ext in "${SUPPORTED_EXTS[@]}"; do if [ -f "$MEDIA_MODELS_DIR/$MODEL_ARG.$ext" ]; then MODEL_ARG="$MODEL_ARG.$ext" break fi done # Если ни один файл не найден — оставляем как есть. # Ошибка всплывёт ниже, на шаге 6, со списком доступных моделей. fifi# --- 2. Сборка ---SD_CLI="$SD_BUILD_BIN/sd-cli"SD_SERVER="$SD_BUILD_BIN/sd-server"if [ ! -x "$SD_CLI" ] || [ ! -x "$SD_SERVER" ]; then echo "[*] Собираю stable-diffusion.cpp..." [ ! -d "$SD_DIR/.git" ] && git clone --recursive https://github.com/leejet/stable-diffusion.cpp.git "$SD_DIR" cmake -S "$SD_DIR" -B "$SD_DIR/build" -DCMAKE_BUILD_TYPE=Release cmake --build "$SD_DIR/build" --config Release -j "$(nproc)"fi[ -x "$SD_CLI" ] && [ -x "$SD_SERVER" ] || { echo "[!] Сборка не удалась"; exit 1; }echo "[*] sd.cpp: $(git -C "$SD_DIR" rev-parse --short HEAD 2>/dev/null || echo 'unknown')"# --- 3. Хелперы ---resolve_path() { local base="$1" path="$2" if [[ "$path" = /* ]]; then echo "$path"; else echo "$base/$path"; fi}# --- 4. Маппинг моделей ---# Ключ — имя .gguf файла (с расширением).# Значение: "VAE(от MEDIA_MODELS_DIR)|LLM(от TEXT_MODELS_DIR)|доп.флаги"declare -A MODEL_MAP# z_image_turbo — быстрая (8 шагов), отличная композиция и планы, точная передача# цвета/одежды; слаба в интеграции источников света и в «воздушных» техниках (гохуа).MODEL_MAP["z_image_turbo-Q4_K.gguf"]="vae/Z-Image/ae.safetensors|Qwen3-4B-Instruct-2507-Q4_K_M.gguf|--steps 8 --cfg-scale 1.0"# mage_flow_turbo — лучший баланс (~8 мин, 2 ГБ resident через mmap): сильна в epic# (дракон, кристаллы, свет), канон в anime; слаба в guohua (коллаж) и 3D (нет среднего плана).MODEL_MAP["mage_flow_turbo_bf16.safetensors"]="vae/mage/mage_flow_vae_bf16.safetensors|Qwen3VL-4B-Instruct-Q4_K_M/Qwen3VL-4B-Instruct-Q4_K_M.gguf|--cfg-scale 1.0 --steps 4"# Заготовки (раскомментировать после скачивания VAE/LLM):# MODEL_MAP["MOSS-Image-SFT-Q4_0.gguf"]="vae/moss/flux2-vae.safetensors|Qwen3.5-9B-Q4_K_M.gguf|--tokenizer $MEDIA_MODELS_DIR/vae/moss/tokenizer.json --cfg-scale 3.0"# MODEL_MAP["Wan2.2-TI2V-5B-Q4_K_M.gguf"]="vae/Wan/wan2.2_vae.safetensors|umt5-xxl-encoder-Q5_K_M.gguf|--steps 20"# sigmas можно вписать вручную, вместо строкого параметра, пример:# --sigmas 1.0,0.978453,0.95418,0.926626,0.89508,0.845148,0.704534,0.414568,0.0# qwen-image-2.1-turbo — медленная (~15 мин/картинку), но лучший реализм и свет;# слаба в композиции/среднем плане, OOM при лимите 6 ГБ (VAE-декод ест ~5 ГБ).# MODEL_MAP["qwen-image-2.1-turbo-Q4_K_M.gguf"]="vae/qwen-image/qwen_image_2.1_vae_bf16.safetensors|Qwen3VL-8B-Instruct-Q4_K_M.gguf|--sigmas qwen-image-2.1-turbo --steps 8 --cfg-scale 1.0 --sampling-method euler"# flux3-image-klein — самый медленный (~33 мин), но лучшая композиция: аниме-постеры# и канонический guohua; слаб в epic (кристаллы/дракон не интегрированы в сцену).# MODEL_MAP["flux3-image-klein-Q4_0.gguf"]="vae/FLUX/ae.safetensors|Qwen3VL-4B-Instruct-Q4_K_M.gguf|--cfg-scale 1.0 --steps 16 --sampling-method euler"# --- 5. Доступные модели ---# Собираем все файлы поддерживаемых расширений через find.# Это надёжнее wildcard-раскрытия и корректно работает с любыми путями.FIND_EXPR=()for ext in "${SUPPORTED_EXTS[@]}"; do [ ${#FIND_EXPR[@]} -gt 0 ] && FIND_EXPR+=(-o) FIND_EXPR+=(-name "*.$ext")donemapfile -t MODEL_FILES < <( find "$MEDIA_MODELS_DIR" -maxdepth 1 -type f \( "${FIND_EXPR[@]}" \) 2>/dev/null | sort)MODELS_AVAILABLE=()for f in "${MODEL_FILES[@]}"; do [ -f "$f" ] || continue base=$(basename "$f") [ -n "${MODEL_MAP[$base]:-}" ] || continue IFS='|' read -r VAE_REL LLM_REL _ <<< "${MODEL_MAP[$base]}" VAE_CHECK="$(resolve_path "$MEDIA_MODELS_DIR" "$VAE_REL")" LLM_CHECK="$(resolve_path "$TEXT_MODELS_DIR" "$LLM_REL")" if [ -f "$VAE_CHECK" ] && [ -f "$LLM_CHECK" ]; then MODELS_AVAILABLE+=("$base") else # Отладка: почему модель отброшена (только при явном запросе) if [ "${DEBUG:-0}" = "1" ]; then echo "[debug] '$base' отброшена:" >&2 [ -f "$VAE_CHECK" ] || echo " нет VAE: $VAE_CHECK" >&2 [ -f "$LLM_CHECK" ] || echo " нет LLM: $LLM_CHECK" >&2 fi fidone# --- 6. Выбор модели ---if [ -n "$MODEL_ARG" ]; then [[ " ${MODELS_AVAILABLE[*]} " =~ " $MODEL_ARG " ]] || { echo "[!] Модель '$MODEL_ARG' недоступна"; exit 1; } MODEL_NAME="$MODEL_ARG"elif [ ${#MODELS_AVAILABLE[@]} -eq 1 ]; then MODEL_NAME="${MODELS_AVAILABLE[0]}"else DEFAULT_MODEL="${DEFAULT_MODEL:-z_image_turbo-Q4_K.gguf}" [[ " ${MODELS_AVAILABLE[*]} " =~ " $DEFAULT_MODEL " ]] || DEFAULT_MODEL="${MODELS_AVAILABLE[0]}" echo "[*] Доступные модели:" for i in "${!MODELS_AVAILABLE[@]}"; do printf ' %d) %s\n' "$((i+1))" "${MODELS_AVAILABLE[$i]}"; done while true; do read -r -p "Выбор [Enter = $DEFAULT_MODEL]: " choice [ -z "$choice" ] && MODEL_NAME="$DEFAULT_MODEL" && break [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#MODELS_AVAILABLE[@]} ] && MODEL_NAME="${MODELS_AVAILABLE[$((choice-1))]}" && break echo " Неверно. Введите 1..${#MODELS_AVAILABLE[@]} или Enter." donefiIFS='|' read -r VAE_REL LLM_REL EXTRA_FLAGS <<< "${MODEL_MAP[$MODEL_NAME]}"MODEL_PATH="$MEDIA_MODELS_DIR/$MODEL_NAME"VAE_PATH="$(resolve_path "$MEDIA_MODELS_DIR" "$VAE_REL")"LLM_PATH="$(resolve_path "$TEXT_MODELS_DIR" "$LLM_REL")"echo "[*] Модель: $MODEL_NAME"echo "[*] VAE: $VAE_PATH"echo "[*] LLM: $LLM_PATH"# --- 7. Опциональные флаги ---HELP_OUT="$("$SD_CLI" --help 2>&1 || true)"MMAP_FLAG=""echo "$HELP_OUT" | grep -q -- "--use-mmap" && MMAP_FLAG="--use-mmap"[ -z "$MMAP_FLAG" ] && echo "$HELP_OUT" | grep -q -- "--mmap" && MMAP_FLAG="--mmap"OFFLOAD_FLAG=""echo "$HELP_OUT" | grep -q -- "--offload-to-cpu" && OFFLOAD_FLAG="--offload-to-cpu"DIM_FLAGS=""[ -n "$WIDTH_ARG" ] && DIM_FLAGS="-W $WIDTH_ARG -H $HEIGHT_ARG"# ==================================================================# CLI-режим# ==================================================================if [ "$MODE" = "cli" ]; then # --- 9a. Промпт --- PROMPT="$PROMPT_ARG" if [ -z "$PROMPT" ]; then if [ ! -t 0 ]; then echo "[!] Промпт не передан, а stdin не терминал. Передайте промпт аргументом." >&2 exit 1 fi read -r -p "Введите промпт: " PROMPT fi [ -n "$PROMPT" ] || { echo "[!] Пустой промпт."; exit 1; } # --- 10a. Выходной файл --- OUTPUT_FILE="output_$(date +%Y%m%d_%H%M%S)_$$.png" echo "[*] Генерация в $OUTPUT_FILE ..." echo "[*] Лимит памяти: $MEM_MAX (soft $MEM_HIGH, swap off)" echo "------------------------------------------------------------" # --- 11a. Запуск (foreground, с лимитом памяти через systemd-run --scope) --- # --scope = синхронный режим: терминал блокируется до завершения sd-cli, # cgroup-лимиты при этом действуют (в отличие от --service). # shellcheck disable=SC2086 CLI_CMD=( "$SD_CLI" --diffusion-model "$MODEL_PATH" --vae "$VAE_PATH" --llm "$LLM_PATH" -p "$PROMPT" -o "$OUTPUT_FILE" ) [ -n "$MMAP_FLAG" ] && CLI_CMD+=($MMAP_FLAG) [ -n "$OFFLOAD_FLAG" ] && CLI_CMD+=($OFFLOAD_FLAG) [ -n "$EXTRA_FLAGS" ] && CLI_CMD+=($EXTRA_FLAGS) [ -n "$DIM_FLAGS" ] && CLI_CMD+=($DIM_FLAGS) [ -n "$STEPS_OVERRIDE" ] && CLI_CMD+=($STEPS_OVERRIDE) [ -n "$CFG_OVERRIDE" ] && CLI_CMD+=($CFG_OVERRIDE) if command -v systemd-run &>/dev/null; then systemd-run --user --scope \ -p MemoryMax="$MEM_MAX" \ -p MemoryHigh="$MEM_HIGH" \ -p MemorySwapMax=0 \ -- "${CLI_CMD[@]}" else echo "[!] systemd-run не найден, запускаю без лимита памяти" "${CLI_CMD[@]}" fi echo "------------------------------------------------------------" echo "[*] Готово: $OUTPUT_FILE" exit 0fi# ==================================================================# Серверный режим (по умолчанию)# ==================================================================[ -f "$SERVER_HTML" ] || { echo "[!] Не найден HTML: $SERVER_HTML"; exit 1; }# Проверка портаif (echo >/dev/tcp/127.0.0.1/"$SERVER_PORT") 2>/dev/null; then echo "[!] Порт $SERVER_PORT занят"; exit 1fiLOG_FILE="$(mktemp -t sd-server.XXXXXX.log)"SERVER_PID=""TAIL_PID=""cleanup() { echo ""; echo "[*] Завершение..." [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true [ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null || true rm -f "$LOG_FILE" echo "[*] Готово."}trap cleanup EXIT INT TERMSERVER_CMD=( "$SD_SERVER" --listen-port "$SERVER_PORT" --listen-ip "$SERVER_HOST" --serve-html-path "$SERVER_HTML" --diffusion-model "$MODEL_PATH" --vae "$VAE_PATH" --llm "$LLM_PATH" -s -1)[ -n "$MMAP_FLAG" ] && SERVER_CMD+=($MMAP_FLAG)[ -n "$OFFLOAD_FLAG" ] && SERVER_CMD+=($OFFLOAD_FLAG)[ -n "$EXTRA_FLAGS" ] && SERVER_CMD+=($EXTRA_FLAGS)[ -n "$STEPS_OVERRIDE" ] && SERVER_CMD+=($STEPS_OVERRIDE)[ -n "$CFG_OVERRIDE" ] && SERVER_CMD+=($CFG_OVERRIDE)echo "[*] Запуск sd-server (лимит памяти: $MEM_MAX, soft: $MEM_HIGH)..."# Оборачиваем в systemd-run для лимита памятиif command -v systemd-run &>/dev/null; then systemd-run --user --scope \ -p MemoryMax="$MEM_MAX" \ -p MemoryHigh="$MEM_HIGH" \ -p MemorySwapMax=0 \ -- "${SERVER_CMD[@]}" >"$LOG_FILE" 2>&1 &else echo "[!] systemd-run не найден, запускаю без лимита памяти" "${SERVER_CMD[@]}" >"$LOG_FILE" 2>&1 &fiSERVER_PID=$!tail -n +1 -f "$LOG_FILE" &TAIL_PID=$!echo "[*] Жду готовности (до 180 с)..."READY=0for i in $(seq 1 180); do kill -0 "$SERVER_PID" 2>/dev/null || { echo "[!] Сервер умер:"; tail -n 40 "$LOG_FILE"; exit 1; } if (echo >/dev/tcp/127.0.0.1/"$SERVER_PORT") 2>/dev/null; then READY=1; break; fi [ $((i % 15)) -eq 0 ] && echo " ... ждём ($i с)" sleep 1done[ "$READY" -eq 1 ] || { echo "[!] Сервер не поднялся"; exit 1; }echo ""echo "============================================================"echo " sd-server is UP"echo "------------------------------------------------------------"echo " Модель: $MODEL_NAME"echo " Listen: $SERVER_HOST:$SERVER_PORT"echo " Web UI: http://127.0.0.1:$SERVER_PORT/"if [ "$SERVER_HOST" = "0.0.0.0" ]; then LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" [ -n "$LAN_IP" ] && echo " LAN: http://$LAN_IP:$SERVER_PORT/"fiecho " Лимит: $MEM_MAX (soft $MEM_HIGH, swap off)"echo "------------------------------------------------------------"echo " Ctrl+C — остановить."echo "============================================================"echo ""while true; do kill -0 "$SERVER_PID" 2>/dev/null || { echo "[!] Сервер остановлен."; break; } sleep 2done
Код HTML-интерфейса web/index.html
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>sd.cpp — генерация</title><style>:root { --bg: #121212; --surface: #1e1e1e; --primary: #bb86fc; --primary-dim: #9a67ea; --text: #e1e1e1; --text-dim: #9e9e9e; --error: #cf6679;}* { box-sizing: border-box; margin: 0; padding: 0; }body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; min-height: 100vh; display: flex; justify-content: center; padding: 24px 16px;}.container { width: 100%; max-width: 1000px; }header { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; border-bottom: 1px solid #333; padding-bottom: 16px;}h1 { font-size: 1.4rem; font-weight: 400; }.model-info { font-size: 0.85rem; color: var(--text-dim); background: var(--surface); padding: 6px 12px; border-radius: 16px; word-break: break-all;}.card { background: var(--surface); border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.4);}label { display: block; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 6px; }textarea, select, input[type="number"], button { font-family: inherit; font-size: 1rem; border: none; border-radius: 8px;}textarea { width: 100%; min-height: 100px; padding: 12px; background: var(--bg); color: var(--text); resize: vertical; border: 1px solid #333; transition: border-color 0.2s;}textarea:focus { outline: none; border-color: var(--primary); }.row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }.field { flex: 1 1 180px; min-width: 0; }select, input[type="number"] { width: 100%; padding: 10px 12px; background: var(--bg); color: var(--text); border: 1px solid #333;}select:focus, input[type="number"]:focus { outline: none; border-color: var(--primary); }button { padding: 10px 24px; background: var(--primary); color: #000; font-weight: 600; cursor: pointer; transition: background 0.2s, opacity 0.2s; white-space: nowrap;}button:hover:not(:disabled) { background: var(--primary-dim); }button:disabled { opacity: 0.5; cursor: not-allowed; }.status { margin-top: 16px; min-height: 24px; font-size: 0.9rem; color: var(--text-dim); display: flex; align-items: center; gap: 8px;}.status.working { color: var(--primary); }.status.error { color: var(--error); }.spinner { width: 16px; height: 16px; border: 2px solid #444; border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; display: none;}.status.working .spinner { display: inline-block; }@keyframes spin { to { transform: rotate(360deg); } }.result { margin-top: 16px; text-align: center; }.result img { max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.6); }.result a { display: inline-block; margin-top: 12px; color: var(--primary); text-decoration: none; font-size: 0.9rem; }.result a:hover { text-decoration: underline; }.hint { font-size: 0.8rem; color: var(--text-dim); margin-top: 8px; }/* Адаптивность */@media (max-width: 600px) { body { padding: 16px 8px; } .card { padding: 16px; } h1 { font-size: 1.2rem; } .row { flex-direction: column; align-items: stretch; } .field { flex: 1 1 auto; } button { width: 100%; padding: 12px; } .model-info { font-size: 0.75rem; padding: 4px 8px; } textarea { min-height: 80px; font-size: 0.95rem; } select, input[type="number"] { font-size: 0.95rem; }}</style></head><body><div class="container"> <header> <h1>sd.cpp — генерация</h1> <span class="model-info" id="model-info">загрузка…</span> </header> <div class="card"> <div class="row"> <div class="field" style="flex: 3 1 300px;"> <label for="prompt">Промпт</label> <textarea id="prompt" placeholder="a cozy cabin in snowy forest, cinematic lighting">a cozy cabin in snowy forest, cinematic lighting</textarea> </div> <div class="field"> <label for="size">Размер</label> <select id="size"> <optgroup label="1:1 — квадрат"> <option value="512x512">512 × 512</option> <option value="768x768">768 × 768</option> <option value="1024x1024">1024 × 1024</option> <option value="1536x1536">1536 × 1536</option> <option value="2048x2048">2048 × 2048</option> </optgroup> <optgroup label="16:9 — широкий"> <option value="1024x576">1024 × 576</option> <option value="1280x720">1280 × 720</option> <option value="1536x864">1536 × 864</option> <option value="2048x1152">2048 × 1152</option> </optgroup> <optgroup label="9:16 — портрет"> <option value="576x1024" selected>576 × 1024</option> <option value="720x1280">720 × 1280</option> <option value="864x1536">864 × 1536</option> <option value="1152x2048">1152 × 2048</option> </optgroup> <optgroup label="4:3 / 3:4"> <option value="1024x768">1024 × 768</option> <option value="768x1024">768 × 1024</option> <option value="1152x864">1152 × 864</option> <option value="864x1152">864 × 1152</option> </optgroup> <optgroup label="3:2 / 2:3"> <option value="1216x832">1216 × 832</option> <option value="832x1216">832 × 1216</option> </optgroup> </select> </div> </div> <div class="row" style="margin-top: 16px;"> <button id="genBtn" onclick="generate()">Сгенерировать</button> </div> <div class="status" id="status"> <span class="spinner"></span> <span id="status-text">Готов к работе</span> </div> <div class="result" id="result"></div> <div class="hint" id="hint"></div> </div></div><script>// --- Автоподгрузка имени модели ---async function fetchModelName() { try { const res = await fetch('/sdapi/v1/options'); if (!res.ok) throw new Error(); const data = await res.json(); const name = data.sd_model_checkpoint || 'неизвестно'; document.getElementById('model-info').textContent = 'Модель: ' + name; } catch { document.getElementById('model-info').textContent = 'Модель: не удалось определить'; }}fetchModelName();const btn = document.getElementById('genBtn');const status = document.getElementById('status');const statusText = document.getElementById('status-text');const resultDiv = document.getElementById('result');function setStatus(text, state) { status.className = 'status' + (state ? ' ' + state : ''); statusText.textContent = text;}async function generate() { const prompt = document.getElementById('prompt').value.trim(); const size = document.getElementById('size').value; if (!prompt) { setStatus('Введите промпт', 'error'); return; } const fullPrompt = `${prompt}`; btn.disabled = true; resultDiv.innerHTML = ''; setStatus(`Работаю…`, 'working'); const startTime = Date.now(); try { const response = await fetch('/v1/images/generations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: fullPrompt, n: 1, size, response_format: 'b64_json' }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const json = await response.json(); if (json.data?.[0]?.b64_json) { const imgSrc = `data:image/png;base64,${json.data[0].b64_json}`; const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); resultDiv.innerHTML = ` <img src="${imgSrc}" alt="Результат"> <br><a href="${imgSrc}" download="generated.png">Скачать PNG</a> <div class="hint">Готово за ${elapsed} с</div>`; setStatus('Готово'); } else { throw new Error('Пустой ответ'); } } catch (err) { setStatus('Ошибка: ' + err.message, 'error'); } finally { btn.disabled = false; }}document.getElementById('prompt').addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && !btn.disabled) { e.preventDefault(); generate(); }});</script></body></html>
Проверка после запуска
1. Сервер поднялся:
./run_sd_media.sh -m z_image_turbo-Q4_K
Плашка:
============================================================ sd-server is UP------------------------------------------------------------ Модель: z_image_turbo-Q4_K.gguf Listen: 0.0.0.0:8888 Web UI: http://127.0.0.1:8888/ LAN: http://<IP>:8888/ Лимит: 6G (soft 5G, swap off)
2. UI открывается: http://127.0.0.1:8888/ — поле промпта, размер, кнопка.
3. Генерация работает: введите «a cozy cabin in snowy forest», размер 576×1024, нажмите. Через 3–8 минут появится картинка.
4. Модель видна в UI: в шапке — Модель: z_image_turbo-Q4_K.gguf (или имя из sd_model_checkpoint).
5. Память:
ps -o pid,rss,%mem -C sd-server
RSS может показывать 6–8 ГБ (виртуальное адресное пространство через mmap), но реально занято ~2–3 ГБ. Проверить через cgroup:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/*/memory.current
Про OOM. При MEM_MAX=6G и высоком разрешении (1024×1024+) VAE-декод может упереться в лимит. Решения:
-
Уменьшить размер (
-W 768 -H 768или-W 512 -H 512). -
Поднять лимит (
MEM_MAX=8G ./run_sd_media.sh). -
Использовать
--vae-tiling(если поддерживается в вашей сборке) — декодирует по тайлам, снижая пик памяти.
ссылка на оригинал статьи https://habr.com/ru/articles/1099504/