Update documentation to clarify GPU control commands
- Revised README, architecture, CLI, and other documentation to specify that both `Ctrl+C` and `Ctrl+D` now stop the GPU while preserving disk data, correcting previous inaccuracies. - Enhanced access card and setup instructions to reflect the updated command behavior for better user understanding. - Updated tests to ensure the new command behaviors are validated and documented correctly, improving overall clarity in GPU management.
This commit is contained in:
@@ -32,17 +32,15 @@ Unix: `./gpu-rent.sh …` (один раз `chmod +x gpu-rent.sh`).
|
|||||||
```powershell
|
```powershell
|
||||||
.\gpu-rent.ps1 up --yes # GPU + SwarmUI + туннель :17801
|
.\gpu-rent.ps1 up --yes # GPU + SwarmUI + туннель :17801
|
||||||
# работаешь в браузере…
|
# работаешь в браузере…
|
||||||
# Ctrl+C = туннель off, GPU жив
|
# Ctrl+C / Ctrl+D = stop GPU (диски остаются)
|
||||||
# Ctrl+D = stop GPU (диски остаются)
|
.\gpu-rent.ps1 tunnel --open # снова UI, если GPU ещё жив
|
||||||
.\gpu-rent.ps1 tunnel --open # снова UI, если закрыл Ctrl+C
|
|
||||||
.\gpu-rent.ps1 hold # отложить idle-killer
|
.\gpu-rent.ps1 hold # отложить idle-killer
|
||||||
.\gpu-rent.ps1 stop # погасить GPU, диски оставить
|
.\gpu-rent.ps1 stop # погасить GPU, диски оставить
|
||||||
```
|
```
|
||||||
|
|
||||||
| Важно | |
|
| Важно | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `Ctrl+C` на туннеле | **не** гасит GPU |
|
| `Ctrl+C` / `Ctrl+D` на туннеле | **stop** GPU, диски остаются |
|
||||||
| `Ctrl+D` на туннеле | **stop** GPU, диски остаются |
|
|
||||||
| Простой ~30 мин | idle-killer сам удалит compute |
|
| Простой ~30 мин | idle-killer сам удалит compute |
|
||||||
| Preemptible | ~24 ч → `EXPIRED`; `tunnel` / `up` поднимут снова |
|
| Preemptible | ~24 ч → `EXPIRED`; `tunnel` / `up` поднимут снова |
|
||||||
| Двойной клик лаунчера | задай `GPU_RENT_DEFAULT_ARGS=up --yes` в `gpu-rent.vars` |
|
| Двойной клик лаунчера | задай `GPU_RENT_DEFAULT_ARGS=up --yes` в `gpu-rent.vars` |
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ Killer молчит:
|
|||||||
3. `ERROR` / нет GPU → выход, не бесконечный recreate.
|
3. `ERROR` / нет GPU → выход, не бесконечный recreate.
|
||||||
4. Туннель мёртв при ACTIVE → reconnect.
|
4. Туннель мёртв при ACTIVE → reconnect.
|
||||||
|
|
||||||
`Ctrl+C` здесь закрывает туннель, **не** вызывает `stop`. **Ctrl+D** — `stop` (диски остаются).
|
`Ctrl+C` и `Ctrl+D` вызывают `stop` (диски остаются).
|
||||||
|
|
||||||
## Teardown (`gpu-rent stop`)
|
## Teardown (`gpu-rent stop`)
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -23,8 +23,7 @@ gpu-rent stop
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
gpu-rent up --yes
|
gpu-rent up --yes
|
||||||
# Ctrl+C → туннель off, GPU жив
|
# Ctrl+C / Ctrl+D → stop GPU (диски остаются)
|
||||||
# Ctrl+D → stop GPU
|
|
||||||
gpu-rent tunnel --open
|
gpu-rent tunnel --open
|
||||||
gpu-rent hold
|
gpu-rent hold
|
||||||
gpu-rent stop
|
gpu-rent stop
|
||||||
@@ -66,7 +65,7 @@ gpu-rent up --yes --ollama
|
|||||||
| `gpu-rent up --no-tunnel` | Только облако |
|
| `gpu-rent up --no-tunnel` | Только облако |
|
||||||
| `gpu-rent up --no-spot` | Не preemptible |
|
| `gpu-rent up --no-spot` | Не preemptible |
|
||||||
| `gpu-rent up --flavor ID --yes` | Явный flavor; без `--flavor` + `--yes` — первый доступный из `FLAVOR_PREFERENCE` |
|
| `gpu-rent up --flavor ID --yes` | Явный flavor; без `--flavor` + `--yes` — первый доступный из `FLAVOR_PREFERENCE` |
|
||||||
| `gpu-rent tunnel` / `tunnel --open` | Повторный проброс; `--open` сразу браузер. Ctrl+C GPU **не** гасит; **Ctrl+D** вызывает `stop` |
|
| `gpu-rent tunnel` / `tunnel --open` | Повторный проброс; `--open` сразу браузер. **Ctrl+C / Ctrl+D** вызывают `stop` |
|
||||||
| `gpu-rent open` / `open --llm` | Браузер на SwarmUI / LLM-порт (туннель уже должен слушать) |
|
| `gpu-rent open` / `open --llm` | Браузер на SwarmUI / LLM-порт (туннель уже должен слушать) |
|
||||||
| `gpu-rent status` | State, Nova, диск, killer/hold, LLM, local-watchdog |
|
| `gpu-rent status` | State, Nova, диск, killer/hold, LLM, local-watchdog |
|
||||||
| `gpu-rent hold` / `--minutes N` / `--until ISO` / `--clear` | Пауза idle-killer (нужны живая VM + SSH) |
|
| `gpu-rent hold` / `--minutes N` / `--until ISO` / `--clear` | Пауза idle-killer (нужны живая VM + SSH) |
|
||||||
@@ -125,7 +124,7 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
|
|||||||
|
|
||||||
1. `gpu-rent watchdog install` (раз на машине, из корня репо)
|
1. `gpu-rent watchdog install` (раз на машине, из корня репо)
|
||||||
2. Пока крутится `up`/`tunnel`, пишется heartbeat `.gpu-rent/local-lease.json`
|
2. Пока крутится `up`/`tunnel`, пишется heartbeat `.gpu-rent/local-lease.json`
|
||||||
3. **Ctrl+C** → detach, GPU **не** трогаем. **Ctrl+D** → `stop` (как `gpu-rent stop`)
|
3. **Ctrl+C / Ctrl+D** → `stop` (как `gpu-rent stop`)
|
||||||
4. Процесс умер / ребут → через `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) → `stop`
|
4. Процесс умер / ребут → через `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) → `stop`
|
||||||
5. `gpu-rent stop` чистит lease сам
|
5. `gpu-rent stop` чистит lease сам
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@
|
|||||||
| Тема | Решение |
|
| Тема | Решение |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Аудитория | Личный инструмент, репозиторий можно показать другим: MIT, без аккаунтных id и без имён локальных чекпоинтов |
|
| Аудитория | Личный инструмент, репозиторий можно показать другим: MIT, без аккаунтных id и без имён локальных чекпоинтов |
|
||||||
| Сессия | GPU живёт до `stop` / **Ctrl+D на туннеле** **или** простоя (idle-killer **на VM**). **Ctrl+C** туннель закрывает, compute оставляет. Ноут можно закрыть — compute не обязан умереть. Опционально: `gpu-rent watchdog install` — локальный тик; если туннель умер без Ctrl+C/`Ctrl+D`/`stop`, после `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) → `stop` |
|
| Сессия | GPU живёт до `stop` / **Ctrl+C или Ctrl+D на туннеле** **или** простоя (idle-killer **на VM**). Выход из туннеля гасит compute (диски остаются). Опционально: `gpu-rent watchdog install` — локальный тик; если процесс умер без stop, после `LOCAL_WATCHDOG_GRACE_MINUTES` (дефолт 10) → `stop` |
|
||||||
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
|
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
|
||||||
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
|
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
|
||||||
| Пул GPU | Перед `up`/`flavors` сканируем `SCAN_POOLS` (дефолт `ru-6,ru-7`). `ru-6` — мультизональный: ходим на `https://ru-6.cloud.api.selcloud.ru/compute/` тем же токеном (SDK-каталог часто знает только RC-пул). Собираем типы GPU из extra_specs и совпадения с `FLAVOR_PREFERENCE`. Автоматом `.env` не пишем — печатаем рекомендацию `OS_REGION_NAME` / `GPU_RENT_AZ` |
|
| Пул GPU | Перед `up`/`flavors` сканируем `SCAN_POOLS` (дефолт `ru-6,ru-7`). `ru-6` — мультизональный: ходим на `https://ru-6.cloud.api.selcloud.ru/compute/` тем же токеном (SDK-каталог часто знает только RC-пул). Собираем типы GPU из extra_specs и совпадения с `FLAVOR_PREFERENCE`. Автоматом `.env` не пишем — печатаем рекомендацию `OS_REGION_NAME` / `GPU_RENT_AZ` |
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
| Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` |
|
| Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` |
|
||||||
| Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь |
|
| Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь |
|
||||||
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
||||||
| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. Ctrl+C на туннеле GPU не гасит. **Ctrl+D — `stop`** (диски остаются). Команда `tunnel` остаётся для повторного входа |
|
| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. **Ctrl+C и Ctrl+D на туннеле — `stop`** (диски остаются). Команда `tunnel` остаётся для повторного входа |
|
||||||
| Interactive `up` | Без `--yes`: нумерованные меню (LLM, пресет, flavor #, data GB, preemptible) → confirm. Изменения можно сохранить в `gpu-rent.vars` |
|
| 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` — не тянуть |
|
| Git update | На каждом `up` по умолчанию: `git pull` SwarmUI + репы из `extensions.yaml` + уже установленные на data (`Extensions`/`DLNodes`). `--no-update` или `UPDATE_GIT=false` — не тянуть |
|
||||||
| LLM (opt-in) | `none` по умолчанию. Флаги / `LLM_RUNTIME` / меню. Манифест: `ollama-models.yaml`. Порт 17811. Prompt-help, не замена SwarmUI |
|
| LLM (opt-in) | `none` по умолчанию. Флаги / `LLM_RUNTIME` / меню. Манифест: `ollama-models.yaml`. Порт 17811. Prompt-help, не замена SwarmUI |
|
||||||
@@ -49,6 +49,6 @@
|
|||||||
|
|
||||||
1. **Туннель ≠ жизнь GPU.** `stop` и idle-killer не зависят от того, открыт ли SSH с ноутбука.
|
1. **Туннель ≠ жизнь GPU.** `stop` и idle-killer не зависят от того, открыт ли SSH с ноутбука.
|
||||||
2. **Локальный SwarmUI на 7801 не трогаем.** Туннель по умолчанию на **17801** (на VM по-прежнему 7801 на loopback).
|
2. **Локальный SwarmUI на 7801 не трогаем.** Туннель по умолчанию на **17801** (на VM по-прежнему 7801 на loopback).
|
||||||
3. **`Ctrl+C` на туннеле не удаляет VM.** Иначе «закрыл ноут» невозможно. **`Ctrl+D` вызывает `stop`** (диски на месте). При установленном **local-watchdog** Ctrl+C по-прежнему detach; убийство процесса/ребут без detach → stop после grace.
|
3. **`Ctrl+C` и `Ctrl+D` на туннеле вызывают `stop`** (диски на месте). Убийство процесса/ребут без stop → idle-killer на VM; при **local-watchdog** — ещё и локальный stop после grace.
|
||||||
4. Idle-killer на VM **не может** быть `shutdown -h`: у Selectel остановленная изнутри VM часто продолжает тарифицировать ресурсы. Нужен вызов OpenStack: удалить **этот** compute, диски оставить.
|
4. Idle-killer на VM **не может** быть `shutdown -h`: у Selectel остановленная изнутри VM часто продолжает тарифицировать ресурсы. Нужен вызов OpenStack: удалить **этот** compute, диски оставить.
|
||||||
5. Для этого на VM — OpenStack **application credential** с правом DELETE/GET **только этого** compute (не `/servers/*`, не unrestricted fallback). На `stop`/`destroy` cred отзывается. Компрометация SwarmUI в худшем случае сносит текущую GPU-сессию, а не произвольные машины проекта.
|
5. Для этого на VM — OpenStack **application credential** с правом DELETE/GET **только этого** compute (не `/servers/*`, не unrestricted fallback). На `stop`/`destroy` cred отзывается. Компрометация SwarmUI в худшем случае сносит текущую GPU-сессию, а не произвольные машины проекта.
|
||||||
|
|||||||
+1
-1
@@ -235,7 +235,7 @@ copy models.example.yaml models.yaml
|
|||||||
2. Create/unshelve GPU + диски (после confirm).
|
2. Create/unshelve GPU + диски (после confirm).
|
||||||
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок; optional LLM.
|
3. Bootstrap SwarmUI, extensions, autocomplete, Civitai-seed, push локальных папок; optional LLM.
|
||||||
4. Туннель на `localhost:17801` (или LLM-порт), **проверка** что сервисы отвечают, access-card.
|
4. Туннель на `localhost:17801` (или LLM-порт), **проверка** что сервисы отвечают, access-card.
|
||||||
5. Процесс ждёт: **Ctrl+C** закрывает только туннель (GPU остаётся); **Ctrl+D** — `stop` GPU (диски остаются).
|
5. Процесс ждёт: **Ctrl+C** и **Ctrl+D** — `stop` GPU (диски остаются).
|
||||||
|
|
||||||
Полезные флаги:
|
Полезные флаги:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
## 3. Туннель и API
|
## 3. Туннель и API
|
||||||
|
|
||||||
Если закрыл туннель (**Ctrl+C**, GPU жив):
|
Если GPU ещё жив, а туннель закрыт (`--no-tunnel` или оборвался SSH):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\gpu-rent.ps1 tunnel --open
|
.\gpu-rent.ps1 tunnel --open
|
||||||
|
|||||||
@@ -125,8 +125,7 @@ def render_access_panel(
|
|||||||
cmds.add_row("hold killer", "gpu-rent hold")
|
cmds.add_row("hold killer", "gpu-rent hold")
|
||||||
cmds.add_row("стоп GPU", "gpu-rent stop")
|
cmds.add_row("стоп GPU", "gpu-rent stop")
|
||||||
if tunneled:
|
if tunneled:
|
||||||
cmds.add_row("Ctrl+C", "туннель off, GPU жив")
|
cmds.add_row("Ctrl+C / Ctrl+D", "stop GPU, диски остаются")
|
||||||
cmds.add_row("Ctrl+D", "stop GPU, диски остаются")
|
|
||||||
if not tunneled:
|
if not tunneled:
|
||||||
cmds.add_row("туннель", "gpu-rent tunnel --open")
|
cmds.add_row("туннель", "gpu-rent tunnel --open")
|
||||||
|
|
||||||
@@ -220,7 +219,7 @@ def print_access_card(
|
|||||||
for line in mcp_snippet_lines(cfg):
|
for line in mcp_snippet_lines(cfg):
|
||||||
log(line)
|
log(line)
|
||||||
log("hold: gpu-rent hold | stop: gpu-rent stop")
|
log("hold: gpu-rent hold | stop: gpu-rent stop")
|
||||||
log("Ctrl+C — туннель off, GPU жив | Ctrl+D — stop GPU")
|
log("Ctrl+C / Ctrl+D — stop GPU (диски остаются)")
|
||||||
log("")
|
log("")
|
||||||
return
|
return
|
||||||
from gpu_rent.term import console as default_console
|
from gpu_rent.term import console as default_console
|
||||||
|
|||||||
+3
-3
@@ -49,7 +49,7 @@ def _die(exc: BaseException) -> None:
|
|||||||
err(str(exc))
|
err(str(exc))
|
||||||
hint = (
|
hint = (
|
||||||
"Дальше: gpu-rent status · gpu-rent logs · gpu-rent stop "
|
"Дальше: gpu-rent status · gpu-rent logs · gpu-rent stop "
|
||||||
"(Ctrl+C на туннеле GPU не гасит; Ctrl+D гасит)"
|
"(Ctrl+C / Ctrl+D на туннеле гасят GPU, диски остаются)"
|
||||||
)
|
)
|
||||||
msg = str(exc)
|
msg = str(exc)
|
||||||
if "gpu-rent status" not in msg and "Дальше:" not in msg:
|
if "gpu-rent status" not in msg and "Дальше:" not in msg:
|
||||||
@@ -809,7 +809,7 @@ def logs(
|
|||||||
def tunnel(
|
def tunnel(
|
||||||
open_browser: bool = typer.Option(False, "--open", help="Открыть браузер на 17801"),
|
open_browser: bool = typer.Option(False, "--open", help="Открыть браузер на 17801"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""SSH localhost:17801 -> VM :7801. Ctrl+C — туннель off; Ctrl+D — stop GPU."""
|
"""SSH localhost:17801 -> VM :7801. Ctrl+C / Ctrl+D — stop GPU (диски остаются)."""
|
||||||
try:
|
try:
|
||||||
cfg = load_config(require_auth=True)
|
cfg = load_config(require_auth=True)
|
||||||
state = load_state()
|
state = load_state()
|
||||||
@@ -1052,7 +1052,7 @@ def resize_data(gb: int = typer.Option(..., "--gb", help="Новый разме
|
|||||||
|
|
||||||
watchdog_app = typer.Typer(
|
watchdog_app = typer.Typer(
|
||||||
help=(
|
help=(
|
||||||
"Локальный сервис: если туннель умер без Ctrl+C / Ctrl+D / stop — "
|
"Локальный сервис: если процесс туннеля умер без stop (kill/reboot) — "
|
||||||
"через grace удалить compute. Не путать с idle-killer на VM."
|
"через grace удалить compute. Не путать с idle-killer на VM."
|
||||||
),
|
),
|
||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ def dry_run_plan(checks: list[Check]) -> list[str]:
|
|||||||
"₽: в API нет — смотри панель; диск 24/7 даже после stop",
|
"₽: в API нет — смотри панель; диск 24/7 даже после stop",
|
||||||
f"туннель: localhost:{cfg.swarmui_local_port} -> VM :7801",
|
f"туннель: localhost:{cfg.swarmui_local_port} -> VM :7801",
|
||||||
"gpu-rent up --yes создаст GPU + SwarmUI и откроет туннель :17801 "
|
"gpu-rent up --yes создаст GPU + SwarmUI и откроет туннель :17801 "
|
||||||
"(Ctrl+C не гасит GPU, Ctrl+D гасит)",
|
"(Ctrl+C / Ctrl+D гасят GPU)",
|
||||||
"только облако без туннеля: gpu-rent up --yes --no-tunnel",
|
"только облако без туннеля: gpu-rent up --yes --no-tunnel",
|
||||||
]
|
]
|
||||||
flavor = next((c.detail for c in checks if c.name == "flavor" and c.ok), None)
|
flavor = next((c.detail for c in checks if c.name == "flavor" and c.ok), None)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
VM idle-killer remains the default safety net. This module is opt-in via
|
VM idle-killer remains the default safety net. This module is opt-in via
|
||||||
`gpu-rent watchdog install`: while a tunnel is armed, a scheduled tick stops
|
`gpu-rent watchdog install`: while a tunnel is armed, a scheduled tick stops
|
||||||
compute if the laptop/process died without Ctrl+C detach or `gpu-rent stop`.
|
compute if the laptop/process died without Ctrl+C/Ctrl+D ``stop`` or `gpu-rent stop`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -189,7 +189,7 @@ def touch_heartbeat() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def detach_lease_keep_gpu() -> None:
|
def detach_lease_keep_gpu() -> None:
|
||||||
"""Ctrl+C on tunnel: leave GPU running; local tick must not stop."""
|
"""Mark the lease detached so the local tick will not stop compute."""
|
||||||
lease = load_lease()
|
lease = load_lease()
|
||||||
if lease is None:
|
if lease is None:
|
||||||
lease = LocalLease()
|
lease = LocalLease()
|
||||||
@@ -271,7 +271,7 @@ def install_watchdog(
|
|||||||
log(
|
log(
|
||||||
f"local-watchdog установлен ({platform}): тик каждые {interval} мин. "
|
f"local-watchdog установлен ({platform}): тик каждые {interval} мин. "
|
||||||
f"Grace {grace_seconds() // 60} мин после смерти процесса туннеля → stop. "
|
f"Grace {grace_seconds() // 60} мин после смерти процесса туннеля → stop. "
|
||||||
f"Ctrl+C на туннеле GPU не гасит; Ctrl+D — stop. "
|
f"Ctrl+C / Ctrl+D на туннеле — stop. "
|
||||||
"При SELECTEL_API_TOKEN — toast каждые BALANCE_NOTIFY_STEP_RUB ₽ (дефолт 200)."
|
"При SELECTEL_API_TOKEN — toast каждые BALANCE_NOTIFY_STEP_RUB ₽ (дефолт 200)."
|
||||||
)
|
)
|
||||||
return marker
|
return marker
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ def run_setup(
|
|||||||
|
|
||||||
do_wd = install_watchdog
|
do_wd = install_watchdog
|
||||||
if do_wd is None and confirm:
|
if do_wd is None and confirm:
|
||||||
do_wd = confirm("Установить local-watchdog (аварийный stop без Ctrl+C)?")
|
do_wd = confirm("Установить local-watchdog (аварийный stop, если процесс умер)?")
|
||||||
if do_wd:
|
if do_wd:
|
||||||
from gpu_rent.local_watchdog import install_watchdog as _install
|
from gpu_rent.local_watchdog import install_watchdog as _install
|
||||||
|
|
||||||
|
|||||||
+51
-47
@@ -1,6 +1,6 @@
|
|||||||
"""SSH local forward with Nova watchdog.
|
"""SSH local forward with Nova watchdog.
|
||||||
|
|
||||||
Ctrl+C closes the tunnel and leaves the GPU. Ctrl+D (EOF) runs ``stop``.
|
Ctrl+C and Ctrl+D (EOF) both run ``stop`` (disks kept).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -242,7 +242,7 @@ def run_tunnel(
|
|||||||
current_host = host
|
current_host = host
|
||||||
for loc, rem in forwards:
|
for loc, rem in forwards:
|
||||||
log(f"туннель 127.0.0.1:{loc} -> {current_host}:{rem}")
|
log(f"туннель 127.0.0.1:{loc} -> {current_host}:{rem}")
|
||||||
log("Ctrl+C — туннель off, GPU жив. Ctrl+D — stop GPU (диски остаются).")
|
log("Ctrl+C / Ctrl+D — stop GPU (диски остаются).")
|
||||||
log("watchdog: EXPIRED → unshelve + reconnect")
|
log("watchdog: EXPIRED → unshelve + reconnect")
|
||||||
|
|
||||||
server = _start_forwarder(cfg, current_host, forwards)
|
server = _start_forwarder(cfg, current_host, forwards)
|
||||||
@@ -255,46 +255,61 @@ def run_tunnel(
|
|||||||
else:
|
else:
|
||||||
open_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
open_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
||||||
|
|
||||||
from gpu_rent.ready import verify_stack_local
|
|
||||||
|
|
||||||
try:
|
|
||||||
local_checks = verify_stack_local(cfg, log, timeout=90.0)
|
|
||||||
state = load_state()
|
|
||||||
state.notes = dict(state.notes or {})
|
|
||||||
state.notes["stack_local"] = [
|
|
||||||
{"name": c.name, "ok": c.ok, "detail": c.detail} for c in local_checks
|
|
||||||
]
|
|
||||||
save_state(state)
|
|
||||||
except CloudError as exc:
|
|
||||||
log(f"проверка туннеля: {exc}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
from gpu_rent.access_card import print_access_card
|
|
||||||
|
|
||||||
print_access_card(cfg, tunneled=True, host=current_host)
|
|
||||||
|
|
||||||
if open_browser:
|
|
||||||
webbrowser.open(open_url)
|
|
||||||
|
|
||||||
state = load_state()
|
|
||||||
state.phase = "ready_tunneled"
|
|
||||||
save_state(state)
|
|
||||||
|
|
||||||
from gpu_rent.local_watchdog import (
|
from gpu_rent.local_watchdog import (
|
||||||
detach_lease_keep_gpu,
|
clear_lease,
|
||||||
start_heartbeat_thread,
|
start_heartbeat_thread,
|
||||||
stop_heartbeat_thread,
|
stop_heartbeat_thread,
|
||||||
watchdog_installed,
|
watchdog_installed,
|
||||||
)
|
)
|
||||||
|
|
||||||
if watchdog_installed():
|
def _halt_gpu(reason: str) -> None:
|
||||||
start_heartbeat_thread()
|
nonlocal server
|
||||||
log(
|
log(f"{reason} — гашу GPU (диски остаются)")
|
||||||
"local-watchdog: heartbeat активен — аварийное закрытие "
|
_stop_forwarder(server)
|
||||||
"(не Ctrl+C / не Ctrl+D) → stop после grace"
|
server = None
|
||||||
)
|
stop_heartbeat_thread()
|
||||||
|
clear_lease()
|
||||||
|
if stop_gpu is not None:
|
||||||
|
stop_gpu()
|
||||||
|
else:
|
||||||
|
from gpu_rent.session import cmd_stop
|
||||||
|
|
||||||
|
cmd_stop(cfg, log=log)
|
||||||
|
log("туннель закрыт. GPU остановлен.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if watchdog_installed():
|
||||||
|
start_heartbeat_thread()
|
||||||
|
log(
|
||||||
|
"local-watchdog: heartbeat активен — kill/reboot без stop "
|
||||||
|
"→ stop после grace"
|
||||||
|
)
|
||||||
|
|
||||||
|
from gpu_rent.ready import verify_stack_local
|
||||||
|
|
||||||
|
try:
|
||||||
|
local_checks = verify_stack_local(cfg, log, timeout=90.0)
|
||||||
|
state = load_state()
|
||||||
|
state.notes = dict(state.notes or {})
|
||||||
|
state.notes["stack_local"] = [
|
||||||
|
{"name": c.name, "ok": c.ok, "detail": c.detail} for c in local_checks
|
||||||
|
]
|
||||||
|
save_state(state)
|
||||||
|
except CloudError as exc:
|
||||||
|
log(f"проверка туннеля: {exc}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
from gpu_rent.access_card import print_access_card
|
||||||
|
|
||||||
|
print_access_card(cfg, tunneled=True, host=current_host)
|
||||||
|
|
||||||
|
if open_browser:
|
||||||
|
webbrowser.open(open_url)
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
state.phase = "ready_tunneled"
|
||||||
|
save_state(state)
|
||||||
|
|
||||||
if wait is not None:
|
if wait is not None:
|
||||||
wait()
|
wait()
|
||||||
return
|
return
|
||||||
@@ -303,17 +318,7 @@ def run_tunnel(
|
|||||||
next_poll = time.time() + poll_seconds
|
next_poll = time.time() + poll_seconds
|
||||||
while True:
|
while True:
|
||||||
if end_poll(1.0):
|
if end_poll(1.0):
|
||||||
log("Ctrl+D — гашу GPU (диски остаются)")
|
_halt_gpu("Ctrl+D")
|
||||||
_stop_forwarder(server)
|
|
||||||
server = None
|
|
||||||
stop_heartbeat_thread()
|
|
||||||
if stop_gpu is not None:
|
|
||||||
stop_gpu()
|
|
||||||
else:
|
|
||||||
from gpu_rent.session import cmd_stop
|
|
||||||
|
|
||||||
cmd_stop(cfg, log=log)
|
|
||||||
log("туннель закрыт. GPU остановлен.")
|
|
||||||
return
|
return
|
||||||
if not server.is_active:
|
if not server.is_active:
|
||||||
next_poll = 0
|
next_poll = 0
|
||||||
@@ -350,8 +355,7 @@ def run_tunnel(
|
|||||||
log(f"unshelve/reconnect fail: {exc}")
|
log(f"unshelve/reconnect fail: {exc}")
|
||||||
return
|
return
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
detach_lease_keep_gpu()
|
_halt_gpu("Ctrl+C")
|
||||||
log("Ctrl+C — туннель закрыт. GPU жив.")
|
|
||||||
finally:
|
finally:
|
||||||
stop_heartbeat_thread()
|
stop_heartbeat_thread()
|
||||||
_stop_forwarder(server)
|
_stop_forwarder(server)
|
||||||
|
|||||||
+3
-3
@@ -49,9 +49,9 @@ def cost_and_risk_lines(cfg: Config, *, spot: bool, flavor_name: str) -> list[st
|
|||||||
f"{cfg.idle_minutes} мин пустой очереди → delete compute. Отложить: gpu-rent hold",
|
f"{cfg.idle_minutes} мин пустой очереди → delete compute. Отложить: gpu-rent hold",
|
||||||
"preemptible: хостер может усыпить (~24 ч окно) → EXPIRED; tunnel сам unshelve, "
|
"preemptible: хостер может усыпить (~24 ч окно) → EXPIRED; tunnel сам unshelve, "
|
||||||
"или gpu-rent up",
|
"или gpu-rent up",
|
||||||
"Ctrl+C на tunnel — туннель off, GPU жив. Ctrl+D — stop GPU (диски остаются). "
|
"Ctrl+C / Ctrl+D на tunnel — stop GPU (диски остаются). "
|
||||||
"Опционально: gpu-rent watchdog install — аварийное закрытие окна/ребут "
|
"Опционально: gpu-rent watchdog install — kill/reboot окна "
|
||||||
"после grace тоже stop (Ctrl+C по-прежнему detach)",
|
"после grace тоже stop",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ def _stub_tunnel(monkeypatch) -> None:
|
|||||||
monkeypatch.setattr("gpu_rent.tunnel.save_state", lambda s: None)
|
monkeypatch.setattr("gpu_rent.tunnel.save_state", lambda s: None)
|
||||||
monkeypatch.setattr("gpu_rent.access_card.print_access_card", lambda *a, **k: None)
|
monkeypatch.setattr("gpu_rent.access_card.print_access_card", lambda *a, **k: None)
|
||||||
monkeypatch.setattr("gpu_rent.local_watchdog.watchdog_installed", lambda: False)
|
monkeypatch.setattr("gpu_rent.local_watchdog.watchdog_installed", lambda: False)
|
||||||
|
monkeypatch.setattr("gpu_rent.local_watchdog.clear_lease", lambda: None)
|
||||||
|
|
||||||
|
|
||||||
def test_run_tunnel_ctrl_d_stops_gpu(monkeypatch):
|
def test_run_tunnel_ctrl_d_stops_gpu(monkeypatch):
|
||||||
@@ -163,7 +164,7 @@ def test_run_tunnel_ctrl_d_stops_gpu(monkeypatch):
|
|||||||
assert any("GPU остановлен" in x for x in logs)
|
assert any("GPU остановлен" in x for x in logs)
|
||||||
|
|
||||||
|
|
||||||
def test_run_tunnel_ctrl_c_keeps_gpu(monkeypatch):
|
def test_run_tunnel_ctrl_c_stops_gpu(monkeypatch):
|
||||||
_stub_tunnel(monkeypatch)
|
_stub_tunnel(monkeypatch)
|
||||||
logs: list[str] = []
|
logs: list[str] = []
|
||||||
stopped: list[bool] = []
|
stopped: list[bool] = []
|
||||||
@@ -178,5 +179,6 @@ def test_run_tunnel_ctrl_c_keeps_gpu(monkeypatch):
|
|||||||
wait=boom,
|
wait=boom,
|
||||||
stop_gpu=lambda: stopped.append(True),
|
stop_gpu=lambda: stopped.append(True),
|
||||||
)
|
)
|
||||||
assert stopped == []
|
assert stopped == [True]
|
||||||
assert any("GPU жив" in x for x in logs)
|
assert any("GPU остановлен" in x for x in logs)
|
||||||
|
assert not any("GPU жив" in x for x in logs)
|
||||||
|
|||||||
Reference in New Issue
Block a user