diff --git a/README.md b/README.md index 955a345..6ba66fe 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/docs/README.md b/docs/README.md index 6182fbf..8558d11 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) | diff --git a/docs/architecture.md b/docs/architecture.md index 9ff3dec..48201f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.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/`. diff --git a/docs/cli.md b/docs/cli.md index c335c90..8e47dc6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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) diff --git a/docs/concept.md b/docs/concept.md index 9df1069..900301c 100644 --- a/docs/concept.md +++ b/docs/concept.md @@ -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. ## Что не входит (пока) diff --git a/docs/decisions.md b/docs/decisions.md index a6919bc..f3a2100 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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 генерирует `/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair | diff --git a/docs/llm.md b/docs/llm.md index a0f5aee..52f3f32 100644 --- a/docs/llm.md +++ b/docs/llm.md @@ -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= - LLAMACPP_TAG=b4690 # или LLAMACPP_ASSET_URL=... + LLAMACPP_SHA256=... ``` diff --git a/docs/open-questions.md b/docs/open-questions.md index 8d68b66..5737e52 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index fe31f4e..498254c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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 diff --git a/docs/setup.md b/docs/setup.md index ee48f51..974ef54 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -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` Секреты только в `/.env` и runtime в `/.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. diff --git a/docs/spike-notes.md b/docs/spike-notes.md index 12703d3..e1e4427 100644 --- a/docs/spike-notes.md +++ b/docs/spike-notes.md @@ -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). diff --git a/env.example b/env.example index 1818935..59fe2ce 100644 --- a/env.example +++ b/env.example @@ -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 diff --git a/gpu-rent.vars.example b/gpu-rent.vars.example index 3c56ce1..7f15c97 100644 --- a/gpu-rent.vars.example +++ b/gpu-rent.vars.example @@ -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 diff --git a/src/gpu_rent/access_card.py b/src/gpu_rent/access_card.py index bc93ccb..4120818 100644 --- a/src/gpu_rent/access_card.py +++ b/src/gpu_rent/access_card.py @@ -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 [ "{", diff --git a/src/gpu_rent/bootstrap.py b/src/gpu_rent/bootstrap.py index 5d22310..77143e0 100644 --- a/src/gpu_rent/bootstrap.py +++ b/src/gpu_rent/bootstrap.py @@ -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") diff --git a/src/gpu_rent/cli.py b/src/gpu_rent/cli.py index 5de3da7..13a3cd0 100644 --- a/src/gpu_rent/cli.py +++ b/src/gpu_rent/cli.py @@ -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) diff --git a/src/gpu_rent/config.py b/src/gpu_rent/config.py index b48dd68..f9b5f9f 100644 --- a/src/gpu_rent/config.py +++ b/src/gpu_rent/config.py @@ -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), diff --git a/src/gpu_rent/idle_killer.py b/src/gpu_rent/idle_killer.py index b1fb6cd..f4e658e 100644 --- a/src/gpu_rent/idle_killer.py +++ b/src/gpu_rent/idle_killer.py @@ -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 diff --git a/src/gpu_rent/llm_runtime.py b/src/gpu_rent/llm_runtime.py index ed4d769..1e33561 100644 --- a/src/gpu_rent/llm_runtime.py +++ b/src/gpu_rent/llm_runtime.py @@ -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 diff --git a/src/gpu_rent/provision.py b/src/gpu_rent/provision.py index fe3f1aa..1b3c38f 100644 --- a/src/gpu_rent/provision.py +++ b/src/gpu_rent/provision.py @@ -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": diff --git a/src/gpu_rent/remote/bootstrap.sh b/src/gpu_rent/remote/bootstrap.sh index b3dd111..a40a7ec 100644 --- a/src/gpu_rent/remote/bootstrap.sh +++ b/src/gpu_rent/remote/bootstrap.sh @@ -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}" diff --git a/src/gpu_rent/remote/idle_killer.py b/src/gpu_rent/remote/idle_killer.py index 686b985..0a95342 100644 --- a/src/gpu_rent/remote/idle_killer.py +++ b/src/gpu_rent/remote/idle_killer.py @@ -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}") diff --git a/src/gpu_rent/session.py b/src/gpu_rent/session.py index 47e7c5a..4862acb 100644 --- a/src/gpu_rent/session.py +++ b/src/gpu_rent/session.py @@ -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 diff --git a/src/gpu_rent/tunnel.py b/src/gpu_rent/tunnel.py index 1b57cb8..16504a4 100644 --- a/src/gpu_rent/tunnel.py +++ b/src/gpu_rent/tunnel.py @@ -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" diff --git a/tests/test_llm_only.py b/tests/test_llm_only.py new file mode 100644 index 0000000..ac8ad95 --- /dev/null +++ b/tests/test_llm_only.py @@ -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("#")