From 6871c511c4bb10feef4d6c4ad3378268e403fa5f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 21 Aug 2026 08:32:33 +0300 Subject: [PATCH] Implement UP_STOP_ON_FAIL option to manage GPU state on installation failure - Added a new configuration option `UP_STOP_ON_FAIL` to control whether the GPU should be stopped automatically if the `up` command fails, enhancing user control over resource management. - Updated the CLI to include a `--keep-on-fail` flag, allowing users to prevent GPU shutdown during installation errors. - Enhanced the installation scripts and documentation to reflect these changes, providing clearer guidance on the new behavior and configuration options. - Improved error handling in the CLI to ensure proper cleanup of resources in case of failure, preventing unexpected billing for unused GPU resources. --- docs/cli.md | 1 + docs/llm.md | 13 ++++-- env.example | 3 ++ gpu-rent.vars.example | 17 ++++--- src/gpu_rent/cli.py | 40 +++++++++++++++++ src/gpu_rent/config.py | 3 ++ src/gpu_rent/provision.py | 5 ++- src/gpu_rent/remote/install_llamacpp.sh | 51 +++++++++++++++++++-- tests/test_up_stop_on_fail.py | 60 +++++++++++++++++++++++++ 9 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 tests/test_up_stop_on_fail.py diff --git a/docs/cli.md b/docs/cli.md index f6447ce..e269476 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -57,6 +57,7 @@ gpu-rent up --yes --ollama | `gpu-rent dry-run` | План без mutating-вызовов | | `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` | +| `up --keep-on-fail` | Не гасить GPU при ошибке install (по умолчанию `UP_STOP_ON_FAIL=true` → `stop`) | | `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 | diff --git a/docs/llm.md b/docs/llm.md index b4bfb16..98f246a 100644 --- a/docs/llm.md +++ b/docs/llm.md @@ -120,15 +120,22 @@ Unit `gpu-rent-ollama` читает `/mnt/swarm_data/.gpu-rent-gpu.json`: На `up`: скачать GGUF → `/mnt/swarm_data/llamacpp/models` → `llama-server` + systemd. Уже скачанные крупные файлы не трогает. -**Бинарник (без compile по умолчанию):** официальные Releases **не** дают Linux CUDA — только Windows CUDA + Ubuntu CPU/Vulkan. `install_llamacpp.sh` берёт **Ubuntu Vulkan** prebuilt (~30 MB, GPU через Vulkan). CUDA-сборка из исходников — только крайний случай (prebuilt упал) или явно `LLAMACPP_BUILD_CUDA=1` (5–15 мин, тихий лог + heartbeat). +**Бинарник:** в GitHub Releases **нет** Linux CUDA — только Windows CUDA + Ubuntu CPU/Vulkan. Драйвер/CUDA runtime на VM ≠ готовый `llama-server` с CUDA: его нужно **собрать** (`nvcc`) или взять Vulkan prebuilt. -Переменные (`.env` или `gpu-rent.vars`, пробрасываются на VM при `up`): +Порядок по умолчанию (`LLAMACPP_BACKEND=auto`): + +1. Если на VM уже есть `nvcc` (часто после прошлого `up`) → **CUDA-сборка** (тихо + heartbeat 5–15 мин). +2. Иначе → Ubuntu **Vulkan** prebuilt (~30 MB). +3. Fallback на другой путь при ошибке. + +Был только Vulkan-stamp, а `nvcc` появился — следующий `up` сам пересоберёт CUDA. | Var | Зачем | | --- | --- | +| `LLAMACPP_BACKEND=auto\|cuda\|vulkan` | выбор пути (default auto) | | `LLAMACPP_TAG` | pin release (`b10545`) | | `LLAMACPP_ASSET_URL` + `LLAMACPP_SHA256` | свой архив | -| `LLAMACPP_BUILD_CUDA=1` | сразу CUDA из исходников | +| `LLAMACPP_BUILD_CUDA=1` | форс CUDA; `=0` — никогда не собирать | | `LLAMACPP_FORCE_REINSTALL=1` | снести бинарь и поставить заново | | `LLAMACPP_NGL` / `LLAMACPP_CTX` | override GPU layers / context | | `LLAMACPP_HOST` / `LLAMACPP_PORT` | bind (default `127.0.0.1:8080`) | diff --git a/env.example b/env.example index 1045288..4e38adf 100644 --- a/env.example +++ b/env.example @@ -48,6 +48,7 @@ LLAMACPP_LOCAL_PORT=17812 # LLAMACPP_MODELS_MANIFEST= # Pin / тонкая настройка LLM (несecреты; удобнее в gpu-rent.vars — см. кейсы A–H): # LLAMACPP_TAG=b10545 +# LLAMACPP_BACKEND=auto # LLAMACPP_BUILD_CUDA=1 # LLAMACPP_FORCE_REINSTALL=1 # LLAMACPP_ASSET_URL= @@ -77,6 +78,8 @@ IDLE_GRACE_MINUTES=90 # LOCAL_WATCHDOG_GRACE_MINUTES=10 PULL_OUTPUT=false NOTIFY_READY=true +# При падении install/up гасить compute (диски оставить). Отключить: false или up --keep-on-fail +# UP_STOP_ON_FAIL=true # Статический API-ключ панели (X-Token) — только для баланса, не для OpenStack: # SELECTEL_API_TOKEN= # Уведомления о списании (local-watchdog tick), шаг с момента up: diff --git a/gpu-rent.vars.example b/gpu-rent.vars.example index 94f7ba9..8a1d8c2 100644 --- a/gpu-rent.vars.example +++ b/gpu-rent.vars.example @@ -22,6 +22,7 @@ # IDLE_MINUTES=60 # IDLE_GRACE_MINUTES=90 # NOTIFY_READY=true +# UP_STOP_ON_FAIL=true # при падении up гасить GPU (default); false / --keep-on-fail # Prefer auto /32 (leave unset). Spike/WARP only: # GPU_RENT_SSH_CIDR=0.0.0.0/0 @@ -30,23 +31,27 @@ # Подробнее: docs/llm.md # --------------------------------------------------------------------------- -# --- A) Быстрый default: SwarmUI + llama.cpp, Ubuntu Vulkan prebuilt --- +# --- A) Default: SwarmUI + llama.cpp (auto: CUDA если nvcc на VM, иначе Vulkan) --- # LLM_RUNTIME=llamacpp # ENABLE_SWARMUI=true # LLAMACPP_TAG=b10545 -# (GGUF — llamacpp-models.yaml; бинарь без compile) +# LLAMACPP_BACKEND=auto -# --- B) Только LLM, без SwarmUI (дешевле по времени bootstrap / VRAM) --- +# --- B) Только LLM, без SwarmUI --- # WORKLOAD=llm # LLM_RUNTIME=llamacpp # LLAMACPP_TAG=b10545 -# --- C) Максимальная скорость инференса: CUDA-сборка (5–15 мин первый раз) --- +# --- C) Явно CUDA (и снести старый Vulkan-бинарь) --- # LLM_RUNTIME=llamacpp # LLAMACPP_TAG=b10545 -# LLAMACPP_BUILD_CUDA=1 +# LLAMACPP_BACKEND=cuda # LLAMACPP_FORCE_REINSTALL=1 -# (FORCE — снести старый Vulkan/битый бинарь и переустановить) + +# --- C2) Явно быстрый Vulkan, без compile --- +# LLM_RUNTIME=llamacpp +# LLAMACPP_BACKEND=vulkan +# LLAMACPP_BUILD_CUDA=0 # --- D) Свой бинарь / pin URL (supply-chain) --- # LLM_RUNTIME=llamacpp diff --git a/src/gpu_rent/cli.py b/src/gpu_rent/cli.py index 947f74e..7abb046 100644 --- a/src/gpu_rent/cli.py +++ b/src/gpu_rent/cli.py @@ -57,6 +57,21 @@ def _die(exc: BaseException) -> None: raise typer.Exit(1) +def _stop_after_failed_up(cfg, cause: BaseException) -> None: + """Delete compute after a failed up so billing does not continue unnoticed.""" + warn( + "up упал — гашу GPU (UP_STOP_ON_FAIL; оставить: --keep-on-fail / UP_STOP_ON_FAIL=false)" + ) + try: + cmd_stop(cfg, no_pull=True, log=log) + ok("compute остановлен, диски на месте") + except Exception as stop_exc: + err( + f"auto-stop не удался: {stop_exc} — срочно: gpu-rent stop " + f"(причина up: {cause})" + ) + + def _print_checks(checks, *, quiet: bool = False) -> int: failed = blocking_failed(checks) if quiet: @@ -439,6 +454,11 @@ def up( "--no-update", help="Не делать git pull SwarmUI и установленных extensions", ), + keep_on_fail: bool = typer.Option( + False, + "--keep-on-fail", + help="Не гасить GPU если up упал (по умолчанию stop; UP_STOP_ON_FAIL=false)", + ), verbose: bool = typer.Option( False, "--verbose", @@ -458,6 +478,8 @@ def up( ), ) -> None: """Create/unshelve GPU; SwarmUI и/или LLM; по умолчанию туннель.""" + cfg = None + up_ok = False try: from dataclasses import replace @@ -680,6 +702,7 @@ def up( ask=None if yes else ask, log=log, ) + up_ok = True if no_tunnel: from gpu_rent.access_card import print_access_card @@ -705,7 +728,24 @@ def up( open_browser=open_browser, log=log, ) + except KeyboardInterrupt as exc: + if ( + not up_ok + and cfg is not None + and not keep_on_fail + and bool(getattr(cfg, "up_stop_on_fail", True)) + ): + _stop_after_failed_up(cfg, exc) + err("прервано (Ctrl+C)") + raise typer.Exit(130) from exc except GpuRentError as exc: + if ( + not up_ok + and cfg is not None + and not keep_on_fail + and bool(getattr(cfg, "up_stop_on_fail", True)) + ): + _stop_after_failed_up(cfg, exc) _die(exc) diff --git a/src/gpu_rent/config.py b/src/gpu_rent/config.py index 750f559..53ee397 100644 --- a/src/gpu_rent/config.py +++ b/src/gpu_rent/config.py @@ -121,6 +121,7 @@ class Config: idle_grace_minutes: int pull_output: bool notify_ready: bool + up_stop_on_fail: bool selectel_api_token: str balance_notify_step_rub: float balance_notify_low_rub: float @@ -258,6 +259,8 @@ def load_config(*, require_auth: bool = True) -> Config: idle_grace_minutes=_as_int(os.environ.get("IDLE_GRACE_MINUTES"), 90), pull_output=_as_bool(os.environ.get("PULL_OUTPUT"), False), notify_ready=_as_bool(os.environ.get("NOTIFY_READY"), True), + # Default on: failed up must not leave a billing GPU running. + up_stop_on_fail=_as_bool(os.environ.get("UP_STOP_ON_FAIL"), True), selectel_api_token=(os.environ.get("SELECTEL_API_TOKEN") or "").strip(), balance_notify_step_rub=_as_float(os.environ.get("BALANCE_NOTIFY_STEP_RUB"), 200.0), balance_notify_low_rub=_as_float(os.environ.get("BALANCE_NOTIFY_LOW_RUB"), 0.0), diff --git a/src/gpu_rent/provision.py b/src/gpu_rent/provision.py index 41c24be..a9b2338 100644 --- a/src/gpu_rent/provision.py +++ b/src/gpu_rent/provision.py @@ -36,6 +36,7 @@ _LLAMACPP_INSTALL_ENV = ( "LLAMACPP_ASSET_URL", "LLAMACPP_SHA256", "LLAMACPP_BUILD_CUDA", + "LLAMACPP_BACKEND", "LLAMACPP_FORCE_REINSTALL", "LLAMACPP_NGL", "LLAMACPP_CTX", @@ -544,13 +545,13 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None: log("LLM: ставим/запускаем llama.cpp server") import os - build_cuda = (os.environ.get("LLAMACPP_BUILD_CUDA") or "").strip() == "1" + # Vulkan finishes in seconds; CUDA compile needs up to ~15–20 min. run_script_sudo( cfg, host, _pkg_text("install_llamacpp.sh"), remote_path="/tmp/gpu-rent-install_llamacpp.sh", - timeout=3600 if build_cuda else 1200, + timeout=3600, env=_remote_llm_env(cfg, *_LLAMACPP_INSTALL_ENV), log=log, ) diff --git a/src/gpu_rent/remote/install_llamacpp.sh b/src/gpu_rent/remote/install_llamacpp.sh index bf71bd9..ab82171 100644 --- a/src/gpu_rent/remote/install_llamacpp.sh +++ b/src/gpu_rent/remote/install_llamacpp.sh @@ -31,8 +31,10 @@ SERVER_BIN="${BIN_DIR}/llama-server" LLAMACPP_TAG="${LLAMACPP_TAG:-}" LLAMACPP_ASSET_URL="${LLAMACPP_ASSET_URL:-}" LLAMACPP_SHA256="${LLAMACPP_SHA256:-}" -# 1 = force CUDA compile; 0/empty = prebuilt first, build only if prebuilt fails +# 1 = force CUDA compile; 0 = never compile (Vulkan/CPU prebuilt only) LLAMACPP_BUILD_CUDA="${LLAMACPP_BUILD_CUDA:-}" +# auto | cuda | vulkan — default auto: CUDA if nvcc already on VM, else Vulkan prebuilt +LLAMACPP_BACKEND="${LLAMACPP_BACKEND:-auto}" LLAMACPP_FORCE_REINSTALL="${LLAMACPP_FORCE_REINSTALL:-}" LLAMACPP_NGL="${LLAMACPP_NGL:-}" LLAMACPP_CTX="${LLAMACPP_CTX:-}" @@ -40,11 +42,48 @@ LLAMACPP_HOST="${LLAMACPP_HOST:-127.0.0.1}" LLAMACPP_PORT="${LLAMACPP_PORT:-8080}" LLAMACPP_EXTRA_ARGS="${LLAMACPP_EXTRA_ARGS:-}" +have_nvcc() { + if command -v nvcc >/dev/null 2>&1; then + return 0 + fi + if [[ -x /usr/local/cuda/bin/nvcc ]]; then + export PATH="/usr/local/cuda/bin:${PATH}" + return 0 + fi + return 1 +} + +# Prefer CUDA when toolkit already present (GPU images / prior up). Vulkan = fast no-compile. +want_cuda_build() { + case "${LLAMACPP_BUILD_CUDA}" in + 1|yes|true) return 0 ;; + 0|no|false) return 1 ;; + esac + case "${LLAMACPP_BACKEND}" in + cuda) return 0 ;; + vulkan) return 1 ;; + *) + if have_nvcc; then + return 0 + fi + return 1 + ;; + esac +} + if [[ "${LLAMACPP_FORCE_REINSTALL}" == "1" ]]; then log "LLAMACPP_FORCE_REINSTALL=1 — удаляю старый бинарь" rm -f "$SERVER_BIN" "$STAMP" fi +# Upgrade path: previous default was Vulkan prebuilt; if nvcc is here, prefer CUDA. +if [[ -x "$SERVER_BIN" && -f "$STAMP" && "${LLAMACPP_BACKEND}" != "vulkan" && "${LLAMACPP_BUILD_CUDA}" != "0" ]]; then + if grep -q '^asset:' "$STAMP" 2>/dev/null && want_cuda_build; then + log "был Vulkan/CPU prebuilt, nvcc есть — пересобираю CUDA (лучше на NVIDIA)" + rm -f "$SERVER_BIN" "$STAMP" + fi +fi + # Prefer ubuntu CUDA (rare) → vulkan → cpu. Never Windows/macOS/cudart-only. pick_linux_asset_url() { python3 -c ' @@ -321,12 +360,18 @@ else fi installed=0 - if [[ "${LLAMACPP_BUILD_CUDA}" == "1" ]]; then - log "LLAMACPP_BUILD_CUDA=1 — сразу CUDA-сборка" + if want_cuda_build; then + log "backend: CUDA (nvcc есть или LLAMACPP_BACKEND/BUILD_CUDA) — сборка, Vulkan только если упадёт" if build_cuda_from_source "$tag"; then installed=1 + else + log "CUDA-сборка не вышла — fallback на Linux prebuilt (Vulkan/CPU)" + if install_linux_release "$tag"; then + installed=1 + fi fi else + log "backend: Linux prebuilt (нет nvcc / LLAMACPP_BACKEND=vulkan) — без compile" if install_linux_release "$tag"; then installed=1 else diff --git a/tests/test_up_stop_on_fail.py b/tests/test_up_stop_on_fail.py new file mode 100644 index 0000000..cc9c1c3 --- /dev/null +++ b/tests/test_up_stop_on_fail.py @@ -0,0 +1,60 @@ +from types import SimpleNamespace + +from gpu_rent.cli import _stop_after_failed_up + + +def test_stop_after_failed_up_calls_cmd_stop(monkeypatch): + calls: list[dict] = [] + + def fake_stop(cfg, *, no_pull=False, log=None, destroy_disks=False): + calls.append({"no_pull": no_pull, "destroy_disks": destroy_disks}) + return SimpleNamespace() + + monkeypatch.setattr("gpu_rent.cli.cmd_stop", fake_stop) + monkeypatch.setattr("gpu_rent.cli.warn", lambda *_a, **_k: None) + monkeypatch.setattr("gpu_rent.cli.ok", lambda *_a, **_k: None) + monkeypatch.setattr("gpu_rent.cli.err", lambda *_a, **_k: None) + monkeypatch.setattr("gpu_rent.cli.log", lambda *_a, **_k: None) + + _stop_after_failed_up(SimpleNamespace(), RuntimeError("boom")) + assert calls == [{"no_pull": True, "destroy_disks": False}] + + +def test_up_stop_on_fail_default_true(monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + for key in ( + "OS_AUTH_URL", + "OS_USER_DOMAIN_NAME", + "OS_USERNAME", + "OS_PASSWORD", + "OS_PROJECT_ID", + "OS_REGION_NAME", + "GPU_RENT_AZ", + ): + monkeypatch.setenv(key, "x") + monkeypatch.delenv("UP_STOP_ON_FAIL", raising=False) + from gpu_rent.config import load_config + + cfg = load_config(require_auth=False) + assert cfg.up_stop_on_fail is True + + +def test_up_stop_on_fail_can_disable(monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + for key in ( + "OS_AUTH_URL", + "OS_USER_DOMAIN_NAME", + "OS_USERNAME", + "OS_PASSWORD", + "OS_PROJECT_ID", + "OS_REGION_NAME", + "GPU_RENT_AZ", + ): + monkeypatch.setenv(key, "x") + monkeypatch.setenv("UP_STOP_ON_FAIL", "false") + from gpu_rent.config import load_config + + cfg = load_config(require_auth=False) + assert cfg.up_stop_on_fail is False