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:
@@ -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 |
|
||||
|
||||
+10
-3
@@ -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`) |
|
||||
|
||||
@@ -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:
|
||||
|
||||
+11
-6
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user