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.
This commit is contained in:
Leonid Pershin
2026-08-21 08:32:33 +03:00
parent 5081a6bc1e
commit 6871c511c4
9 changed files with 179 additions and 14 deletions
+1
View File
@@ -57,6 +57,7 @@ gpu-rent up --yes --ollama
| `gpu-rent dry-run` | План без mutating-вызовов | | `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`: меню 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 --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 -v` / `--verbose` | Полная таблица doctor на `up` (по умолчанию кратко) |
| `gpu-rent up --ollama` / `--llamacpp` / `--llm …` | LLM рядом со SwarmUI | | `gpu-rent up --ollama` / `--llamacpp` / `--llm …` | LLM рядом со SwarmUI |
| `gpu-rent up --no-swarm` / `--llm-only` | Только LLM (нужен `--ollama`/`--llamacpp`); без clone SwarmUI | | `gpu-rent up --no-swarm` / `--llm-only` | Только LLM (нужен `--ollama`/`--llamacpp`); без clone SwarmUI |
+10 -3
View File
@@ -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. Уже скачанные крупные файлы не трогает. На `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 (~30MB, 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 515 мин).
2. Иначе → Ubuntu **Vulkan** prebuilt (~30MB).
3. Fallback на другой путь при ошибке.
Был только Vulkan-stamp, а `nvcc` появился — следующий `up` сам пересоберёт CUDA.
| Var | Зачем | | Var | Зачем |
| --- | --- | | --- | --- |
| `LLAMACPP_BACKEND=auto\|cuda\|vulkan` | выбор пути (default auto) |
| `LLAMACPP_TAG` | pin release (`b10545`) | | `LLAMACPP_TAG` | pin release (`b10545`) |
| `LLAMACPP_ASSET_URL` + `LLAMACPP_SHA256` | свой архив | | `LLAMACPP_ASSET_URL` + `LLAMACPP_SHA256` | свой архив |
| `LLAMACPP_BUILD_CUDA=1` | сразу CUDA из исходников | | `LLAMACPP_BUILD_CUDA=1` | форс CUDA; `=0` — никогда не собирать |
| `LLAMACPP_FORCE_REINSTALL=1` | снести бинарь и поставить заново | | `LLAMACPP_FORCE_REINSTALL=1` | снести бинарь и поставить заново |
| `LLAMACPP_NGL` / `LLAMACPP_CTX` | override GPU layers / context | | `LLAMACPP_NGL` / `LLAMACPP_CTX` | override GPU layers / context |
| `LLAMACPP_HOST` / `LLAMACPP_PORT` | bind (default `127.0.0.1:8080`) | | `LLAMACPP_HOST` / `LLAMACPP_PORT` | bind (default `127.0.0.1:8080`) |
+3
View File
@@ -48,6 +48,7 @@ LLAMACPP_LOCAL_PORT=17812
# LLAMACPP_MODELS_MANIFEST= # LLAMACPP_MODELS_MANIFEST=
# Pin / тонкая настройка LLM (несecреты; удобнее в gpu-rent.vars — см. кейсы AH): # Pin / тонкая настройка LLM (несecреты; удобнее в gpu-rent.vars — см. кейсы AH):
# LLAMACPP_TAG=b10545 # LLAMACPP_TAG=b10545
# LLAMACPP_BACKEND=auto
# LLAMACPP_BUILD_CUDA=1 # LLAMACPP_BUILD_CUDA=1
# LLAMACPP_FORCE_REINSTALL=1 # LLAMACPP_FORCE_REINSTALL=1
# LLAMACPP_ASSET_URL= # LLAMACPP_ASSET_URL=
@@ -77,6 +78,8 @@ IDLE_GRACE_MINUTES=90
# LOCAL_WATCHDOG_GRACE_MINUTES=10 # LOCAL_WATCHDOG_GRACE_MINUTES=10
PULL_OUTPUT=false PULL_OUTPUT=false
NOTIFY_READY=true NOTIFY_READY=true
# При падении install/up гасить compute (диски оставить). Отключить: false или up --keep-on-fail
# UP_STOP_ON_FAIL=true
# Статический API-ключ панели (X-Token) — только для баланса, не для OpenStack: # Статический API-ключ панели (X-Token) — только для баланса, не для OpenStack:
# SELECTEL_API_TOKEN= # SELECTEL_API_TOKEN=
# Уведомления о списании (local-watchdog tick), шаг с момента up: # Уведомления о списании (local-watchdog tick), шаг с момента up:
+11 -6
View File
@@ -22,6 +22,7 @@
# IDLE_MINUTES=60 # IDLE_MINUTES=60
# IDLE_GRACE_MINUTES=90 # IDLE_GRACE_MINUTES=90
# NOTIFY_READY=true # NOTIFY_READY=true
# UP_STOP_ON_FAIL=true # при падении up гасить GPU (default); false / --keep-on-fail
# Prefer auto /32 (leave unset). Spike/WARP only: # Prefer auto /32 (leave unset). Spike/WARP only:
# GPU_RENT_SSH_CIDR=0.0.0.0/0 # GPU_RENT_SSH_CIDR=0.0.0.0/0
@@ -30,23 +31,27 @@
# Подробнее: docs/llm.md # Подробнее: docs/llm.md
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# --- A) Быстрый default: SwarmUI + llama.cpp, Ubuntu Vulkan prebuilt --- # --- A) Default: SwarmUI + llama.cpp (auto: CUDA если nvcc на VM, иначе Vulkan) ---
# LLM_RUNTIME=llamacpp # LLM_RUNTIME=llamacpp
# ENABLE_SWARMUI=true # ENABLE_SWARMUI=true
# LLAMACPP_TAG=b10545 # LLAMACPP_TAG=b10545
# (GGUF — llamacpp-models.yaml; бинарь без compile) # LLAMACPP_BACKEND=auto
# --- B) Только LLM, без SwarmUI (дешевле по времени bootstrap / VRAM) --- # --- B) Только LLM, без SwarmUI ---
# WORKLOAD=llm # WORKLOAD=llm
# LLM_RUNTIME=llamacpp # LLM_RUNTIME=llamacpp
# LLAMACPP_TAG=b10545 # LLAMACPP_TAG=b10545
# --- C) Максимальная скорость инференса: CUDA-сборка (5–15 мин первый раз) --- # --- C) Явно CUDA (и снести старый Vulkan-бинарь) ---
# LLM_RUNTIME=llamacpp # LLM_RUNTIME=llamacpp
# LLAMACPP_TAG=b10545 # LLAMACPP_TAG=b10545
# LLAMACPP_BUILD_CUDA=1 # LLAMACPP_BACKEND=cuda
# LLAMACPP_FORCE_REINSTALL=1 # LLAMACPP_FORCE_REINSTALL=1
# (FORCE — снести старый Vulkan/битый бинарь и переустановить)
# --- C2) Явно быстрый Vulkan, без compile ---
# LLM_RUNTIME=llamacpp
# LLAMACPP_BACKEND=vulkan
# LLAMACPP_BUILD_CUDA=0
# --- D) Свой бинарь / pin URL (supply-chain) --- # --- D) Свой бинарь / pin URL (supply-chain) ---
# LLM_RUNTIME=llamacpp # LLM_RUNTIME=llamacpp
+40
View File
@@ -57,6 +57,21 @@ def _die(exc: BaseException) -> None:
raise typer.Exit(1) 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: def _print_checks(checks, *, quiet: bool = False) -> int:
failed = blocking_failed(checks) failed = blocking_failed(checks)
if quiet: if quiet:
@@ -439,6 +454,11 @@ def up(
"--no-update", "--no-update",
help="Не делать git pull SwarmUI и установленных extensions", 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( verbose: bool = typer.Option(
False, False,
"--verbose", "--verbose",
@@ -458,6 +478,8 @@ def up(
), ),
) -> None: ) -> None:
"""Create/unshelve GPU; SwarmUI и/или LLM; по умолчанию туннель.""" """Create/unshelve GPU; SwarmUI и/или LLM; по умолчанию туннель."""
cfg = None
up_ok = False
try: try:
from dataclasses import replace from dataclasses import replace
@@ -680,6 +702,7 @@ def up(
ask=None if yes else ask, ask=None if yes else ask,
log=log, log=log,
) )
up_ok = True
if no_tunnel: if no_tunnel:
from gpu_rent.access_card import print_access_card from gpu_rent.access_card import print_access_card
@@ -705,7 +728,24 @@ def up(
open_browser=open_browser, open_browser=open_browser,
log=log, 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: 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) _die(exc)
+3
View File
@@ -121,6 +121,7 @@ class Config:
idle_grace_minutes: int idle_grace_minutes: int
pull_output: bool pull_output: bool
notify_ready: bool notify_ready: bool
up_stop_on_fail: bool
selectel_api_token: str selectel_api_token: str
balance_notify_step_rub: float balance_notify_step_rub: float
balance_notify_low_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), idle_grace_minutes=_as_int(os.environ.get("IDLE_GRACE_MINUTES"), 90),
pull_output=_as_bool(os.environ.get("PULL_OUTPUT"), False), pull_output=_as_bool(os.environ.get("PULL_OUTPUT"), False),
notify_ready=_as_bool(os.environ.get("NOTIFY_READY"), True), 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(), 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_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), balance_notify_low_rub=_as_float(os.environ.get("BALANCE_NOTIFY_LOW_RUB"), 0.0),
+3 -2
View File
@@ -36,6 +36,7 @@ _LLAMACPP_INSTALL_ENV = (
"LLAMACPP_ASSET_URL", "LLAMACPP_ASSET_URL",
"LLAMACPP_SHA256", "LLAMACPP_SHA256",
"LLAMACPP_BUILD_CUDA", "LLAMACPP_BUILD_CUDA",
"LLAMACPP_BACKEND",
"LLAMACPP_FORCE_REINSTALL", "LLAMACPP_FORCE_REINSTALL",
"LLAMACPP_NGL", "LLAMACPP_NGL",
"LLAMACPP_CTX", "LLAMACPP_CTX",
@@ -544,13 +545,13 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
log("LLM: ставим/запускаем llama.cpp server") log("LLM: ставим/запускаем llama.cpp server")
import os import os
build_cuda = (os.environ.get("LLAMACPP_BUILD_CUDA") or "").strip() == "1" # Vulkan finishes in seconds; CUDA compile needs up to ~1520 min.
run_script_sudo( run_script_sudo(
cfg, cfg,
host, host,
_pkg_text("install_llamacpp.sh"), _pkg_text("install_llamacpp.sh"),
remote_path="/tmp/gpu-rent-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), env=_remote_llm_env(cfg, *_LLAMACPP_INSTALL_ENV),
log=log, log=log,
) )
+48 -3
View File
@@ -31,8 +31,10 @@ SERVER_BIN="${BIN_DIR}/llama-server"
LLAMACPP_TAG="${LLAMACPP_TAG:-}" LLAMACPP_TAG="${LLAMACPP_TAG:-}"
LLAMACPP_ASSET_URL="${LLAMACPP_ASSET_URL:-}" LLAMACPP_ASSET_URL="${LLAMACPP_ASSET_URL:-}"
LLAMACPP_SHA256="${LLAMACPP_SHA256:-}" 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:-}" 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_FORCE_REINSTALL="${LLAMACPP_FORCE_REINSTALL:-}"
LLAMACPP_NGL="${LLAMACPP_NGL:-}" LLAMACPP_NGL="${LLAMACPP_NGL:-}"
LLAMACPP_CTX="${LLAMACPP_CTX:-}" LLAMACPP_CTX="${LLAMACPP_CTX:-}"
@@ -40,11 +42,48 @@ LLAMACPP_HOST="${LLAMACPP_HOST:-127.0.0.1}"
LLAMACPP_PORT="${LLAMACPP_PORT:-8080}" LLAMACPP_PORT="${LLAMACPP_PORT:-8080}"
LLAMACPP_EXTRA_ARGS="${LLAMACPP_EXTRA_ARGS:-}" 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 if [[ "${LLAMACPP_FORCE_REINSTALL}" == "1" ]]; then
log "LLAMACPP_FORCE_REINSTALL=1 — удаляю старый бинарь" log "LLAMACPP_FORCE_REINSTALL=1 — удаляю старый бинарь"
rm -f "$SERVER_BIN" "$STAMP" rm -f "$SERVER_BIN" "$STAMP"
fi 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. # Prefer ubuntu CUDA (rare) → vulkan → cpu. Never Windows/macOS/cudart-only.
pick_linux_asset_url() { pick_linux_asset_url() {
python3 -c ' python3 -c '
@@ -321,12 +360,18 @@ else
fi fi
installed=0 installed=0
if [[ "${LLAMACPP_BUILD_CUDA}" == "1" ]]; then if want_cuda_build; then
log "LLAMACPP_BUILD_CUDA=1сразу CUDA-сборка" log "backend: CUDA (nvcc есть или LLAMACPP_BACKEND/BUILD_CUDA) — сборка, Vulkan только если упадёт"
if build_cuda_from_source "$tag"; then if build_cuda_from_source "$tag"; then
installed=1 installed=1
else
log "CUDA-сборка не вышла — fallback на Linux prebuilt (Vulkan/CPU)"
if install_linux_release "$tag"; then
installed=1
fi
fi fi
else else
log "backend: Linux prebuilt (нет nvcc / LLAMACPP_BACKEND=vulkan) — без compile"
if install_linux_release "$tag"; then if install_linux_release "$tag"; then
installed=1 installed=1
else else
+60
View File
@@ -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