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 |
|
||||
| `doctor` | Preflight без create |
|
||||
| `flavors` / `dry-run` | Что выберет / план без денег |
|
||||
| `up` / `up --yes` | GPU + seed + туннель |
|
||||
| `up --ollama` | + Ollama (см. [docs/llm.md](docs/llm.md)) |
|
||||
| `up` / `up --yes` | GPU + seed + туннель; без `--yes` — меню Selectel/LLM |
|
||||
| `up --ollama` / `--llamacpp` | + LLM (см. [docs/llm.md](docs/llm.md)) |
|
||||
| `up --llm-only --llamacpp` | только LLM, без SwarmUI |
|
||||
| `tunnel` / `open` | Снова UI / браузер |
|
||||
| `hold` / `status` | Пауза killer / состояние |
|
||||
| `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) |
|
||||
| `gpu-rent.vars` | из example — несекретные дефолты, лаунчер |
|
||||
| `models.yaml` / `extensions.yaml` | манифесты (из `*.example.yaml`) |
|
||||
| `ollama-models.yaml` / `llamacpp-models.yaml` | LLM (из example; gitignore) |
|
||||
| `Models/` … | локальный push на `up` |
|
||||
| `.gpu-rent/` | state, SSH-ключ (gitignore) |
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ CLI поднимает прерываемый GPU в Selectel, держит Swar
|
||||
| Word-list промптов | [autocomplete.md](autocomplete.md) |
|
||||
| Push/pull папок | [local-folders.md](local-folders.md) |
|
||||
| Ollama / llama.cpp | [llm.md](llm.md) |
|
||||
| Capture ссылок с VM | [cli.md](cli.md) (`capture`) + [models.md](models.md) |
|
||||
| Как устроены диски и killer | [architecture.md](architecture.md) |
|
||||
| Контракт Selectel | [selectel.md](selectel.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`, пути |
|
||||
| `state` | JSON сессии: ids, фаза, timestamps |
|
||||
| `os_client` / `cloud` / `pools` | openstacksdk, ресурсы, скан пулов |
|
||||
| `inventory` | Flavors/images, квоты, фоллбек flavor |
|
||||
| `inventory` / `ux` | Flavors, квоты; preview / ServerPlan (flavor/disk/spot) |
|
||||
| `session` | `cmd_up` / `cmd_stop` / adopt |
|
||||
| `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 |
|
||||
| `sync_files` | SFTP `Models` / Wildcards / workflows / Output |
|
||||
| `notify` | Toast/звук при backend Idle |
|
||||
| `tunnel` | sshtunnel + Nova EXPIRED watchdog |
|
||||
| `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-файл |
|
||||
| `ready` / `snapshot` | Idle backend + boot snapshot |
|
||||
| `access_card` | URL / MCP panel после ready |
|
||||
|
||||
Каталог сервисов — из 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 flavors` | Скан `SCAN_POOLS` × `FLAVOR_PREFERENCE`, список в текущем регионе |
|
||||
| `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 --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-tunnel` | Только облако |
|
||||
| `gpu-rent up --no-spot` | Не preemptible |
|
||||
@@ -135,7 +137,7 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
|
||||
| --- | --- |
|
||||
| `.env` | Секреты и OpenStack (`OS_*`, токены). Не в git |
|
||||
| `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`)
|
||||
|
||||
@@ -148,6 +150,9 @@ GPU_RENT_DEFAULT_ARGS=up --yes
|
||||
|
||||
UPDATE_GIT=true
|
||||
LLM_RUNTIME=none
|
||||
# DATA_VOLUME_SIZE_GB=100
|
||||
# DEFAULT_FLAVOR_ID=
|
||||
# DEFAULT_SPOT=true
|
||||
```
|
||||
|
||||
### Основные переменные (`.env` / vars)
|
||||
|
||||
+4
-4
@@ -23,10 +23,10 @@ GPU в облаке дорогой. Веса для генерации карт
|
||||
- Push из `./Models`, `./Wildcards`, `./CustomWorkflows`, если папки не пустые; optional pull `./Output`.
|
||||
- Первый clone git-реп расширений SwarmUI и ComfyUI nodes из `extensions.yaml`.
|
||||
- Autocomplete: word-list в `Data/Autocompletions` до старта UI, на каждом `up` проверка новой версии.
|
||||
- `doctor` до create; фоллбек flavor; `hold` для idle-killer; toast когда backend Idle; `open` на 17801.
|
||||
- Opt-in LLM (Ollama / llama.cpp) через `setup` / `--ollama` / `LLM_RUNTIME`.
|
||||
- `status`: диск, ₽/час, окно preempt 24 ч, состояние killer.
|
||||
- Snapshot boot-диска после первого удачного bootstrap.
|
||||
- `doctor` до create; фоллбек flavor; интерактивный выбор flavor/диска; `hold`; toast Idle; `open` на 17801.
|
||||
- Opt-in LLM (Ollama / llama.cpp) через `setup` / флаги / `LLM_RUNTIME`; `capture` ссылок с VM.
|
||||
- `status`: диск, окно preempt 24 ч, killer / LLM.
|
||||
- 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) |
|
||||
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
||||
| 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 |
|
||||
| Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` |
|
||||
| Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь |
|
||||
| Льгота после 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` остаётся для повторного входа |
|
||||
| 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` — не тянуть |
|
||||
| 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 |
|
||||
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
|
||||
| SSH | CLI генерирует `<repo>/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
|
||||
|
||||
+59
-54
@@ -1,37 +1,52 @@
|
||||
# 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
|
||||
gpu-rent setup
|
||||
```
|
||||
|
||||
Выбери `ollama` или `llamacpp`. Для **обоих** спросит пресет моделей. Значение пишется в `gpu-rent.vars` (`LLM_RUNTIME=…`).
|
||||
Меню: runtime → пресет моделей → опционально local-watchdog. Пишет `LLM_RUNTIME` в `gpu-rent.vars`.
|
||||
|
||||
### Вариант B — флаг на `up`
|
||||
### B — флаг на `up`
|
||||
|
||||
```text
|
||||
gpu-rent up --yes --ollama
|
||||
gpu-rent up --yes --llm llamacpp
|
||||
gpu-rent up --yes --llm-only --llamacpp # без SwarmUI
|
||||
```
|
||||
|
||||
### Вариант C — вручную в vars
|
||||
С `--yes` пресеты не спрашивает (берёт существующий yaml / example).
|
||||
|
||||
### C — vars вручную
|
||||
|
||||
```env
|
||||
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
|
||||
```
|
||||
|
||||
Клиент Ollama:
|
||||
|
||||
```text
|
||||
# Windows PowerShell
|
||||
```powershell
|
||||
$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.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, меньше отказов |
|
||||
| light | `qwen2.5:3b` | быстрее, слабее |
|
||||
| stock | `qwen2.5:7b` | официальный |
|
||||
| alt | `richardyoung/qwen2.5-7b-instruct-abliterated` | другой abliterate |
|
||||
| empty | `[]` | только runtime, pull руками |
|
||||
| 1 | **recommended** | `huihui_ai/qwen2.5-abliterate:7b` — RU/EN, ~5 GB |
|
||||
| 2 | light | `qwen2.5:3b` |
|
||||
| 3 | stock | `qwen2.5:7b` |
|
||||
| 4 | alt | другой abliterate 7B |
|
||||
| 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 |
|
||||
| mid (16–23) | on* | 5m | q8_0 | 10 GiB |
|
||||
| high (24–47) | on* | 15m | q8_0 | 14 GiB |
|
||||
| ultra (≥48) | on* | 30m | q8_0 | 20 GiB |
|
||||
|
||||
\*Flash на Ampere+ (compute ≥ 8.0). Всегда `NUM_PARALLEL=1`, `MAX_LOADED_MODELS=1`.
|
||||
|
||||
Файл: `/mnt/swarm_data/.gpu-rent-ollama.env` (пересоздаётся на каждом install Ollama).
|
||||
\*Flash на Ampere+ (compute ≥ 8.0). `NUM_PARALLEL=1`, `MAX_LOADED_MODELS=1`. Env: `/mnt/swarm_data/.gpu-rent-ollama.env`.
|
||||
|
||||
---
|
||||
|
||||
## llama.cpp
|
||||
|
||||
Манифест GGUF:
|
||||
|
||||
| Файл | Роль |
|
||||
| --- | --- |
|
||||
| `llamacpp-models.example.yaml` | шаблон в git |
|
||||
| `llamacpp-models.yaml` | URL на `.gguf` (gitignore) |
|
||||
| `llamacpp-models.example.yaml` | шаблон |
|
||||
| `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) |
|
||||
| light | Qwen2.5 3B Instruct Q4_K_M |
|
||||
| stock | официальный Qwen2.5 7B Instruct Q4_K_M |
|
||||
| empty | только runtime |
|
||||
| # | ключ | что |
|
||||
| --- | --- | --- |
|
||||
| 1 | **recommended** | Qwen2.5 7B abliterate Q4_K_M (~4.7 GB) |
|
||||
| 2 | light | Qwen2.5 3B Instruct Q4_K_M |
|
||||
| 3 | stock | официальный 7B Instruct Q4_K_M |
|
||||
| 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
|
||||
|
||||
Busy (не гасить GPU), если:
|
||||
Busy (не гасить GPU):
|
||||
|
||||
- идёт `ollama pull` (маркер младше ~45 мин; старше — сбрасывается);
|
||||
- в Ollama есть загруженная модель;
|
||||
- llama.cpp занимает слоты.
|
||||
- `ollama pull` (маркер младше ~45 мин; старше сбрасывается);
|
||||
- загруженная модель в Ollama;
|
||||
- llama.cpp: слоты заняты.
|
||||
|
||||
Ошибка установки LLM на `up` — **fail** (не тихий лог): почини или поставь `LLM_RUNTIME=none`.
|
||||
Ошибка установки LLM на `up` — **fail** (не тихий лог).
|
||||
|
||||
---
|
||||
|
||||
## Supply-chain (опциональный pin)
|
||||
|
||||
По умолчанию install-скрипты тянут upstream **без pin** (Ollama: `curl|sh`; llama.cpp: GitHub `latest`) — риск подмены. В логе будет `WARN`.
|
||||
|
||||
Жёстче — задай env на VM / при bootstrap:
|
||||
По умолчанию install тянет upstream без pin (`WARN` в логе). Жёстче:
|
||||
|
||||
```bash
|
||||
OLLAMA_VERSION=0.6.5
|
||||
OLLAMA_SHA256=<sha256 of ollama-linux-amd64.tgz>
|
||||
|
||||
LLAMACPP_TAG=b4690
|
||||
# или LLAMACPP_ASSET_URL=... + LLAMACPP_SHA256=...
|
||||
```
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
Продуктовые развилки закрыты — см. [decisions.md](decisions.md).
|
||||
|
||||
Ниже — что **подтвердить на spike** (живой GPU), не в споре:
|
||||
Подтвердить **на spike** (живой GPU), не в споре:
|
||||
|
||||
- Качалка модели в UI: отдельного poll-API нет → v1 считает busy через очередь / loading / backend≠idle; иначе пользователь жмёт `hold`.
|
||||
- Реальная цена 100 GB сетевого диска и 1×4090 preemptible ₽/час в твоём сегменте (в OpenStack API нет — только панель).
|
||||
- Имя GPU-образа (без Docker) и flavor id в твоём пуле (в git не класть); какие из `FLAVOR_PREFERENCE` реально есть.
|
||||
- Application credential с **узкими** access_rules (DELETE/GET только этого `server_id`) на Selectel: если Keystone отвергает rules — CLI **fail closed** (idle-killer не вооружается unrestricted). Нужно проверить на spike и при необходимости править права / путь.
|
||||
- Snapshot attached boot volume после Idle: время и можно ли сразу create from snapshot.
|
||||
- Качалка модели в UI: отдельного poll-API нет → busy через очередь / loading / backend≠idle; иначе `hold`.
|
||||
- Реальная цена data-диска и 1×4090 preemptible ₽/час в твоём сегменте (в API нет — панель).
|
||||
- Имя GPU-образа Driver 580 и живые flavor id в пуле (в git не класть).
|
||||
- App cred с узкими access_rules на Selectel: код fail closed; на spike убедиться, что Keystone принимает rules.
|
||||
- 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] `NOTIFY_READY`: Windows toast + звук при backend Idle
|
||||
- [x] Пользовательский README поверх `docs/`
|
||||
- [x] Opt-in LLM + `setup`; нумерованные меню; interactive Selectel plan (flavor/disk/spot)
|
||||
- [x] Цветной CLI-лог; `capture` ссылок с VM
|
||||
- [x] Local-watchdog; access-card
|
||||
|
||||
## Вне скоупа v1
|
||||
|
||||
|
||||
+16
-6
@@ -49,6 +49,7 @@ chmod +x gpu-rent.sh
|
||||
- `env.example` → `.env`
|
||||
- `models.example.yaml` → `models.yaml`
|
||||
- `extensions.example.yaml` → `extensions.yaml`
|
||||
- `ollama-models.example.yaml` / `llamacpp-models.example.yaml` → соответствующие yaml (лаунчер / setup)
|
||||
- `gpu-rent.vars.example` → `gpu-rent.vars`
|
||||
|
||||
Секреты только в `<repo>/.env` и runtime в `<repo>/.gpu-rent/` (оба в `.gitignore`).
|
||||
@@ -59,7 +60,7 @@ chmod +x gpu-rent.sh
|
||||
.\gpu-rent.ps1 setup
|
||||
```
|
||||
|
||||
Спросит LLM (none/ollama/llamacpp) и local-watchdog. Можно пропустить и настроить позже.
|
||||
Спросит (нумерованные меню): LLM runtime → пресет моделей → local-watchdog. Можно пропустить и настроить позже (`LLM_RUNTIME` / `up`).
|
||||
|
||||
Для разработки / pytest:
|
||||
|
||||
@@ -215,15 +216,23 @@ copy models.example.yaml models.yaml
|
||||
|
||||
Первый прогон долгий (образ, SwarmUI, Comfy, seed): ориентир **20–40 минут**. Не закрывай терминал посередине bootstrap.
|
||||
|
||||
Без вопросов (CI / двойной клик):
|
||||
|
||||
```powershell
|
||||
.\gpu-rent.ps1 up --yes
|
||||
```
|
||||
|
||||
Интерактивно (меню LLM, пресет, **flavor / диск / preemptible**, confirm):
|
||||
|
||||
```powershell
|
||||
.\gpu-rent.ps1 up
|
||||
```
|
||||
|
||||
Что произойдёт:
|
||||
|
||||
1. Снова короткий doctor (кратко; полный — `up -v`).
|
||||
2. Create/unshelve preemptible GPU + диски.
|
||||
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок.
|
||||
1. Короткий doctor (полный — `up -v`).
|
||||
2. Create/unshelve GPU + диски (после confirm).
|
||||
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок; optional LLM.
|
||||
4. Туннель на `localhost:17801`, access-card с URL / MCP.
|
||||
5. Процесс ждёт: **Ctrl+C** закрывает только туннель, GPU остаётся.
|
||||
|
||||
@@ -233,9 +242,10 @@ copy models.example.yaml models.yaml
|
||||
| --- | --- |
|
||||
| `--no-tunnel` | только облако; UI потом: `tunnel --open` |
|
||||
| `--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) тариф |
|
||||
| `--flavor ID` | явный flavor, без фоллбека |
|
||||
| `--flavor ID` | явный flavor (пропускает меню flavor) |
|
||||
|
||||
Двойной клик без аргументов: в `gpu-rent.vars` задай `GPU_RENT_DEFAULT_ARGS=up --yes`. Сам `gpu-rent` без args показывает **help**, не поднимает GPU.
|
||||
|
||||
|
||||
+3
-2
@@ -103,12 +103,13 @@
|
||||
|
||||
```powershell
|
||||
.\gpu-rent.ps1 up --yes --ollama
|
||||
# или интерактивно: up → меню runtime/пресет
|
||||
```
|
||||
|
||||
| Шаг | OK? |
|
||||
| --- | --- |
|
||||
| Ollama на :17811 через туннель | |
|
||||
| `ollama pull` из манифеста | |
|
||||
| Ollama :17811 или llama.cpp :17812 через туннель | |
|
||||
| pull / GGUF из манифеста | |
|
||||
| `LLM_RUNTIME=none` гасит unit на следующем up | |
|
||||
|
||||
См. [llm.md](llm.md).
|
||||
|
||||
+6
-1
@@ -38,10 +38,15 @@ AUTOCOMPLETE_GITHUB_REF=main
|
||||
AUTOCOMPLETE_FILENAME=danbooru.csv
|
||||
|
||||
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
|
||||
OLLAMA_LOCAL_PORT=17811
|
||||
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
|
||||
UPDATE_GIT=true
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
# SCAN_POOLS=ru-6,ru-7
|
||||
# FLAVOR_PREFERENCE=4090-24,4090-48,a5000,a100-40
|
||||
# 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
|
||||
# OLLAMA_LOCAL_PORT=17811
|
||||
# LLAMACPP_LOCAL_PORT=17812
|
||||
|
||||
@@ -40,17 +40,19 @@ def resolve_llm_runtime(cfg: Config) -> str:
|
||||
|
||||
def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
||||
"""Build the list of user-facing endpoints (unit-tested)."""
|
||||
port = cfg.swarmui_local_port
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
links: list[AccessLink] = []
|
||||
swarm = bool(getattr(cfg, "enable_swarmui", True))
|
||||
if tunneled:
|
||||
links.extend(
|
||||
[
|
||||
AccessLink("SwarmUI UI", base, "браузер"),
|
||||
AccessLink("SwarmUI API", f"{base}/API/", "HTTP JSON"),
|
||||
AccessLink("SwarmUI MCP", f"{base}/mcp", "Cursor mcp.json"),
|
||||
]
|
||||
)
|
||||
if swarm:
|
||||
port = cfg.swarmui_local_port
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
links.extend(
|
||||
[
|
||||
AccessLink("SwarmUI UI", base, "браузер"),
|
||||
AccessLink("SwarmUI API", f"{base}/API/", "HTTP JSON"),
|
||||
AccessLink("SwarmUI MCP", f"{base}/mcp", "Cursor mcp.json"),
|
||||
]
|
||||
)
|
||||
runtime = resolve_llm_runtime(cfg)
|
||||
if runtime == "ollama":
|
||||
o = cfg.ollama_local_port
|
||||
@@ -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"),
|
||||
]
|
||||
)
|
||||
if not links:
|
||||
links.append(
|
||||
AccessLink(
|
||||
"Туннель",
|
||||
"gpu-rent tunnel",
|
||||
"нет сервисов — ENABLE_SWARMUI / LLM_RUNTIME",
|
||||
)
|
||||
)
|
||||
else:
|
||||
links.append(
|
||||
AccessLink(
|
||||
@@ -94,6 +104,8 @@ def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
||||
|
||||
|
||||
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
|
||||
return [
|
||||
"{",
|
||||
|
||||
@@ -24,14 +24,18 @@ def run_bootstrap(
|
||||
update: bool = True,
|
||||
light: bool = False,
|
||||
) -> 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)")
|
||||
else:
|
||||
log("bootstrap SwarmUI на VM (идемпотентно, без Docker)")
|
||||
if update:
|
||||
log("git update: SwarmUI on")
|
||||
else:
|
||||
log("git update: SwarmUI off (--no-update)")
|
||||
if not skip_swarm:
|
||||
if update:
|
||||
log("git update: SwarmUI on")
|
||||
else:
|
||||
log("git update: SwarmUI off (--no-update)")
|
||||
script = bootstrap_script()
|
||||
out = run_script_sudo(
|
||||
cfg,
|
||||
@@ -43,9 +47,13 @@ def run_bootstrap(
|
||||
"SWARM_USER": cfg.ssh_user,
|
||||
"GPU_RENT_UPDATE_GIT": "1" if update else "0",
|
||||
"GPU_RENT_BOOTSTRAP_LIGHT": "1" if light else "0",
|
||||
"GPU_RENT_SKIP_SWARMUI": "1" if skip_swarm else "0",
|
||||
},
|
||||
log=log,
|
||||
)
|
||||
if "bootstrap ok" not in out:
|
||||
raise CloudError(f"bootstrap не подтвердил успех:\n{out[-800:]}")
|
||||
log("диски и systemd unit готовы; дальше seed, потом start swarmui")
|
||||
if skip_swarm:
|
||||
log("data disk готов — дальше LLM")
|
||||
else:
|
||||
log("диски и systemd unit готовы; дальше seed, потом start swarmui")
|
||||
|
||||
+95
-21
@@ -303,10 +303,10 @@ def status() -> None:
|
||||
def open(
|
||||
llm: bool = typer.Option(False, "--llm", help="Открыть LLM API URL вместо SwarmUI"),
|
||||
) -> None:
|
||||
"""Открыть браузер на SwarmUI :17801 (или --llm на Ollama/llama.cpp)."""
|
||||
"""Открыть браузер на SwarmUI :17801 (или --llm / llm-only на Ollama/llama.cpp)."""
|
||||
cfg = load_config(require_auth=False)
|
||||
if llm:
|
||||
from gpu_rent.llm_runtime import normalize_runtime
|
||||
use_llm = llm or not bool(getattr(cfg, "enable_swarmui", True))
|
||||
if use_llm:
|
||||
from gpu_rent.access_card import resolve_llm_runtime
|
||||
|
||||
runtime = resolve_llm_runtime(cfg)
|
||||
@@ -398,18 +398,25 @@ def up(
|
||||
),
|
||||
ollama: bool = typer.Option(False, "--ollama", help="То же что --llm ollama"),
|
||||
llamacpp: bool = typer.Option(False, "--llamacpp", help="То же что --llm llamacpp"),
|
||||
no_swarm: bool = typer.Option(
|
||||
False,
|
||||
"--no-swarm",
|
||||
"--llm-only",
|
||||
help="Только LLM на GPU, без установки SwarmUI",
|
||||
),
|
||||
) -> None:
|
||||
"""Create/unshelve GPU, bootstrap SwarmUI, по умолчанию туннель на :17801."""
|
||||
"""Create/unshelve GPU; SwarmUI и/или LLM; по умолчанию туннель."""
|
||||
try:
|
||||
from dataclasses import replace
|
||||
|
||||
from gpu_rent.llm_runtime import (
|
||||
append_vars_llm_runtime,
|
||||
decide_runtime,
|
||||
ensure_ollama_manifest_from_example,
|
||||
write_ollama_models_preset,
|
||||
)
|
||||
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()
|
||||
code = _print_checks(checks, quiet=not verbose)
|
||||
@@ -427,33 +434,93 @@ def up(
|
||||
except ValueError as exc:
|
||||
raise GpuRentError(str(exc)) from exc
|
||||
|
||||
enable_swarm = False if no_swarm else cfg.enable_swarmui
|
||||
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 (
|
||||
llamacpp_preset_menu,
|
||||
llm_runtime_menu,
|
||||
ollama_preset_menu,
|
||||
workload_menu,
|
||||
)
|
||||
from gpu_rent.prompts import prompt_menu
|
||||
|
||||
def _ask(msg: str, default: str = "") -> str:
|
||||
return typer.prompt(msg, default=default)
|
||||
|
||||
default_stack = (
|
||||
"llm"
|
||||
if not cfg.enable_swarmui
|
||||
else ("both" if runtime != "none" else "swarm")
|
||||
)
|
||||
try:
|
||||
choice = prompt_menu(
|
||||
"LLM рядом со SwarmUI",
|
||||
llm_runtime_menu(),
|
||||
default="none",
|
||||
stack = prompt_menu(
|
||||
"Что поднять на GPU",
|
||||
workload_menu(),
|
||||
default=default_stack,
|
||||
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 runtime != "none" and typer.confirm("Запомнить LLM_RUNTIME в gpu-rent.vars?", default=True):
|
||||
append_vars_llm_runtime(vars_path(), runtime)
|
||||
|
||||
if stack == "swarm":
|
||||
enable_swarm = True
|
||||
runtime = "none"
|
||||
elif stack == "both":
|
||||
enable_swarm = True
|
||||
if runtime == "none":
|
||||
try:
|
||||
choice = prompt_menu(
|
||||
"LLM runtime",
|
||||
[
|
||||
MenuItem("ollama", "Ollama (+ pull моделей)"),
|
||||
MenuItem("llamacpp", "llama.cpp server (+ GGUF)"),
|
||||
],
|
||||
default="ollama",
|
||||
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
|
||||
else:
|
||||
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":
|
||||
ensure_ollama_manifest_from_example()
|
||||
try:
|
||||
@@ -498,7 +565,6 @@ def up(
|
||||
ollama_preset_menu,
|
||||
write_llamacpp_models_preset,
|
||||
)
|
||||
from gpu_rent.prompts import prompt_menu
|
||||
|
||||
def _ask2(msg: str, default: str = "") -> str:
|
||||
return typer.prompt(msg, default=default)
|
||||
@@ -529,9 +595,17 @@ def up(
|
||||
except ValueError as exc:
|
||||
raise GpuRentError(str(exc)) from exc
|
||||
|
||||
cfg = replace(cfg, llm_runtime=runtime)
|
||||
if runtime != "none":
|
||||
ok(f"LLM runtime: {runtime}")
|
||||
if not enable_swarm and runtime == "none":
|
||||
raise GpuRentError(
|
||||
"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:
|
||||
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"}
|
||||
|
||||
|
||||
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:
|
||||
if value is None or value.strip() == "":
|
||||
return default
|
||||
@@ -84,6 +98,7 @@ class Config:
|
||||
update_git: bool
|
||||
|
||||
llm_runtime: str
|
||||
enable_swarmui: bool
|
||||
ollama_models_manifest: Path
|
||||
llamacpp_models_manifest: Path
|
||||
ollama_local_port: int
|
||||
@@ -169,6 +184,8 @@ def load_config(*, require_auth: bool = True) -> Config:
|
||||
except ValueError:
|
||||
llm_runtime = "none"
|
||||
|
||||
enable_swarmui = parse_enable_swarmui(os.environ.get("ENABLE_SWARMUI"), default=True)
|
||||
|
||||
def _dir(env_name: str, folder: str) -> Path:
|
||||
raw = (os.environ.get(env_name) or "").strip()
|
||||
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(),
|
||||
update_git=_as_bool(os.environ.get("UPDATE_GIT"), True),
|
||||
llm_runtime=llm_runtime,
|
||||
enable_swarmui=enable_swarmui,
|
||||
ollama_models_manifest=ollama_manifest,
|
||||
llamacpp_models_manifest=llamacpp_manifest,
|
||||
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]
|
||||
Description=gpu-rent idle-killer (one shot)
|
||||
After=network-online.target swarmui.service
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@@ -71,6 +71,12 @@ LLM_RUNTIME_LABELS: dict[str, str] = {
|
||||
"llamacpp": "llama.cpp server (+ GGUF)",
|
||||
}
|
||||
|
||||
WORKLOAD_LABELS: dict[str, str] = {
|
||||
"swarm": "только SwarmUI",
|
||||
"both": "SwarmUI + LLM",
|
||||
"llm": "только LLM (без SwarmUI)",
|
||||
}
|
||||
|
||||
|
||||
def llm_runtime_menu() -> list:
|
||||
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")]
|
||||
|
||||
|
||||
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:
|
||||
from gpu_rent.prompts import MenuItem
|
||||
|
||||
|
||||
+57
-25
@@ -440,33 +440,68 @@ def provision_vm(
|
||||
server_id: str | None = None,
|
||||
update: bool = True,
|
||||
) -> 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)
|
||||
try:
|
||||
if seed_extensions(cfg, host, log, update=update):
|
||||
restart = True
|
||||
except GpuRentError as exc:
|
||||
log(f"extensions: {exc}")
|
||||
raise
|
||||
try:
|
||||
if seed_autocomplete(cfg, host, log):
|
||||
restart = True
|
||||
except GpuRentError as exc:
|
||||
log(f"autocomplete: {exc}")
|
||||
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_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:
|
||||
pull_tree(cfg, host, f"{DATA}/Output", cfg.local_output_dir, log)
|
||||
if swarm:
|
||||
try:
|
||||
if seed_extensions(cfg, host, log, update=update):
|
||||
restart = True
|
||||
except GpuRentError as exc:
|
||||
log(f"extensions: {exc}")
|
||||
raise
|
||||
try:
|
||||
if seed_autocomplete(cfg, host, log):
|
||||
restart = True
|
||||
except GpuRentError as exc:
|
||||
log(f"autocomplete: {exc}")
|
||||
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_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:
|
||||
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:
|
||||
probe_gpu(cfg, host, log)
|
||||
except Exception as exc:
|
||||
log(f"GPU probe: {exc}")
|
||||
|
||||
ensure_swarmui_running(cfg, host, log, restart=restart)
|
||||
|
||||
from gpu_rent.state import load_state, save_state
|
||||
if swarm:
|
||||
ensure_swarmui_running(cfg, host, log, restart=restart)
|
||||
run_ssh(
|
||||
cfg,
|
||||
host,
|
||||
"sudo -n rm -f /mnt/swarm_data/.gpu-rent-llm-only",
|
||||
check=False,
|
||||
)
|
||||
|
||||
try:
|
||||
provision_llm(cfg, host, log)
|
||||
@@ -474,7 +509,6 @@ def provision_vm(
|
||||
st = load_state()
|
||||
st.notes = dict(st.notes or {})
|
||||
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"
|
||||
save_state(st)
|
||||
raise CloudError(f"LLM runtime: {exc}") from exc
|
||||
@@ -498,10 +532,8 @@ def provision_vm(
|
||||
"⚠ idle-killer НЕ вооружён — GPU может тарифицироваться без авто-stop. "
|
||||
"См. status / docs/setup.md"
|
||||
)
|
||||
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 swarm:
|
||||
log("SwarmUI слушает 127.0.0.1:7801 — gpu-rent tunnel")
|
||||
if rt == "ollama":
|
||||
log(f"Ollama API → localhost:{cfg.ollama_local_port} (туннель)")
|
||||
elif rt == "llamacpp":
|
||||
|
||||
@@ -95,7 +95,26 @@ mkdir -p \
|
||||
"${DATA_ROOT}/dlbackend" \
|
||||
"${DATA_ROOT}/Extensions" \
|
||||
"${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
|
||||
log "clone SwarmUI -> ${SWARM_ROOT}"
|
||||
|
||||
@@ -236,7 +236,10 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
swarm_url = str(creds.get("swarm_url") or "http://127.0.0.1:7801")
|
||||
busy, detail = swarm_busy(swarm_url)
|
||||
if (DATA / ".gpu-rent-llm-only").is_file():
|
||||
busy, detail = False, "llm-only (swarm skip)"
|
||||
else:
|
||||
busy, detail = swarm_busy(swarm_url)
|
||||
if busy:
|
||||
write_ts(IDLE_SINCE, None)
|
||||
log(f"busy: {detail}")
|
||||
|
||||
+24
-21
@@ -86,27 +86,26 @@ def _bind_access(
|
||||
log(f"SSH {cfg.ssh_user}@{ip}")
|
||||
state.phase = "bootstrapping"
|
||||
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()
|
||||
if active == "active":
|
||||
log("systemctl stop swarmui перед git update")
|
||||
run_ssh(cfg, ip, "sudo -n systemctl stop swarmui", timeout=120, check=False)
|
||||
# Skip apt-heavy bootstrap when the VM already finished first-boot.
|
||||
marker = run_ssh(
|
||||
cfg,
|
||||
ip,
|
||||
"test -f /opt/swarmui/.gpu-rent-bootstrapped && echo yes || echo no",
|
||||
check=False,
|
||||
).strip()
|
||||
if swarm:
|
||||
marker_cmd = "test -f /opt/swarmui/.gpu-rent-bootstrapped && echo yes || echo no"
|
||||
else:
|
||||
marker_cmd = "test -f /mnt/swarm_data/.gpu-rent-ready && echo yes || echo no"
|
||||
marker = run_ssh(cfg, ip, marker_cmd, check=False).strip()
|
||||
if marker == "yes":
|
||||
# VM marker survives stop; local bootstrapped is cleared on stop — still light.
|
||||
if not state.bootstrapped:
|
||||
log("маркер bootstrap на VM — лёгкий проход (локальный bootstrapped был сброшен)")
|
||||
else:
|
||||
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:
|
||||
run_bootstrap(cfg, ip, log, update=update, light=False)
|
||||
run_bootstrap(cfg, ip, log, update=update and swarm, light=False)
|
||||
provision_vm(
|
||||
cfg,
|
||||
ip,
|
||||
@@ -115,17 +114,19 @@ def _bind_access(
|
||||
server_id=getattr(server, "id", None) or state.server_id,
|
||||
update=update,
|
||||
)
|
||||
try:
|
||||
wait_backend_idle(cfg, ip, log)
|
||||
except CloudError as exc:
|
||||
log(f"ready: {exc}")
|
||||
# Comfy venv + Backends.fds exist after Idle — sage/triton + ExtraArgs.
|
||||
try:
|
||||
if tune_swarm_perf(cfg, ip, log):
|
||||
log("systemctl restart swarmui (perf ExtraArgs)")
|
||||
run_ssh(cfg, ip, "sudo -n systemctl restart swarmui", timeout=120)
|
||||
except Exception as exc:
|
||||
log(f"perf tune: {exc}")
|
||||
if swarm:
|
||||
try:
|
||||
wait_backend_idle(cfg, ip, log)
|
||||
except CloudError as exc:
|
||||
log(f"ready: {exc}")
|
||||
try:
|
||||
if tune_swarm_perf(cfg, ip, log):
|
||||
log("systemctl restart swarmui (perf ExtraArgs)")
|
||||
run_ssh(cfg, ip, "sudo -n systemctl restart swarmui", timeout=120)
|
||||
except Exception as exc:
|
||||
log(f"perf tune: {exc}")
|
||||
else:
|
||||
log("ready: llm-only (без ожидания SwarmUI Idle)")
|
||||
try:
|
||||
ensure_boot_snapshot(
|
||||
conn,
|
||||
@@ -138,6 +139,8 @@ def _bind_access(
|
||||
notify_ready(cfg, log)
|
||||
state.bootstrapped = True
|
||||
state.phase = "ready_cloud"
|
||||
state.notes = dict(state.notes or {})
|
||||
state.notes["enable_swarmui"] = swarm
|
||||
save_state(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]]:
|
||||
"""List of (local_port, remote_port). SwarmUI always; LLM if configured.
|
||||
|
||||
Prefer live config (`LLM_RUNTIME`) over stale state.notes.
|
||||
"""
|
||||
pairs = [(cfg.swarmui_local_port, 7801)]
|
||||
"""List of (local_port, remote_port). SwarmUI if enabled; LLM if configured."""
|
||||
pairs: list[tuple[int, int]] = []
|
||||
if bool(getattr(cfg, "enable_swarmui", True)):
|
||||
pairs.append((cfg.swarmui_local_port, 7801))
|
||||
runtime = normalize_runtime(cfg.llm_runtime)
|
||||
if runtime == "ollama":
|
||||
pairs.append((cfg.ollama_local_port, 11434))
|
||||
elif runtime == "llamacpp":
|
||||
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
|
||||
|
||||
|
||||
@@ -181,14 +183,23 @@ def run_tunnel(
|
||||
log("watchdog: EXPIRED → unshelve + reconnect")
|
||||
|
||||
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
|
||||
|
||||
print_access_card(cfg, tunneled=True, host=current_host)
|
||||
|
||||
if open_browser:
|
||||
webbrowser.open(swarm_url)
|
||||
webbrowser.open(open_url)
|
||||
|
||||
state = load_state()
|
||||
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