Enhance LLM and SwarmUI integration with improved configuration options
- Updated `env.example` and `gpu-rent.vars.example` to include new variables for LLM runtime and SwarmUI options. - Refactored CLI commands to support interactive selection of LLM runtime and workload type (SwarmUI, LLM, or both). - Improved access link generation to handle cases where SwarmUI is disabled, providing clearer user feedback. - Enhanced provisioning logic to conditionally bootstrap SwarmUI based on user configuration, allowing for LLM-only setups. - Updated documentation across multiple files to reflect changes in LLM integration, CLI usage, and configuration management.
This commit is contained in:
@@ -54,8 +54,9 @@ Unix: `./gpu-rent.sh …` (один раз `chmod +x gpu-rent.sh`).
|
|||||||
| `setup` | Wizard: `.env`, LLM, watchdog |
|
| `setup` | Wizard: `.env`, LLM, watchdog |
|
||||||
| `doctor` | Preflight без create |
|
| `doctor` | Preflight без create |
|
||||||
| `flavors` / `dry-run` | Что выберет / план без денег |
|
| `flavors` / `dry-run` | Что выберет / план без денег |
|
||||||
| `up` / `up --yes` | GPU + seed + туннель |
|
| `up` / `up --yes` | GPU + seed + туннель; без `--yes` — меню Selectel/LLM |
|
||||||
| `up --ollama` | + Ollama (см. [docs/llm.md](docs/llm.md)) |
|
| `up --ollama` / `--llamacpp` | + LLM (см. [docs/llm.md](docs/llm.md)) |
|
||||||
|
| `up --llm-only --llamacpp` | только LLM, без SwarmUI |
|
||||||
| `tunnel` / `open` | Снова UI / браузер |
|
| `tunnel` / `open` | Снова UI / браузер |
|
||||||
| `hold` / `status` | Пауза killer / состояние |
|
| `hold` / `status` | Пауза killer / состояние |
|
||||||
| `stop` / `destroy --i-understand-data-loss` | Стоп GPU / + диски |
|
| `stop` / `destroy --i-understand-data-loss` | Стоп GPU / + диски |
|
||||||
@@ -72,6 +73,7 @@ Unix: `./gpu-rent.sh …` (один раз `chmod +x gpu-rent.sh`).
|
|||||||
| `.env` | из `env.example` — `OS_*`, Civitai (не в git) |
|
| `.env` | из `env.example` — `OS_*`, Civitai (не в git) |
|
||||||
| `gpu-rent.vars` | из example — несекретные дефолты, лаунчер |
|
| `gpu-rent.vars` | из example — несекретные дефолты, лаунчер |
|
||||||
| `models.yaml` / `extensions.yaml` | манифесты (из `*.example.yaml`) |
|
| `models.yaml` / `extensions.yaml` | манифесты (из `*.example.yaml`) |
|
||||||
|
| `ollama-models.yaml` / `llamacpp-models.yaml` | LLM (из example; gitignore) |
|
||||||
| `Models/` … | локальный push на `up` |
|
| `Models/` … | локальный push на `up` |
|
||||||
| `.gpu-rent/` | state, SSH-ключ (gitignore) |
|
| `.gpu-rent/` | state, SSH-ключ (gitignore) |
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ CLI поднимает прерываемый GPU в Selectel, держит Swar
|
|||||||
| Word-list промптов | [autocomplete.md](autocomplete.md) |
|
| Word-list промптов | [autocomplete.md](autocomplete.md) |
|
||||||
| Push/pull папок | [local-folders.md](local-folders.md) |
|
| Push/pull папок | [local-folders.md](local-folders.md) |
|
||||||
| Ollama / llama.cpp | [llm.md](llm.md) |
|
| Ollama / llama.cpp | [llm.md](llm.md) |
|
||||||
|
| Capture ссылок с VM | [cli.md](cli.md) (`capture`) + [models.md](models.md) |
|
||||||
| Как устроены диски и killer | [architecture.md](architecture.md) |
|
| Как устроены диски и killer | [architecture.md](architecture.md) |
|
||||||
| Контракт Selectel | [selectel.md](selectel.md) |
|
| Контракт Selectel | [selectel.md](selectel.md) |
|
||||||
| Нативный SwarmUI на VM | [swarmui.md](swarmui.md) |
|
| Нативный SwarmUI на VM | [swarmui.md](swarmui.md) |
|
||||||
|
|||||||
@@ -35,22 +35,24 @@
|
|||||||
|
|
||||||
| Модуль | Ответственность |
|
| Модуль | Ответственность |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `cli` | Typer: `up`, `setup`, `tunnel`, `open`, `status`, `stop`, `destroy`, `logs`, `ssh`, `doctor`, `hold`, `seed-*`, `push` / `pull-output`, `resize-data`, `watchdog`, `dry-run` |
|
| `cli` / `term` / `prompts` | Typer + цветной лог + нумерованные меню |
|
||||||
| `config` / `varsfile` | `.env` + `gpu-rent.vars`, пути |
|
| `config` / `varsfile` | `.env` + `gpu-rent.vars`, пути |
|
||||||
| `state` | JSON сессии: ids, фаза, timestamps |
|
| `state` | JSON сессии: ids, фаза, timestamps |
|
||||||
| `os_client` / `cloud` / `pools` | openstacksdk, ресурсы, скан пулов |
|
| `os_client` / `cloud` / `pools` | openstacksdk, ресурсы, скан пулов |
|
||||||
| `inventory` | Flavors/images, квоты, фоллбек flavor |
|
| `inventory` / `ux` | Flavors, квоты; preview / ServerPlan (flavor/disk/spot) |
|
||||||
| `session` | `cmd_up` / `cmd_stop` / adopt |
|
| `session` | `cmd_up` / `cmd_stop` / adopt |
|
||||||
| `bootstrap` + `remote/bootstrap.sh` | Идемпотентный first-boot; light без apt |
|
| `bootstrap` + `remote/bootstrap.sh` | Идемпотентный first-boot; light без apt |
|
||||||
| `provision` | extensions, autocomplete, civitai seed, push, idle-killer arm, start SwarmUI |
|
| `provision` | extensions, autocomplete, civitai seed, LLM, push, idle-killer arm, start SwarmUI |
|
||||||
|
| `capture` | Инвентарь VM → merge ссылок в локальные манифесты |
|
||||||
| `doctor` | Preflight без mutating compute |
|
| `doctor` | Preflight без mutating compute |
|
||||||
| `sync_files` | SFTP `Models` / Wildcards / workflows / Output |
|
| `sync_files` | SFTP `Models` / Wildcards / workflows / Output |
|
||||||
| `notify` | Toast/звук при backend Idle |
|
| `notify` | Toast/звук при backend Idle |
|
||||||
| `tunnel` | sshtunnel + Nova EXPIRED watchdog |
|
| `tunnel` | sshtunnel + Nova EXPIRED watchdog |
|
||||||
| `local_watchdog` | Опциональный локальный тик → stop при unclean exit |
|
| `local_watchdog` | Опциональный локальный тик → stop при unclean exit |
|
||||||
| `llm_runtime` / `setup_wizard` | Opt-in Ollama/llama.cpp + `ollama-models.yaml` |
|
| `llm_runtime` / `setup_wizard` | Opt-in Ollama/llama.cpp + `ollama-models.yaml` / `llamacpp-models.yaml` |
|
||||||
| `idle_killer` / `hold` | systemd на VM + hold-файл |
|
| `idle_killer` / `hold` | systemd на VM + hold-файл |
|
||||||
| `ready` / `snapshot` | Idle backend + boot snapshot |
|
| `ready` / `snapshot` | Idle backend + boot snapshot |
|
||||||
|
| `access_card` | URL / MCP panel после ready |
|
||||||
|
|
||||||
Каталог сервисов — из Keystone, не из выдуманного `api.selectel.ru/v3/`.
|
Каталог сервисов — из Keystone, не из выдуманного `api.selectel.ru/v3/`.
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -51,13 +51,15 @@ gpu-rent up --yes --ollama
|
|||||||
|
|
||||||
| Команда | Поведение |
|
| Команда | Поведение |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `gpu-rent setup` | Wizard: манифесты, `LLM_RUNTIME`, пресет ollama-models, опционально local-watchdog |
|
| `gpu-rent setup` | Wizard: манифесты (в т.ч. ollama/llamacpp-models), `LLM_RUNTIME` (нумерованное меню), пресет, опционально local-watchdog |
|
||||||
| `gpu-rent doctor` | Preflight **без** create. Exit ≠ 0 → сессию начинать нельзя |
|
| `gpu-rent doctor` | Preflight **без** create. Exit ≠ 0 → сессию начинать нельзя |
|
||||||
| `gpu-rent flavors` | Скан `SCAN_POOLS` × `FLAVOR_PREFERENCE`, список в текущем регионе |
|
| `gpu-rent flavors` | Скан `SCAN_POOLS` × `FLAVOR_PREFERENCE`, список в текущем регионе |
|
||||||
| `gpu-rent dry-run` | План без mutating-вызовов |
|
| `gpu-rent dry-run` | План без mutating-вызовов |
|
||||||
| `gpu-rent up` / `up --yes` | Create/unshelve → bootstrap → optional LLM → **туннель** `:17801`. Без `--yes`: выбор flavor / data GB / preemptible, затем confirm. Ctrl+C = туннель off |
|
| `gpu-rent up` | Без `--yes`: меню LLM (если runtime none) → пресет → **Selectel**: flavor # / data GB / preemptible → confirm → create → туннель. Цветной лог (ошибки/warn/ok) |
|
||||||
|
| `gpu-rent up --yes` | Без вопросов; flavor из `FLAVOR_PREFERENCE` / `DEFAULT_FLAVOR_ID` |
|
||||||
| `gpu-rent up -v` / `--verbose` | Полная таблица doctor на `up` (по умолчанию кратко) |
|
| `gpu-rent up -v` / `--verbose` | Полная таблица doctor на `up` (по умолчанию кратко) |
|
||||||
| `gpu-rent up --ollama` / `--llamacpp` / `--llm …` | LLM рядом со SwarmUI |
|
| `gpu-rent up --ollama` / `--llamacpp` / `--llm …` | LLM рядом со SwarmUI |
|
||||||
|
| `gpu-rent up --no-swarm` / `--llm-only` | Только LLM (нужен `--ollama`/`--llamacpp`); без clone SwarmUI |
|
||||||
| `gpu-rent up --no-update` | Без `git pull` SwarmUI/extensions (только недостающие clone) |
|
| `gpu-rent up --no-update` | Без `git pull` SwarmUI/extensions (только недостающие clone) |
|
||||||
| `gpu-rent up --no-tunnel` | Только облако |
|
| `gpu-rent up --no-tunnel` | Только облако |
|
||||||
| `gpu-rent up --no-spot` | Не preemptible |
|
| `gpu-rent up --no-spot` | Не preemptible |
|
||||||
@@ -135,7 +137,7 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `.env` | Секреты и OpenStack (`OS_*`, токены). Не в git |
|
| `.env` | Секреты и OpenStack (`OS_*`, токены). Не в git |
|
||||||
| `gpu-rent.vars` | Несекретные дефолты; читают лаунчеры и CLI. Пример: `gpu-rent.vars.example` |
|
| `gpu-rent.vars` | Несекретные дефолты; читают лаунчеры и CLI. Пример: `gpu-rent.vars.example` |
|
||||||
| `models.yaml` / `extensions.yaml` / `ollama-models.yaml` | Манифесты |
|
| `models.yaml` / `extensions.yaml` / `ollama-models.yaml` / `llamacpp-models.yaml` | Манифесты (gitignore; из `*.example.yaml`) |
|
||||||
|
|
||||||
### Лаунчер (`gpu-rent.vars`)
|
### Лаунчер (`gpu-rent.vars`)
|
||||||
|
|
||||||
@@ -148,6 +150,9 @@ GPU_RENT_DEFAULT_ARGS=up --yes
|
|||||||
|
|
||||||
UPDATE_GIT=true
|
UPDATE_GIT=true
|
||||||
LLM_RUNTIME=none
|
LLM_RUNTIME=none
|
||||||
|
# DATA_VOLUME_SIZE_GB=100
|
||||||
|
# DEFAULT_FLAVOR_ID=
|
||||||
|
# DEFAULT_SPOT=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Основные переменные (`.env` / vars)
|
### Основные переменные (`.env` / vars)
|
||||||
|
|||||||
+4
-4
@@ -23,10 +23,10 @@ GPU в облаке дорогой. Веса для генерации карт
|
|||||||
- Push из `./Models`, `./Wildcards`, `./CustomWorkflows`, если папки не пустые; optional pull `./Output`.
|
- Push из `./Models`, `./Wildcards`, `./CustomWorkflows`, если папки не пустые; optional pull `./Output`.
|
||||||
- Первый clone git-реп расширений SwarmUI и ComfyUI nodes из `extensions.yaml`.
|
- Первый clone git-реп расширений SwarmUI и ComfyUI nodes из `extensions.yaml`.
|
||||||
- Autocomplete: word-list в `Data/Autocompletions` до старта UI, на каждом `up` проверка новой версии.
|
- Autocomplete: word-list в `Data/Autocompletions` до старта UI, на каждом `up` проверка новой версии.
|
||||||
- `doctor` до create; фоллбек flavor; `hold` для idle-killer; toast когда backend Idle; `open` на 17801.
|
- `doctor` до create; фоллбек flavor; интерактивный выбор flavor/диска; `hold`; toast Idle; `open` на 17801.
|
||||||
- Opt-in LLM (Ollama / llama.cpp) через `setup` / `--ollama` / `LLM_RUNTIME`.
|
- Opt-in LLM (Ollama / llama.cpp) через `setup` / флаги / `LLM_RUNTIME`; `capture` ссылок с VM.
|
||||||
- `status`: диск, ₽/час, окно preempt 24 ч, состояние killer.
|
- `status`: диск, окно preempt 24 ч, killer / LLM.
|
||||||
- Snapshot boot-диска после первого удачного bootstrap.
|
- Snapshot boot после первого удачного bootstrap.
|
||||||
|
|
||||||
## Что не входит (пока)
|
## Что не входит (пока)
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -14,14 +14,17 @@
|
|||||||
| Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) |
|
| Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) |
|
||||||
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
||||||
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
|
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
|
||||||
| Selectel сейчас | Аккаунт есть, GPU-квоту не проверяли — скорее 0. Сначала тикет в поддержку |
|
| Selectel сейчас | Квота GPU часто 0 на новом аккаунте — сначала тикет (setup §2.3), потом `doctor` |
|
||||||
| Образ ОС | **Ubuntu 24.04 LTS GPU Driver 580** (Selectel, **без Docker**). SwarmUI нативно: `launch-linux.sh` + systemd. Нет образа 580 — фоллбек 24.04 Driver 535 без Docker |
|
| Образ ОС | **Ubuntu 24.04 LTS GPU Driver 580** (Selectel, **без Docker**). SwarmUI нативно: `launch-linux.sh` + systemd. Нет образа 580 — фоллбек 24.04 Driver 535 без Docker |
|
||||||
| Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` |
|
| Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` |
|
||||||
| Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь |
|
| Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь |
|
||||||
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
||||||
| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. Ctrl+C на туннеле GPU не гасит (`stop` отдельно). Команда `tunnel` остаётся для повторного входа |
|
| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. Ctrl+C на туннеле GPU не гасит (`stop` отдельно). Команда `tunnel` остаётся для повторного входа |
|
||||||
|
| Interactive `up` | Без `--yes`: нумерованные меню (LLM, пресет, flavor #, data GB, preemptible) → confirm. Изменения можно сохранить в `gpu-rent.vars` |
|
||||||
| Git update | На каждом `up` по умолчанию: `git pull` SwarmUI + репы из `extensions.yaml` + уже установленные на data (`Extensions`/`DLNodes`). `--no-update` или `UPDATE_GIT=false` — не тянуть |
|
| Git update | На каждом `up` по умолчанию: `git pull` SwarmUI + репы из `extensions.yaml` + уже установленные на data (`Extensions`/`DLNodes`). `--no-update` или `UPDATE_GIT=false` — не тянуть |
|
||||||
| LLM (opt-in) | `none` по умолчанию. `ollama` / `llamacpp` — флаг `--ollama`/`--llamacpp`/`--llm`, `LLM_RUNTIME` в vars, или вопрос в interactive `up`/`setup`. Ollama-модели из `ollama-models.yaml`. Порты: Ollama 17811, llama.cpp 17812. Назначение: помощь с промптами, не замена SwarmUI |
|
| LLM (opt-in) | `none` по умолчанию. Флаги / `LLM_RUNTIME` / меню. Манифесты: `ollama-models.yaml`, `llamacpp-models.yaml`. Порты 17811 / 17812. Prompt-help, не замена SwarmUI |
|
||||||
|
| llm-only | `ENABLE_SWARMUI=false` / `WORKLOAD=llm` / `--no-swarm`/`--llm-only`: GPU + LLM без SwarmUI (bootstrap только data disk) |
|
||||||
|
| Capture | `gpu-rent capture` — инвентарь VM → merge **ссылок** в локальные yaml (веса не качать) |
|
||||||
| Perf auto-tune | На `up`: probe GPU → tier. Swarm/Comfy: sageattention ExtraArgs на Ampere+ ≥16 GiB. Ollama: flash/KV/keep-alive + `GPU_OVERHEAD` чтобы оставить VRAM под Krea |
|
| Perf auto-tune | На `up`: probe GPU → tier. Swarm/Comfy: sageattention ExtraArgs на Ampere+ ≥16 GiB. Ollama: flash/KV/keep-alive + `GPU_OVERHEAD` чтобы оставить VRAM под Krea |
|
||||||
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
|
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
|
||||||
| SSH | CLI генерирует `<repo>/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
|
| SSH | CLI генерирует `<repo>/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
|
||||||
|
|||||||
+59
-54
@@ -1,37 +1,52 @@
|
|||||||
# LLM рядом со SwarmUI (opt-in)
|
# LLM рядом со SwarmUI (opt-in)
|
||||||
|
|
||||||
По умолчанию поднимается **только SwarmUI**. Ollama или llama.cpp — отдельно, когда нужны (помощь с промптами и т.п.).
|
По умолчанию поднимается **только SwarmUI**. Ollama или llama.cpp — отдельно (помощь с промптами и т.п.).
|
||||||
|
|
||||||
|
Интерактивные вопросы — **нумерованные меню** (Enter = вариант со ←). Ключ словом (`recommended`, `keep`) тоже принимается.
|
||||||
|
|
||||||
|
Три стека на GPU:
|
||||||
|
|
||||||
|
| # | стек | смысл |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | SwarmUI | только генерация картинок |
|
||||||
|
| 2 | SwarmUI + LLM | UI + Ollama/llama.cpp на той же карте |
|
||||||
|
| 3 | только LLM | **без** SwarmUI — Ollama или llama.cpp |
|
||||||
|
|
||||||
|
Флаги: `up --yes --llm-only --llamacpp` / `--no-swarm --ollama`. Vars: `ENABLE_SWARMUI=false` или `WORKLOAD=llm`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Быстрый путь
|
## Быстрый путь
|
||||||
|
|
||||||
### Вариант A — wizard
|
### A — wizard
|
||||||
|
|
||||||
```text
|
```text
|
||||||
gpu-rent setup
|
gpu-rent setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Выбери `ollama` или `llamacpp`. Для **обоих** спросит пресет моделей. Значение пишется в `gpu-rent.vars` (`LLM_RUNTIME=…`).
|
Меню: runtime → пресет моделей → опционально local-watchdog. Пишет `LLM_RUNTIME` в `gpu-rent.vars`.
|
||||||
|
|
||||||
### Вариант B — флаг на `up`
|
### B — флаг на `up`
|
||||||
|
|
||||||
```text
|
```text
|
||||||
gpu-rent up --yes --ollama
|
gpu-rent up --yes --ollama
|
||||||
gpu-rent up --yes --llm llamacpp
|
gpu-rent up --yes --llm llamacpp
|
||||||
|
gpu-rent up --yes --llm-only --llamacpp # без SwarmUI
|
||||||
```
|
```
|
||||||
|
|
||||||
### Вариант C — вручную в vars
|
С `--yes` пресеты не спрашивает (берёт существующий yaml / example).
|
||||||
|
|
||||||
|
### C — vars вручную
|
||||||
|
|
||||||
```env
|
```env
|
||||||
LLM_RUNTIME=ollama
|
LLM_RUNTIME=ollama
|
||||||
```
|
```
|
||||||
|
|
||||||
Потом обычный `gpu-rent up --yes`.
|
Потом `gpu-rent up --yes`.
|
||||||
|
|
||||||
Чтобы **выключить** LLM на следующих сессиях: `LLM_RUNTIME=none` (на `up` старые unit’ы `gpu-rent-ollama` / `gpu-rent-llamacpp` останавливаются).
|
Выключить: `LLM_RUNTIME=none` (на `up` старые unit’ы `gpu-rent-ollama` / `gpu-rent-llamacpp` останавливаются).
|
||||||
|
|
||||||
Голый `gpu-rent` без args — help, не `up`. Двойной клик: `GPU_RENT_DEFAULT_ARGS=up --yes` в `gpu-rent.vars`. Полный doctor на up: `up -v`.
|
Голый `gpu-rent` без args — **help**. Двойной клик: `GPU_RENT_DEFAULT_ARGS=up --yes`. Полный doctor на up: `up -v`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,105 +63,95 @@ gpu-rent tunnel
|
|||||||
gpu-rent open --llm
|
gpu-rent open --llm
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиент Ollama:
|
```powershell
|
||||||
|
|
||||||
```text
|
|
||||||
# Windows PowerShell
|
|
||||||
$env:OLLAMA_HOST = "http://127.0.0.1:17811"
|
$env:OLLAMA_HOST = "http://127.0.0.1:17811"
|
||||||
```
|
```
|
||||||
|
|
||||||
После успешного `up`/`tunnel` CLI печатает access-card со всеми URL и MCP-сниппетом.
|
После `up`/`tunnel` — access-card с URL и MCP-сниппетом.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ollama: модели
|
## Ollama
|
||||||
|
|
||||||
| Файл | Роль |
|
| Файл | Роль |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ollama-models.example.yaml` | шаблон в git |
|
| `ollama-models.example.yaml` | шаблон в git |
|
||||||
| `ollama-models.yaml` | твой список (gitignore) |
|
| `ollama-models.yaml` | список тегов (gitignore); лаунчер копирует example при отсутствии |
|
||||||
|
|
||||||
На `up` при `LLM_RUNTIME=ollama` CLI делает `ollama pull` по списку. Уже скачанные теги **точно** не трогает (`foo:3b` ≠ `foo:7b`). Лишние модели на диске не удаляет.
|
На `up` — `ollama pull` по списку. Точные теги: уже есть `foo:7b` ≠ skip для `foo:3b`. Лишнее на диске не удаляет.
|
||||||
|
|
||||||
### Пресеты `setup`
|
### Пресеты (меню)
|
||||||
|
|
||||||
| preset | tag | зачем |
|
| # | ключ | tag / смысл |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **recommended** | `huihui_ai/qwen2.5-abliterate:7b` | RU/EN, ~5 GB, меньше отказов |
|
| 1 | **recommended** | `huihui_ai/qwen2.5-abliterate:7b` — RU/EN, ~5 GB |
|
||||||
| light | `qwen2.5:3b` | быстрее, слабее |
|
| 2 | light | `qwen2.5:3b` |
|
||||||
| stock | `qwen2.5:7b` | официальный |
|
| 3 | stock | `qwen2.5:7b` |
|
||||||
| alt | `richardyoung/qwen2.5-7b-instruct-abliterated` | другой abliterate |
|
| 4 | alt | другой abliterate 7B |
|
||||||
| empty | `[]` | только runtime, pull руками |
|
| 5 | empty | только runtime |
|
||||||
|
| — | keep | не трогать yaml (если уже спросили повторно) |
|
||||||
|
|
||||||
Community abliterate-модели без гарантий безопасности — для личного prompt-help рядом со SwarmUI.
|
`default: true` в yaml — preferred в логе; pull идёт по всему списку.
|
||||||
|
|
||||||
Поле `default: true` в yaml — preferred модель (лог / подсказка); pull идёт по всему списку.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Автотюнинг Ollama под GPU
|
## Автотюнинг Ollama
|
||||||
|
|
||||||
На установке `gpu-rent-ollama` читает `/mnt/swarm_data/.gpu-rent-gpu.json` и пишет env в systemd unit (одна карта вместе со SwarmUI):
|
Unit `gpu-rent-ollama` читает `/mnt/swarm_data/.gpu-rent-gpu.json`:
|
||||||
|
|
||||||
| Tier (VRAM) | Flash Attn | KEEP_ALIVE | KV cache | GPU_OVERHEAD (запас под Swarm/Krea) |
|
| Tier (VRAM) | Flash Attn | KEEP_ALIVE | KV cache | GPU_OVERHEAD |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| low (<16 GiB) | off | 2m | q4_0 | 6 GiB |
|
| low (<16 GiB) | off | 2m | q4_0 | 6 GiB |
|
||||||
| mid (16–23) | on* | 5m | q8_0 | 10 GiB |
|
| mid (16–23) | on* | 5m | q8_0 | 10 GiB |
|
||||||
| high (24–47) | on* | 15m | q8_0 | 14 GiB |
|
| high (24–47) | on* | 15m | q8_0 | 14 GiB |
|
||||||
| ultra (≥48) | on* | 30m | q8_0 | 20 GiB |
|
| ultra (≥48) | on* | 30m | q8_0 | 20 GiB |
|
||||||
|
|
||||||
\*Flash на Ampere+ (compute ≥ 8.0). Всегда `NUM_PARALLEL=1`, `MAX_LOADED_MODELS=1`.
|
\*Flash на Ampere+ (compute ≥ 8.0). `NUM_PARALLEL=1`, `MAX_LOADED_MODELS=1`. Env: `/mnt/swarm_data/.gpu-rent-ollama.env`.
|
||||||
|
|
||||||
Файл: `/mnt/swarm_data/.gpu-rent-ollama.env` (пересоздаётся на каждом install Ollama).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## llama.cpp
|
## llama.cpp
|
||||||
|
|
||||||
Манифест GGUF:
|
|
||||||
|
|
||||||
| Файл | Роль |
|
| Файл | Роль |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `llamacpp-models.example.yaml` | шаблон в git |
|
| `llamacpp-models.example.yaml` | шаблон |
|
||||||
| `llamacpp-models.yaml` | URL на `.gguf` (gitignore) |
|
| `llamacpp-models.yaml` | HTTPS URL на `.gguf` (gitignore) |
|
||||||
|
|
||||||
На interactive `up` / `setup` после выбора `llamacpp` спрашивается пресет (как у Ollama). На `up` CLI скачивает GGUF в `/mnt/swarm_data/llamacpp/models`, затем ставит `llama-server` + systemd.
|
На `up`: скачать GGUF → `/mnt/swarm_data/llamacpp/models` → `llama-server` + systemd. Уже скачанные крупные файлы не трогает.
|
||||||
|
|
||||||
### Пресеты
|
### Пресеты (меню)
|
||||||
|
|
||||||
| preset | что |
|
| # | ключ | что |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
| **recommended** | Qwen2.5 7B abliterate Q4_K_M (~4.7 GB) |
|
| 1 | **recommended** | Qwen2.5 7B abliterate Q4_K_M (~4.7 GB) |
|
||||||
| light | Qwen2.5 3B Instruct Q4_K_M |
|
| 2 | light | Qwen2.5 3B Instruct Q4_K_M |
|
||||||
| stock | официальный Qwen2.5 7B Instruct Q4_K_M |
|
| 3 | stock | официальный 7B Instruct Q4_K_M |
|
||||||
| empty | только runtime |
|
| 4 | empty | только runtime |
|
||||||
|
| — | keep | не трогать yaml |
|
||||||
|
|
||||||
Параметры `-ngl` / `-c` — по GPU probe. Опционально `HF_TOKEN` для gated HF. Вручную: положи GGUF в каталог models и `systemctl restart gpu-rent-llamacpp`.
|
`-ngl` / `-c` — по GPU probe. Опционально `HF_TOKEN` для gated HF. Вручную: положи GGUF в models и `systemctl restart gpu-rent-llamacpp`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Idle-killer и LLM
|
## Idle-killer и LLM
|
||||||
|
|
||||||
Busy (не гасить GPU), если:
|
Busy (не гасить GPU):
|
||||||
|
|
||||||
- идёт `ollama pull` (маркер младше ~45 мин; старше — сбрасывается);
|
- `ollama pull` (маркер младше ~45 мин; старше сбрасывается);
|
||||||
- в Ollama есть загруженная модель;
|
- загруженная модель в Ollama;
|
||||||
- llama.cpp занимает слоты.
|
- llama.cpp: слоты заняты.
|
||||||
|
|
||||||
Ошибка установки LLM на `up` — **fail** (не тихий лог): почини или поставь `LLM_RUNTIME=none`.
|
Ошибка установки LLM на `up` — **fail** (не тихий лог).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Supply-chain (опциональный pin)
|
## Supply-chain (опциональный pin)
|
||||||
|
|
||||||
По умолчанию install-скрипты тянут upstream **без pin** (Ollama: `curl|sh`; llama.cpp: GitHub `latest`) — риск подмены. В логе будет `WARN`.
|
По умолчанию install тянет upstream без pin (`WARN` в логе). Жёстче:
|
||||||
|
|
||||||
Жёстче — задай env на VM / при bootstrap:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OLLAMA_VERSION=0.6.5
|
OLLAMA_VERSION=0.6.5
|
||||||
OLLAMA_SHA256=<sha256 of ollama-linux-amd64.tgz>
|
OLLAMA_SHA256=<sha256 of ollama-linux-amd64.tgz>
|
||||||
|
|
||||||
LLAMACPP_TAG=b4690
|
LLAMACPP_TAG=b4690
|
||||||
# или LLAMACPP_ASSET_URL=... + LLAMACPP_SHA256=...
|
# или LLAMACPP_ASSET_URL=... + LLAMACPP_SHA256=...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
Продуктовые развилки закрыты — см. [decisions.md](decisions.md).
|
Продуктовые развилки закрыты — см. [decisions.md](decisions.md).
|
||||||
|
|
||||||
Ниже — что **подтвердить на spike** (живой GPU), не в споре:
|
Подтвердить **на spike** (живой GPU), не в споре:
|
||||||
|
|
||||||
- Качалка модели в UI: отдельного poll-API нет → v1 считает busy через очередь / loading / backend≠idle; иначе пользователь жмёт `hold`.
|
- Качалка модели в UI: отдельного poll-API нет → busy через очередь / loading / backend≠idle; иначе `hold`.
|
||||||
- Реальная цена 100 GB сетевого диска и 1×4090 preemptible ₽/час в твоём сегменте (в OpenStack API нет — только панель).
|
- Реальная цена data-диска и 1×4090 preemptible ₽/час в твоём сегменте (в API нет — панель).
|
||||||
- Имя GPU-образа (без Docker) и flavor id в твоём пуле (в git не класть); какие из `FLAVOR_PREFERENCE` реально есть.
|
- Имя GPU-образа Driver 580 и живые flavor id в пуле (в git не класть).
|
||||||
- Application credential с **узкими** access_rules (DELETE/GET только этого `server_id`) на Selectel: если Keystone отвергает rules — CLI **fail closed** (idle-killer не вооружается unrestricted). Нужно проверить на spike и при необходимости править права / путь.
|
- App cred с узкими access_rules на Selectel: код fail closed; на spike убедиться, что Keystone принимает rules.
|
||||||
- Snapshot attached boot volume после Idle: время и можно ли сразу create from snapshot.
|
- Snapshot attached boot после Idle: время; create from snapshot ок.
|
||||||
|
|
||||||
Если spike покажет, что 45 минут льготы мало на первую установку ComfyUI — подними `IDLE_GRACE_MINUTES`, не отключай killer.
|
Если 45 мин льготы мало на первый Comfy — подними `IDLE_GRACE_MINUTES`, не отключай killer.
|
||||||
|
|||||||
@@ -83,6 +83,9 @@
|
|||||||
- [x] Оценка ₽ честно («смотри панель»), предупреждение про диск 24/7 и idle-killer
|
- [x] Оценка ₽ честно («смотри панель»), предупреждение про диск 24/7 и idle-killer
|
||||||
- [x] `NOTIFY_READY`: Windows toast + звук при backend Idle
|
- [x] `NOTIFY_READY`: Windows toast + звук при backend Idle
|
||||||
- [x] Пользовательский README поверх `docs/`
|
- [x] Пользовательский README поверх `docs/`
|
||||||
|
- [x] Opt-in LLM + `setup`; нумерованные меню; interactive Selectel plan (flavor/disk/spot)
|
||||||
|
- [x] Цветной CLI-лог; `capture` ссылок с VM
|
||||||
|
- [x] Local-watchdog; access-card
|
||||||
|
|
||||||
## Вне скоупа v1
|
## Вне скоупа v1
|
||||||
|
|
||||||
|
|||||||
+16
-6
@@ -49,6 +49,7 @@ chmod +x gpu-rent.sh
|
|||||||
- `env.example` → `.env`
|
- `env.example` → `.env`
|
||||||
- `models.example.yaml` → `models.yaml`
|
- `models.example.yaml` → `models.yaml`
|
||||||
- `extensions.example.yaml` → `extensions.yaml`
|
- `extensions.example.yaml` → `extensions.yaml`
|
||||||
|
- `ollama-models.example.yaml` / `llamacpp-models.example.yaml` → соответствующие yaml (лаунчер / setup)
|
||||||
- `gpu-rent.vars.example` → `gpu-rent.vars`
|
- `gpu-rent.vars.example` → `gpu-rent.vars`
|
||||||
|
|
||||||
Секреты только в `<repo>/.env` и runtime в `<repo>/.gpu-rent/` (оба в `.gitignore`).
|
Секреты только в `<repo>/.env` и runtime в `<repo>/.gpu-rent/` (оба в `.gitignore`).
|
||||||
@@ -59,7 +60,7 @@ chmod +x gpu-rent.sh
|
|||||||
.\gpu-rent.ps1 setup
|
.\gpu-rent.ps1 setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Спросит LLM (none/ollama/llamacpp) и local-watchdog. Можно пропустить и настроить позже.
|
Спросит (нумерованные меню): LLM runtime → пресет моделей → local-watchdog. Можно пропустить и настроить позже (`LLM_RUNTIME` / `up`).
|
||||||
|
|
||||||
Для разработки / pytest:
|
Для разработки / pytest:
|
||||||
|
|
||||||
@@ -215,15 +216,23 @@ copy models.example.yaml models.yaml
|
|||||||
|
|
||||||
Первый прогон долгий (образ, SwarmUI, Comfy, seed): ориентир **20–40 минут**. Не закрывай терминал посередине bootstrap.
|
Первый прогон долгий (образ, SwarmUI, Comfy, seed): ориентир **20–40 минут**. Не закрывай терминал посередине bootstrap.
|
||||||
|
|
||||||
|
Без вопросов (CI / двойной клик):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\gpu-rent.ps1 up --yes
|
.\gpu-rent.ps1 up --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Интерактивно (меню LLM, пресет, **flavor / диск / preemptible**, confirm):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gpu-rent.ps1 up
|
||||||
|
```
|
||||||
|
|
||||||
Что произойдёт:
|
Что произойдёт:
|
||||||
|
|
||||||
1. Снова короткий doctor (кратко; полный — `up -v`).
|
1. Короткий doctor (полный — `up -v`).
|
||||||
2. Create/unshelve preemptible GPU + диски.
|
2. Create/unshelve GPU + диски (после confirm).
|
||||||
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок.
|
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок; optional LLM.
|
||||||
4. Туннель на `localhost:17801`, access-card с URL / MCP.
|
4. Туннель на `localhost:17801`, access-card с URL / MCP.
|
||||||
5. Процесс ждёт: **Ctrl+C** закрывает только туннель, GPU остаётся.
|
5. Процесс ждёт: **Ctrl+C** закрывает только туннель, GPU остаётся.
|
||||||
|
|
||||||
@@ -233,9 +242,10 @@ copy models.example.yaml models.yaml
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--no-tunnel` | только облако; UI потом: `tunnel --open` |
|
| `--no-tunnel` | только облако; UI потом: `tunnel --open` |
|
||||||
| `--no-update` | не `git pull` SwarmUI/extensions |
|
| `--no-update` | не `git pull` SwarmUI/extensions |
|
||||||
| `--ollama` | поднять Ollama рядом ([llm.md](llm.md)) |
|
| `--ollama` / `--llamacpp` | LLM рядом ([llm.md](llm.md)) |
|
||||||
|
| `--no-swarm` / `--llm-only` | только LLM, без SwarmUI (нужен runtime) |
|
||||||
| `--no-spot` | обычный (не preemptible) тариф |
|
| `--no-spot` | обычный (не preemptible) тариф |
|
||||||
| `--flavor ID` | явный flavor, без фоллбека |
|
| `--flavor ID` | явный flavor (пропускает меню flavor) |
|
||||||
|
|
||||||
Двойной клик без аргументов: в `gpu-rent.vars` задай `GPU_RENT_DEFAULT_ARGS=up --yes`. Сам `gpu-rent` без args показывает **help**, не поднимает GPU.
|
Двойной клик без аргументов: в `gpu-rent.vars` задай `GPU_RENT_DEFAULT_ARGS=up --yes`. Сам `gpu-rent` без args показывает **help**, не поднимает GPU.
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -103,12 +103,13 @@
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\gpu-rent.ps1 up --yes --ollama
|
.\gpu-rent.ps1 up --yes --ollama
|
||||||
|
# или интерактивно: up → меню runtime/пресет
|
||||||
```
|
```
|
||||||
|
|
||||||
| Шаг | OK? |
|
| Шаг | OK? |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Ollama на :17811 через туннель | |
|
| Ollama :17811 или llama.cpp :17812 через туннель | |
|
||||||
| `ollama pull` из манифеста | |
|
| pull / GGUF из манифеста | |
|
||||||
| `LLM_RUNTIME=none` гасит unit на следующем up | |
|
| `LLM_RUNTIME=none` гасит unit на следующем up | |
|
||||||
|
|
||||||
См. [llm.md](llm.md).
|
См. [llm.md](llm.md).
|
||||||
|
|||||||
+6
-1
@@ -38,10 +38,15 @@ AUTOCOMPLETE_GITHUB_REF=main
|
|||||||
AUTOCOMPLETE_FILENAME=danbooru.csv
|
AUTOCOMPLETE_FILENAME=danbooru.csv
|
||||||
|
|
||||||
SWARMUI_LOCAL_PORT=17801
|
SWARMUI_LOCAL_PORT=17801
|
||||||
# Optional LLM beside SwarmUI: none | ollama | llamacpp (or gpu-rent setup / --ollama)
|
# Optional LLM beside SwarmUI: none | ollama | llamacpp (setup / --ollama / --llamacpp)
|
||||||
|
# ENABLE_SWARMUI=true
|
||||||
|
# WORKLOAD=llm # llm-only (no SwarmUI); requires LLM_RUNTIME≠none
|
||||||
LLM_RUNTIME=none
|
LLM_RUNTIME=none
|
||||||
OLLAMA_LOCAL_PORT=17811
|
OLLAMA_LOCAL_PORT=17811
|
||||||
LLAMACPP_LOCAL_PORT=17812
|
LLAMACPP_LOCAL_PORT=17812
|
||||||
|
# OLLAMA_MODELS_MANIFEST=
|
||||||
|
# LLAMACPP_MODELS_MANIFEST=
|
||||||
|
# HF_TOKEN= # optional, gated GGUF on Hugging Face
|
||||||
# git pull SwarmUI + extensions on each up (default true). CLI: --no-update
|
# git pull SwarmUI + extensions on each up (default true). CLI: --no-update
|
||||||
UPDATE_GIT=true
|
UPDATE_GIT=true
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
# SCAN_POOLS=ru-6,ru-7
|
# SCAN_POOLS=ru-6,ru-7
|
||||||
# FLAVOR_PREFERENCE=4090-24,4090-48,a5000,a100-40
|
# FLAVOR_PREFERENCE=4090-24,4090-48,a5000,a100-40
|
||||||
# SWARMUI_LOCAL_PORT=17801
|
# SWARMUI_LOCAL_PORT=17801
|
||||||
|
# DATA_VOLUME_SIZE_GB=100
|
||||||
|
# DEFAULT_FLAVOR_ID=
|
||||||
|
# DEFAULT_SPOT=true
|
||||||
|
# ENABLE_SWARMUI=true
|
||||||
|
# WORKLOAD=llm # то же что ENABLE_SWARMUI=false (только LLM)
|
||||||
# LLM_RUNTIME=none
|
# LLM_RUNTIME=none
|
||||||
# OLLAMA_LOCAL_PORT=17811
|
# OLLAMA_LOCAL_PORT=17811
|
||||||
# LLAMACPP_LOCAL_PORT=17812
|
# LLAMACPP_LOCAL_PORT=17812
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ def resolve_llm_runtime(cfg: Config) -> str:
|
|||||||
|
|
||||||
def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
||||||
"""Build the list of user-facing endpoints (unit-tested)."""
|
"""Build the list of user-facing endpoints (unit-tested)."""
|
||||||
|
links: list[AccessLink] = []
|
||||||
|
swarm = bool(getattr(cfg, "enable_swarmui", True))
|
||||||
|
if tunneled:
|
||||||
|
if swarm:
|
||||||
port = cfg.swarmui_local_port
|
port = cfg.swarmui_local_port
|
||||||
base = f"http://127.0.0.1:{port}"
|
base = f"http://127.0.0.1:{port}"
|
||||||
links: list[AccessLink] = []
|
|
||||||
if tunneled:
|
|
||||||
links.extend(
|
links.extend(
|
||||||
[
|
[
|
||||||
AccessLink("SwarmUI UI", base, "браузер"),
|
AccessLink("SwarmUI UI", base, "браузер"),
|
||||||
@@ -82,6 +84,14 @@ def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
|||||||
AccessLink("Models", f"http://127.0.0.1:{p}/v1/models", "list"),
|
AccessLink("Models", f"http://127.0.0.1:{p}/v1/models", "list"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
if not links:
|
||||||
|
links.append(
|
||||||
|
AccessLink(
|
||||||
|
"Туннель",
|
||||||
|
"gpu-rent tunnel",
|
||||||
|
"нет сервисов — ENABLE_SWARMUI / LLM_RUNTIME",
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
links.append(
|
links.append(
|
||||||
AccessLink(
|
AccessLink(
|
||||||
@@ -94,6 +104,8 @@ def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
|||||||
|
|
||||||
|
|
||||||
def mcp_snippet_lines(cfg: Config) -> list[str]:
|
def mcp_snippet_lines(cfg: Config) -> list[str]:
|
||||||
|
if not bool(getattr(cfg, "enable_swarmui", True)):
|
||||||
|
return ["# SwarmUI MCP skip (llm-only)"]
|
||||||
port = cfg.swarmui_local_port
|
port = cfg.swarmui_local_port
|
||||||
return [
|
return [
|
||||||
"{",
|
"{",
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ def run_bootstrap(
|
|||||||
update: bool = True,
|
update: bool = True,
|
||||||
light: bool = False,
|
light: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if light:
|
skip_swarm = not bool(getattr(cfg, "enable_swarmui", True))
|
||||||
|
if skip_swarm:
|
||||||
|
log("bootstrap llm-only (data disk, без SwarmUI)")
|
||||||
|
elif light:
|
||||||
log("bootstrap SwarmUI (light: без apt)")
|
log("bootstrap SwarmUI (light: без apt)")
|
||||||
else:
|
else:
|
||||||
log("bootstrap SwarmUI на VM (идемпотентно, без Docker)")
|
log("bootstrap SwarmUI на VM (идемпотентно, без Docker)")
|
||||||
|
if not skip_swarm:
|
||||||
if update:
|
if update:
|
||||||
log("git update: SwarmUI on")
|
log("git update: SwarmUI on")
|
||||||
else:
|
else:
|
||||||
@@ -43,9 +47,13 @@ def run_bootstrap(
|
|||||||
"SWARM_USER": cfg.ssh_user,
|
"SWARM_USER": cfg.ssh_user,
|
||||||
"GPU_RENT_UPDATE_GIT": "1" if update else "0",
|
"GPU_RENT_UPDATE_GIT": "1" if update else "0",
|
||||||
"GPU_RENT_BOOTSTRAP_LIGHT": "1" if light else "0",
|
"GPU_RENT_BOOTSTRAP_LIGHT": "1" if light else "0",
|
||||||
|
"GPU_RENT_SKIP_SWARMUI": "1" if skip_swarm else "0",
|
||||||
},
|
},
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
if "bootstrap ok" not in out:
|
if "bootstrap ok" not in out:
|
||||||
raise CloudError(f"bootstrap не подтвердил успех:\n{out[-800:]}")
|
raise CloudError(f"bootstrap не подтвердил успех:\n{out[-800:]}")
|
||||||
|
if skip_swarm:
|
||||||
|
log("data disk готов — дальше LLM")
|
||||||
|
else:
|
||||||
log("диски и systemd unit готовы; дальше seed, потом start swarmui")
|
log("диски и systemd unit готовы; дальше seed, потом start swarmui")
|
||||||
|
|||||||
+92
-18
@@ -303,10 +303,10 @@ def status() -> None:
|
|||||||
def open(
|
def open(
|
||||||
llm: bool = typer.Option(False, "--llm", help="Открыть LLM API URL вместо SwarmUI"),
|
llm: bool = typer.Option(False, "--llm", help="Открыть LLM API URL вместо SwarmUI"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Открыть браузер на SwarmUI :17801 (или --llm на Ollama/llama.cpp)."""
|
"""Открыть браузер на SwarmUI :17801 (или --llm / llm-only на Ollama/llama.cpp)."""
|
||||||
cfg = load_config(require_auth=False)
|
cfg = load_config(require_auth=False)
|
||||||
if llm:
|
use_llm = llm or not bool(getattr(cfg, "enable_swarmui", True))
|
||||||
from gpu_rent.llm_runtime import normalize_runtime
|
if use_llm:
|
||||||
from gpu_rent.access_card import resolve_llm_runtime
|
from gpu_rent.access_card import resolve_llm_runtime
|
||||||
|
|
||||||
runtime = resolve_llm_runtime(cfg)
|
runtime = resolve_llm_runtime(cfg)
|
||||||
@@ -398,18 +398,25 @@ def up(
|
|||||||
),
|
),
|
||||||
ollama: bool = typer.Option(False, "--ollama", help="То же что --llm ollama"),
|
ollama: bool = typer.Option(False, "--ollama", help="То же что --llm ollama"),
|
||||||
llamacpp: bool = typer.Option(False, "--llamacpp", help="То же что --llm llamacpp"),
|
llamacpp: bool = typer.Option(False, "--llamacpp", help="То же что --llm llamacpp"),
|
||||||
|
no_swarm: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--no-swarm",
|
||||||
|
"--llm-only",
|
||||||
|
help="Только LLM на GPU, без установки SwarmUI",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create/unshelve GPU, bootstrap SwarmUI, по умолчанию туннель на :17801."""
|
"""Create/unshelve GPU; SwarmUI и/или LLM; по умолчанию туннель."""
|
||||||
try:
|
try:
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
|
|
||||||
from gpu_rent.llm_runtime import (
|
from gpu_rent.llm_runtime import (
|
||||||
append_vars_llm_runtime,
|
|
||||||
decide_runtime,
|
decide_runtime,
|
||||||
ensure_ollama_manifest_from_example,
|
ensure_ollama_manifest_from_example,
|
||||||
write_ollama_models_preset,
|
write_ollama_models_preset,
|
||||||
)
|
)
|
||||||
from gpu_rent.paths import vars_path
|
from gpu_rent.paths import vars_path
|
||||||
|
from gpu_rent.prompts import MenuItem, prompt_menu
|
||||||
|
from gpu_rent.varsfile import upsert_vars
|
||||||
|
|
||||||
checks = run_doctor()
|
checks = run_doctor()
|
||||||
code = _print_checks(checks, quiet=not verbose)
|
code = _print_checks(checks, quiet=not verbose)
|
||||||
@@ -427,33 +434,93 @@ def up(
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise GpuRentError(str(exc)) from exc
|
raise GpuRentError(str(exc)) from exc
|
||||||
|
|
||||||
|
enable_swarm = False if no_swarm else cfg.enable_swarmui
|
||||||
asked_model_preset = False
|
asked_model_preset = False
|
||||||
if not yes and runtime == "none" and not llm and not ollama and not llamacpp:
|
llm_flags = bool(llm or ollama or llamacpp or no_swarm)
|
||||||
|
|
||||||
|
if not yes and not llm_flags:
|
||||||
from gpu_rent.llm_runtime import (
|
from gpu_rent.llm_runtime import (
|
||||||
llamacpp_preset_menu,
|
llamacpp_preset_menu,
|
||||||
llm_runtime_menu,
|
|
||||||
ollama_preset_menu,
|
ollama_preset_menu,
|
||||||
|
workload_menu,
|
||||||
)
|
)
|
||||||
from gpu_rent.prompts import prompt_menu
|
|
||||||
|
|
||||||
def _ask(msg: str, default: str = "") -> str:
|
def _ask(msg: str, default: str = "") -> str:
|
||||||
return typer.prompt(msg, default=default)
|
return typer.prompt(msg, default=default)
|
||||||
|
|
||||||
|
default_stack = (
|
||||||
|
"llm"
|
||||||
|
if not cfg.enable_swarmui
|
||||||
|
else ("both" if runtime != "none" else "swarm")
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
stack = prompt_menu(
|
||||||
|
"Что поднять на GPU",
|
||||||
|
workload_menu(),
|
||||||
|
default=default_stack,
|
||||||
|
ask=_ask,
|
||||||
|
show=log,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise GpuRentError(str(exc)) from exc
|
||||||
|
|
||||||
|
if stack == "swarm":
|
||||||
|
enable_swarm = True
|
||||||
|
runtime = "none"
|
||||||
|
elif stack == "both":
|
||||||
|
enable_swarm = True
|
||||||
|
if runtime == "none":
|
||||||
try:
|
try:
|
||||||
choice = prompt_menu(
|
choice = prompt_menu(
|
||||||
"LLM рядом со SwarmUI",
|
"LLM runtime",
|
||||||
llm_runtime_menu(),
|
[
|
||||||
default="none",
|
MenuItem("ollama", "Ollama (+ pull моделей)"),
|
||||||
|
MenuItem("llamacpp", "llama.cpp server (+ GGUF)"),
|
||||||
|
],
|
||||||
|
default="ollama",
|
||||||
ask=_ask,
|
ask=_ask,
|
||||||
show=log,
|
show=log,
|
||||||
)
|
)
|
||||||
runtime = decide_runtime(
|
runtime = decide_runtime(
|
||||||
flag=choice, ollama_flag=False, llamacpp_flag=False, from_config="none"
|
flag=choice,
|
||||||
|
ollama_flag=False,
|
||||||
|
llamacpp_flag=False,
|
||||||
|
from_config="none",
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise GpuRentError(str(exc)) from exc
|
raise GpuRentError(str(exc)) from exc
|
||||||
if runtime != "none" and typer.confirm("Запомнить LLM_RUNTIME в gpu-rent.vars?", default=True):
|
else:
|
||||||
append_vars_llm_runtime(vars_path(), runtime)
|
enable_swarm = False
|
||||||
|
if runtime == "none":
|
||||||
|
try:
|
||||||
|
choice = prompt_menu(
|
||||||
|
"LLM runtime",
|
||||||
|
[
|
||||||
|
MenuItem("ollama", "Ollama (+ pull моделей)"),
|
||||||
|
MenuItem("llamacpp", "llama.cpp server (+ GGUF)"),
|
||||||
|
],
|
||||||
|
default="llamacpp",
|
||||||
|
ask=_ask,
|
||||||
|
show=log,
|
||||||
|
)
|
||||||
|
runtime = decide_runtime(
|
||||||
|
flag=choice,
|
||||||
|
ollama_flag=False,
|
||||||
|
llamacpp_flag=False,
|
||||||
|
from_config="none",
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise GpuRentError(str(exc)) from exc
|
||||||
|
|
||||||
|
if typer.confirm("Запомнить стек в gpu-rent.vars?", default=True):
|
||||||
|
upsert_vars(
|
||||||
|
vars_path(),
|
||||||
|
{
|
||||||
|
"ENABLE_SWARMUI": "true" if enable_swarm else "false",
|
||||||
|
"LLM_RUNTIME": runtime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if runtime == "ollama":
|
if runtime == "ollama":
|
||||||
ensure_ollama_manifest_from_example()
|
ensure_ollama_manifest_from_example()
|
||||||
try:
|
try:
|
||||||
@@ -498,7 +565,6 @@ def up(
|
|||||||
ollama_preset_menu,
|
ollama_preset_menu,
|
||||||
write_llamacpp_models_preset,
|
write_llamacpp_models_preset,
|
||||||
)
|
)
|
||||||
from gpu_rent.prompts import prompt_menu
|
|
||||||
|
|
||||||
def _ask2(msg: str, default: str = "") -> str:
|
def _ask2(msg: str, default: str = "") -> str:
|
||||||
return typer.prompt(msg, default=default)
|
return typer.prompt(msg, default=default)
|
||||||
@@ -529,9 +595,17 @@ def up(
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise GpuRentError(str(exc)) from exc
|
raise GpuRentError(str(exc)) from exc
|
||||||
|
|
||||||
cfg = replace(cfg, llm_runtime=runtime)
|
if not enable_swarm and runtime == "none":
|
||||||
if runtime != "none":
|
raise GpuRentError(
|
||||||
ok(f"LLM runtime: {runtime}")
|
"llm-only требует --ollama / --llamacpp / --llm … "
|
||||||
|
"(или убери --no-swarm / ENABLE_SWARMUI=true)"
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg = replace(cfg, llm_runtime=runtime, enable_swarmui=enable_swarm)
|
||||||
|
if enable_swarm:
|
||||||
|
ok("стек: SwarmUI" + (f" + {runtime}" if runtime != "none" else ""))
|
||||||
|
else:
|
||||||
|
ok(f"стек: llm-only ({runtime})")
|
||||||
|
|
||||||
def confirm(msg: str) -> bool:
|
def confirm(msg: str) -> bool:
|
||||||
return typer.confirm(msg)
|
return typer.confirm(msg)
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ def _as_bool(value: str | None, default: bool) -> bool:
|
|||||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_enable_swarmui(raw: str | None, *, default: bool = True) -> bool:
|
||||||
|
"""ENABLE_SWARMUI / WORKLOAD=llm|swarm|both → whether to install SwarmUI."""
|
||||||
|
wl = (os.environ.get("WORKLOAD") or "").strip().lower()
|
||||||
|
if wl in {"llm", "llm-only", "llm_only", "llama", "ollama-only"}:
|
||||||
|
return False
|
||||||
|
if wl in {"swarm", "swarmui", "ui"}:
|
||||||
|
return True
|
||||||
|
if wl in {"both", "all", "full"}:
|
||||||
|
return True
|
||||||
|
if raw is None or str(raw).strip() == "":
|
||||||
|
return default
|
||||||
|
return _as_bool(str(raw), default)
|
||||||
|
|
||||||
|
|
||||||
def _as_int(value: str | None, default: int) -> int:
|
def _as_int(value: str | None, default: int) -> int:
|
||||||
if value is None or value.strip() == "":
|
if value is None or value.strip() == "":
|
||||||
return default
|
return default
|
||||||
@@ -84,6 +98,7 @@ class Config:
|
|||||||
update_git: bool
|
update_git: bool
|
||||||
|
|
||||||
llm_runtime: str
|
llm_runtime: str
|
||||||
|
enable_swarmui: bool
|
||||||
ollama_models_manifest: Path
|
ollama_models_manifest: Path
|
||||||
llamacpp_models_manifest: Path
|
llamacpp_models_manifest: Path
|
||||||
ollama_local_port: int
|
ollama_local_port: int
|
||||||
@@ -169,6 +184,8 @@ def load_config(*, require_auth: bool = True) -> Config:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
llm_runtime = "none"
|
llm_runtime = "none"
|
||||||
|
|
||||||
|
enable_swarmui = parse_enable_swarmui(os.environ.get("ENABLE_SWARMUI"), default=True)
|
||||||
|
|
||||||
def _dir(env_name: str, folder: str) -> Path:
|
def _dir(env_name: str, folder: str) -> Path:
|
||||||
raw = (os.environ.get(env_name) or "").strip()
|
raw = (os.environ.get(env_name) or "").strip()
|
||||||
return Path(raw).expanduser() if raw else (root / folder)
|
return Path(raw).expanduser() if raw else (root / folder)
|
||||||
@@ -210,6 +227,7 @@ def load_config(*, require_auth: bool = True) -> Config:
|
|||||||
swarmui_image=(os.environ.get("SWARMUI_IMAGE") or "").strip(),
|
swarmui_image=(os.environ.get("SWARMUI_IMAGE") or "").strip(),
|
||||||
update_git=_as_bool(os.environ.get("UPDATE_GIT"), True),
|
update_git=_as_bool(os.environ.get("UPDATE_GIT"), True),
|
||||||
llm_runtime=llm_runtime,
|
llm_runtime=llm_runtime,
|
||||||
|
enable_swarmui=enable_swarmui,
|
||||||
ollama_models_manifest=ollama_manifest,
|
ollama_models_manifest=ollama_manifest,
|
||||||
llamacpp_models_manifest=llamacpp_manifest,
|
llamacpp_models_manifest=llamacpp_manifest,
|
||||||
ollama_local_port=_as_int(os.environ.get("OLLAMA_LOCAL_PORT"), 17811),
|
ollama_local_port=_as_int(os.environ.get("OLLAMA_LOCAL_PORT"), 17811),
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def install_units(cfg: Config, host: str, log: Log) -> None:
|
|||||||
)
|
)
|
||||||
service = f"""[Unit]
|
service = f"""[Unit]
|
||||||
Description=gpu-rent idle-killer (one shot)
|
Description=gpu-rent idle-killer (one shot)
|
||||||
After=network-online.target swarmui.service
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ LLM_RUNTIME_LABELS: dict[str, str] = {
|
|||||||
"llamacpp": "llama.cpp server (+ GGUF)",
|
"llamacpp": "llama.cpp server (+ GGUF)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WORKLOAD_LABELS: dict[str, str] = {
|
||||||
|
"swarm": "только SwarmUI",
|
||||||
|
"both": "SwarmUI + LLM",
|
||||||
|
"llm": "только LLM (без SwarmUI)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def llm_runtime_menu() -> list:
|
def llm_runtime_menu() -> list:
|
||||||
from gpu_rent.prompts import MenuItem
|
from gpu_rent.prompts import MenuItem
|
||||||
@@ -78,6 +84,12 @@ def llm_runtime_menu() -> list:
|
|||||||
return [MenuItem(k, f"{k} — {LLM_RUNTIME_LABELS[k]}") for k in ("none", "ollama", "llamacpp")]
|
return [MenuItem(k, f"{k} — {LLM_RUNTIME_LABELS[k]}") for k in ("none", "ollama", "llamacpp")]
|
||||||
|
|
||||||
|
|
||||||
|
def workload_menu() -> list:
|
||||||
|
from gpu_rent.prompts import MenuItem
|
||||||
|
|
||||||
|
return [MenuItem(k, WORKLOAD_LABELS[k]) for k in ("swarm", "both", "llm")]
|
||||||
|
|
||||||
|
|
||||||
def ollama_preset_menu(*, include_keep: bool = False) -> list:
|
def ollama_preset_menu(*, include_keep: bool = False) -> list:
|
||||||
from gpu_rent.prompts import MenuItem
|
from gpu_rent.prompts import MenuItem
|
||||||
|
|
||||||
|
|||||||
@@ -440,7 +440,18 @@ def provision_vm(
|
|||||||
server_id: str | None = None,
|
server_id: str | None = None,
|
||||||
update: bool = True,
|
update: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
from gpu_rent.llm_runtime import normalize_runtime
|
||||||
|
from gpu_rent.state import load_state, save_state
|
||||||
|
|
||||||
|
swarm = bool(getattr(cfg, "enable_swarmui", True))
|
||||||
|
rt = normalize_runtime(cfg.llm_runtime)
|
||||||
|
if not swarm and rt == "none":
|
||||||
|
raise CloudError(
|
||||||
|
"llm-only: нужен LLM_RUNTIME=ollama|llamacpp (или --ollama / --llamacpp)"
|
||||||
|
)
|
||||||
|
|
||||||
restart = bool(update)
|
restart = bool(update)
|
||||||
|
if swarm:
|
||||||
try:
|
try:
|
||||||
if seed_extensions(cfg, host, log, update=update):
|
if seed_extensions(cfg, host, log, update=update):
|
||||||
restart = True
|
restart = True
|
||||||
@@ -454,19 +465,43 @@ def provision_vm(
|
|||||||
log(f"autocomplete: {exc}")
|
log(f"autocomplete: {exc}")
|
||||||
seed_civitai(cfg, host, log)
|
seed_civitai(cfg, host, log)
|
||||||
push_tree(cfg, host, cfg.local_models_dir, f"{DATA}/Models", log, models=True)
|
push_tree(cfg, host, cfg.local_models_dir, f"{DATA}/Models", log, models=True)
|
||||||
push_tree(cfg, host, cfg.local_wildcards_dir, f"{DATA}/Data/Wildcards", log, models=False)
|
push_tree(
|
||||||
push_tree(cfg, host, cfg.local_workflows_dir, f"{DATA}/CustomWorkflows", log, models=False)
|
cfg, host, cfg.local_wildcards_dir, f"{DATA}/Data/Wildcards", log, models=False
|
||||||
|
)
|
||||||
|
push_tree(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
cfg.local_workflows_dir,
|
||||||
|
f"{DATA}/CustomWorkflows",
|
||||||
|
log,
|
||||||
|
models=False,
|
||||||
|
)
|
||||||
if cfg.pull_output:
|
if cfg.pull_output:
|
||||||
pull_tree(cfg, host, f"{DATA}/Output", cfg.local_output_dir, log)
|
pull_tree(cfg, host, f"{DATA}/Output", cfg.local_output_dir, log)
|
||||||
|
else:
|
||||||
|
log("SwarmUI: skip (llm-only)")
|
||||||
|
run_ssh(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
"sudo -n systemctl stop swarmui 2>/dev/null; "
|
||||||
|
"sudo -n systemctl disable swarmui 2>/dev/null || true; "
|
||||||
|
"echo llm-only | sudo -n tee /mnt/swarm_data/.gpu-rent-llm-only >/dev/null",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
probe_gpu(cfg, host, log)
|
probe_gpu(cfg, host, log)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"GPU probe: {exc}")
|
log(f"GPU probe: {exc}")
|
||||||
|
|
||||||
|
if swarm:
|
||||||
ensure_swarmui_running(cfg, host, log, restart=restart)
|
ensure_swarmui_running(cfg, host, log, restart=restart)
|
||||||
|
run_ssh(
|
||||||
from gpu_rent.state import load_state, save_state
|
cfg,
|
||||||
|
host,
|
||||||
|
"sudo -n rm -f /mnt/swarm_data/.gpu-rent-llm-only",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provision_llm(cfg, host, log)
|
provision_llm(cfg, host, log)
|
||||||
@@ -474,7 +509,6 @@ def provision_vm(
|
|||||||
st = load_state()
|
st = load_state()
|
||||||
st.notes = dict(st.notes or {})
|
st.notes = dict(st.notes or {})
|
||||||
st.notes["llm_error"] = str(exc)[:500]
|
st.notes["llm_error"] = str(exc)[:500]
|
||||||
# Do not claim success — leave previous notes.llm_runtime or clear to none.
|
|
||||||
st.notes["llm_runtime"] = "none"
|
st.notes["llm_runtime"] = "none"
|
||||||
save_state(st)
|
save_state(st)
|
||||||
raise CloudError(f"LLM runtime: {exc}") from exc
|
raise CloudError(f"LLM runtime: {exc}") from exc
|
||||||
@@ -498,10 +532,8 @@ def provision_vm(
|
|||||||
"⚠ idle-killer НЕ вооружён — GPU может тарифицироваться без авто-stop. "
|
"⚠ idle-killer НЕ вооружён — GPU может тарифицироваться без авто-stop. "
|
||||||
"См. status / docs/setup.md"
|
"См. status / docs/setup.md"
|
||||||
)
|
)
|
||||||
|
if swarm:
|
||||||
log("SwarmUI слушает 127.0.0.1:7801 — gpu-rent tunnel")
|
log("SwarmUI слушает 127.0.0.1:7801 — gpu-rent tunnel")
|
||||||
from gpu_rent.llm_runtime import normalize_runtime
|
|
||||||
|
|
||||||
rt = normalize_runtime(cfg.llm_runtime)
|
|
||||||
if rt == "ollama":
|
if rt == "ollama":
|
||||||
log(f"Ollama API → localhost:{cfg.ollama_local_port} (туннель)")
|
log(f"Ollama API → localhost:{cfg.ollama_local_port} (туннель)")
|
||||||
elif rt == "llamacpp":
|
elif rt == "llamacpp":
|
||||||
|
|||||||
@@ -95,7 +95,26 @@ mkdir -p \
|
|||||||
"${DATA_ROOT}/dlbackend" \
|
"${DATA_ROOT}/dlbackend" \
|
||||||
"${DATA_ROOT}/Extensions" \
|
"${DATA_ROOT}/Extensions" \
|
||||||
"${DATA_ROOT}/DLNodes" \
|
"${DATA_ROOT}/DLNodes" \
|
||||||
"${DATA_ROOT}/CustomWorkflows"
|
"${DATA_ROOT}/CustomWorkflows" \
|
||||||
|
"${DATA_ROOT}/ollama" \
|
||||||
|
"${DATA_ROOT}/llamacpp/models"
|
||||||
|
|
||||||
|
# LLM-only: data disk + tools, no SwarmUI clone / unit.
|
||||||
|
if [[ "${GPU_RENT_SKIP_SWARMUI:-0}" == "1" ]]; then
|
||||||
|
chown -R "${SWARM_USER}:${SWARM_USER}" "$DATA_ROOT"
|
||||||
|
date -u +"%Y-%m-%dT%H:%M:%SZ" >"$MARKER_DATA"
|
||||||
|
date -u +"%Y-%m-%dT%H:%M:%SZ" >"${DATA_ROOT}/.gpu-rent-llm-only"
|
||||||
|
chown "${SWARM_USER}:${SWARM_USER}" "$MARKER_DATA" "${DATA_ROOT}/.gpu-rent-llm-only"
|
||||||
|
systemctl stop swarmui 2>/dev/null || true
|
||||||
|
systemctl disable swarmui 2>/dev/null || true
|
||||||
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
|
nvidia-smi -L || true
|
||||||
|
fi
|
||||||
|
log "bootstrap ok (llm-only; без SwarmUI)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${DATA_ROOT}/.gpu-rent-llm-only" 2>/dev/null || true
|
||||||
|
|
||||||
if [[ ! -d "${SWARM_ROOT}/.git" ]]; then
|
if [[ ! -d "${SWARM_ROOT}/.git" ]]; then
|
||||||
log "clone SwarmUI -> ${SWARM_ROOT}"
|
log "clone SwarmUI -> ${SWARM_ROOT}"
|
||||||
|
|||||||
@@ -236,6 +236,9 @@ def main() -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
swarm_url = str(creds.get("swarm_url") or "http://127.0.0.1:7801")
|
swarm_url = str(creds.get("swarm_url") or "http://127.0.0.1:7801")
|
||||||
|
if (DATA / ".gpu-rent-llm-only").is_file():
|
||||||
|
busy, detail = False, "llm-only (swarm skip)"
|
||||||
|
else:
|
||||||
busy, detail = swarm_busy(swarm_url)
|
busy, detail = swarm_busy(swarm_url)
|
||||||
if busy:
|
if busy:
|
||||||
write_ts(IDLE_SINCE, None)
|
write_ts(IDLE_SINCE, None)
|
||||||
|
|||||||
+14
-11
@@ -86,27 +86,26 @@ def _bind_access(
|
|||||||
log(f"SSH {cfg.ssh_user}@{ip}")
|
log(f"SSH {cfg.ssh_user}@{ip}")
|
||||||
state.phase = "bootstrapping"
|
state.phase = "bootstrapping"
|
||||||
save_state(state)
|
save_state(state)
|
||||||
if update:
|
swarm = bool(getattr(cfg, "enable_swarmui", True))
|
||||||
|
if swarm and update:
|
||||||
active = run_ssh(cfg, ip, "systemctl is-active swarmui 2>/dev/null || true", check=False).strip()
|
active = run_ssh(cfg, ip, "systemctl is-active swarmui 2>/dev/null || true", check=False).strip()
|
||||||
if active == "active":
|
if active == "active":
|
||||||
log("systemctl stop swarmui перед git update")
|
log("systemctl stop swarmui перед git update")
|
||||||
run_ssh(cfg, ip, "sudo -n systemctl stop swarmui", timeout=120, check=False)
|
run_ssh(cfg, ip, "sudo -n systemctl stop swarmui", timeout=120, check=False)
|
||||||
# Skip apt-heavy bootstrap when the VM already finished first-boot.
|
# Skip apt-heavy bootstrap when the VM already finished first-boot.
|
||||||
marker = run_ssh(
|
if swarm:
|
||||||
cfg,
|
marker_cmd = "test -f /opt/swarmui/.gpu-rent-bootstrapped && echo yes || echo no"
|
||||||
ip,
|
else:
|
||||||
"test -f /opt/swarmui/.gpu-rent-bootstrapped && echo yes || echo no",
|
marker_cmd = "test -f /mnt/swarm_data/.gpu-rent-ready && echo yes || echo no"
|
||||||
check=False,
|
marker = run_ssh(cfg, ip, marker_cmd, check=False).strip()
|
||||||
).strip()
|
|
||||||
if marker == "yes":
|
if marker == "yes":
|
||||||
# VM marker survives stop; local bootstrapped is cleared on stop — still light.
|
|
||||||
if not state.bootstrapped:
|
if not state.bootstrapped:
|
||||||
log("маркер bootstrap на VM — лёгкий проход (локальный bootstrapped был сброшен)")
|
log("маркер bootstrap на VM — лёгкий проход (локальный bootstrapped был сброшен)")
|
||||||
else:
|
else:
|
||||||
log("bootstrap уже на VM — лёгкий проход (без apt)")
|
log("bootstrap уже на VM — лёгкий проход (без apt)")
|
||||||
run_bootstrap(cfg, ip, log, update=update, light=True)
|
run_bootstrap(cfg, ip, log, update=update and swarm, light=True)
|
||||||
else:
|
else:
|
||||||
run_bootstrap(cfg, ip, log, update=update, light=False)
|
run_bootstrap(cfg, ip, log, update=update and swarm, light=False)
|
||||||
provision_vm(
|
provision_vm(
|
||||||
cfg,
|
cfg,
|
||||||
ip,
|
ip,
|
||||||
@@ -115,17 +114,19 @@ def _bind_access(
|
|||||||
server_id=getattr(server, "id", None) or state.server_id,
|
server_id=getattr(server, "id", None) or state.server_id,
|
||||||
update=update,
|
update=update,
|
||||||
)
|
)
|
||||||
|
if swarm:
|
||||||
try:
|
try:
|
||||||
wait_backend_idle(cfg, ip, log)
|
wait_backend_idle(cfg, ip, log)
|
||||||
except CloudError as exc:
|
except CloudError as exc:
|
||||||
log(f"ready: {exc}")
|
log(f"ready: {exc}")
|
||||||
# Comfy venv + Backends.fds exist after Idle — sage/triton + ExtraArgs.
|
|
||||||
try:
|
try:
|
||||||
if tune_swarm_perf(cfg, ip, log):
|
if tune_swarm_perf(cfg, ip, log):
|
||||||
log("systemctl restart swarmui (perf ExtraArgs)")
|
log("systemctl restart swarmui (perf ExtraArgs)")
|
||||||
run_ssh(cfg, ip, "sudo -n systemctl restart swarmui", timeout=120)
|
run_ssh(cfg, ip, "sudo -n systemctl restart swarmui", timeout=120)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"perf tune: {exc}")
|
log(f"perf tune: {exc}")
|
||||||
|
else:
|
||||||
|
log("ready: llm-only (без ожидания SwarmUI Idle)")
|
||||||
try:
|
try:
|
||||||
ensure_boot_snapshot(
|
ensure_boot_snapshot(
|
||||||
conn,
|
conn,
|
||||||
@@ -138,6 +139,8 @@ def _bind_access(
|
|||||||
notify_ready(cfg, log)
|
notify_ready(cfg, log)
|
||||||
state.bootstrapped = True
|
state.bootstrapped = True
|
||||||
state.phase = "ready_cloud"
|
state.phase = "ready_cloud"
|
||||||
|
state.notes = dict(state.notes or {})
|
||||||
|
state.notes["enable_swarmui"] = swarm
|
||||||
save_state(state)
|
save_state(state)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|||||||
+18
-7
@@ -72,16 +72,18 @@ def decide_watch(status: str | None, tunnel_alive: bool) -> WatchDecision:
|
|||||||
|
|
||||||
|
|
||||||
def tunnel_forwards(cfg: Config) -> list[tuple[int, int]]:
|
def tunnel_forwards(cfg: Config) -> list[tuple[int, int]]:
|
||||||
"""List of (local_port, remote_port). SwarmUI always; LLM if configured.
|
"""List of (local_port, remote_port). SwarmUI if enabled; LLM if configured."""
|
||||||
|
pairs: list[tuple[int, int]] = []
|
||||||
Prefer live config (`LLM_RUNTIME`) over stale state.notes.
|
if bool(getattr(cfg, "enable_swarmui", True)):
|
||||||
"""
|
pairs.append((cfg.swarmui_local_port, 7801))
|
||||||
pairs = [(cfg.swarmui_local_port, 7801)]
|
|
||||||
runtime = normalize_runtime(cfg.llm_runtime)
|
runtime = normalize_runtime(cfg.llm_runtime)
|
||||||
if runtime == "ollama":
|
if runtime == "ollama":
|
||||||
pairs.append((cfg.ollama_local_port, 11434))
|
pairs.append((cfg.ollama_local_port, 11434))
|
||||||
elif runtime == "llamacpp":
|
elif runtime == "llamacpp":
|
||||||
pairs.append((cfg.llamacpp_local_port, 8080))
|
pairs.append((cfg.llamacpp_local_port, 8080))
|
||||||
|
if not pairs:
|
||||||
|
# Failsafe: at least SwarmUI port so tunnel isn't empty.
|
||||||
|
pairs.append((cfg.swarmui_local_port, 7801))
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
@@ -181,14 +183,23 @@ def run_tunnel(
|
|||||||
log("watchdog: EXPIRED → unshelve + reconnect")
|
log("watchdog: EXPIRED → unshelve + reconnect")
|
||||||
|
|
||||||
server = _start_forwarder(cfg, current_host, forwards)
|
server = _start_forwarder(cfg, current_host, forwards)
|
||||||
swarm_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
swarm_on = bool(getattr(cfg, "enable_swarmui", True))
|
||||||
|
runtime = normalize_runtime(cfg.llm_runtime)
|
||||||
|
if swarm_on:
|
||||||
|
open_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
||||||
|
elif runtime == "ollama":
|
||||||
|
open_url = f"http://127.0.0.1:{cfg.ollama_local_port}"
|
||||||
|
elif runtime == "llamacpp":
|
||||||
|
open_url = f"http://127.0.0.1:{cfg.llamacpp_local_port}"
|
||||||
|
else:
|
||||||
|
open_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
||||||
|
|
||||||
from gpu_rent.access_card import print_access_card
|
from gpu_rent.access_card import print_access_card
|
||||||
|
|
||||||
print_access_card(cfg, tunneled=True, host=current_host)
|
print_access_card(cfg, tunneled=True, host=current_host)
|
||||||
|
|
||||||
if open_browser:
|
if open_browser:
|
||||||
webbrowser.open(swarm_url)
|
webbrowser.open(open_url)
|
||||||
|
|
||||||
state = load_state()
|
state = load_state()
|
||||||
state.phase = "ready_tunneled"
|
state.phase = "ready_tunneled"
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from gpu_rent.config import parse_enable_swarmui
|
||||||
|
from gpu_rent.tunnel import tunnel_forwards
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_enable_swarmui_workload(monkeypatch):
|
||||||
|
monkeypatch.delenv("WORKLOAD", raising=False)
|
||||||
|
assert parse_enable_swarmui(None) is True
|
||||||
|
assert parse_enable_swarmui("false") is False
|
||||||
|
monkeypatch.setenv("WORKLOAD", "llm")
|
||||||
|
assert parse_enable_swarmui("true") is False
|
||||||
|
monkeypatch.setenv("WORKLOAD", "both")
|
||||||
|
assert parse_enable_swarmui("false") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_tunnel_forwards_llm_only(monkeypatch):
|
||||||
|
class Cfg:
|
||||||
|
swarmui_local_port = 17801
|
||||||
|
llm_runtime = "llamacpp"
|
||||||
|
enable_swarmui = False
|
||||||
|
ollama_local_port = 17811
|
||||||
|
llamacpp_local_port = 17812
|
||||||
|
|
||||||
|
monkeypatch.setattr("gpu_rent.tunnel.load_state", lambda: type("S", (), {"notes": {}})())
|
||||||
|
assert tunnel_forwards(Cfg()) == [(17812, 8080)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_links_llm_only(monkeypatch):
|
||||||
|
from gpu_rent.access_card import collect_access_links, mcp_snippet_lines
|
||||||
|
|
||||||
|
class Cfg:
|
||||||
|
swarmui_local_port = 17801
|
||||||
|
llm_runtime = "llamacpp"
|
||||||
|
enable_swarmui = False
|
||||||
|
ollama_local_port = 17811
|
||||||
|
llamacpp_local_port = 17812
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"gpu_rent.access_card.load_state",
|
||||||
|
lambda: type("S", (), {"notes": {}})(),
|
||||||
|
)
|
||||||
|
labels = [x.label for x in collect_access_links(Cfg(), tunneled=True)]
|
||||||
|
assert "SwarmUI UI" not in labels
|
||||||
|
assert "llama.cpp" in labels
|
||||||
|
assert mcp_snippet_lines(Cfg())[0].startswith("#")
|
||||||
Reference in New Issue
Block a user