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:
Leonid Pershin
2026-08-21 06:44:50 +03:00
parent f93ac5a66a
commit 7ed6a99df2
25 changed files with 455 additions and 177 deletions
+4 -2
View File
@@ -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) |
+1
View File
@@ -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) |
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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+ ≥16GiB. 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
View File
@@ -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 (&lt;16GiB) | off | 2m | q4_0 | 6GiB |
| mid (1623) | on* | 5m | q8_0 | 10GiB |
| high (2447) | on* | 15m | q8_0 | 14GiB |
| ultra (≥48) | on* | 30m | q8_0 | 20GiB |
\*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=...
```
+7 -7
View File
@@ -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.
+3
View File
@@ -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
View File
@@ -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): ориентир **2040 минут**. Не закрывай терминал посередине 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
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
+14 -2
View File
@@ -40,10 +40,12 @@ 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)."""
links: list[AccessLink] = []
swarm = bool(getattr(cfg, "enable_swarmui", True))
if tunneled:
if swarm:
port = cfg.swarmui_local_port
base = f"http://127.0.0.1:{port}"
links: list[AccessLink] = []
if tunneled:
links.extend(
[
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"),
]
)
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 [
"{",
+9 -1
View File
@@ -24,10 +24,14 @@ 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 not skip_swarm:
if update:
log("git update: SwarmUI on")
else:
@@ -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:]}")
if skip_swarm:
log("data disk готов — дальше LLM")
else:
log("диски и systemd unit готовы; дальше seed, потом start swarmui")
+92 -18
View File
@@ -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:
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:
choice = prompt_menu(
"LLM рядом со SwarmUI",
llm_runtime_menu(),
default="none",
"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"
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)
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)
+18
View File
@@ -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),
+1 -1
View File
@@ -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
+12
View File
@@ -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
+40 -8
View File
@@ -440,7 +440,18 @@ 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)
if swarm:
try:
if seed_extensions(cfg, host, log, update=update):
restart = True
@@ -454,19 +465,43 @@ def provision_vm(
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)
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}")
if swarm:
ensure_swarmui_running(cfg, host, log, restart=restart)
from gpu_rent.state import load_state, save_state
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"
)
if swarm:
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":
log(f"Ollama API → localhost:{cfg.ollama_local_port} (туннель)")
elif rt == "llamacpp":
+20 -1
View File
@@ -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}"
+3
View File
@@ -236,6 +236,9 @@ def main() -> int:
return 0
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)
if busy:
write_ts(IDLE_SINCE, None)
+14 -11
View File
@@ -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,
)
if swarm:
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}")
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
View File
@@ -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"
+44
View File
@@ -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("#")