Update configuration and documentation for LLM support and local watchdog
- Added `ollama-models.yaml` to .gitignore and implemented logic to copy it in gpu-rent.ps1 and gpu-rent.sh. - Enhanced env.example to include new variables for LLM runtime options and local watchdog configuration. - Updated CLI commands to support LLM options during setup and execution, including new flags for Ollama and llama.cpp. - Improved documentation in cli.md and README.md to reflect changes in LLM integration and local watchdog functionality. - Adjusted architecture and decisions documentation to clarify the role of LLMs and local watchdog in the system.
This commit is contained in:
+223
-10
@@ -33,22 +33,26 @@ if sys.platform == "win32":
|
||||
pass
|
||||
|
||||
app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
invoke_without_command=True,
|
||||
pretty_exceptions_enable=False,
|
||||
add_completion=False,
|
||||
help="Прерываемый GPU Selectel + SwarmUI на localhost:17801. Сначала: gpu-rent doctor. Ключи: docs/setup.md",
|
||||
help="Прерываемый GPU Selectel + SwarmUI на localhost:17801. Сначала: gpu-rent setup / doctor. Ключи: docs/setup.md",
|
||||
)
|
||||
console = Console(highlight=False, legacy_windows=False)
|
||||
|
||||
_DEBUG = False
|
||||
|
||||
|
||||
@app.callback()
|
||||
@app.callback(invoke_without_command=True)
|
||||
def _root(
|
||||
ctx: typer.Context,
|
||||
debug: bool = typer.Option(False, "--debug", help="Показать traceback"),
|
||||
) -> None:
|
||||
global _DEBUG
|
||||
_DEBUG = debug
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Без подкоманды → interactive up (как «запуск без параметров»).
|
||||
ctx.invoke(up)
|
||||
|
||||
|
||||
def _die(exc: BaseException) -> None:
|
||||
@@ -249,6 +253,20 @@ def status() -> None:
|
||||
table.add_row("диск used/free", "нужен живой FIP + SSH-ключ")
|
||||
table.add_row("idle-killer", "нужен SSH на живую VM")
|
||||
|
||||
from gpu_rent.local_watchdog import watchdog_status_lines
|
||||
|
||||
table.add_row("local-watchdog", "; ".join(watchdog_status_lines()))
|
||||
from gpu_rent.llm_runtime import normalize_runtime
|
||||
|
||||
rt = normalize_runtime(cfg.llm_runtime)
|
||||
noted = (state.notes or {}).get("llm_runtime")
|
||||
if noted:
|
||||
rt = f"{rt} (сессия: {noted})"
|
||||
table.add_row(
|
||||
"LLM",
|
||||
f"{rt}; ollama :{cfg.ollama_local_port} / llamacpp :{cfg.llamacpp_local_port}",
|
||||
)
|
||||
|
||||
if cfg.auth_ok:
|
||||
try:
|
||||
conn = connect(cfg)
|
||||
@@ -271,10 +289,30 @@ def status() -> None:
|
||||
|
||||
|
||||
@app.command()
|
||||
def open() -> None:
|
||||
"""Открыть браузер на http://127.0.0.1:17801. Туннель уже должен слушать порт."""
|
||||
def open(
|
||||
llm: bool = typer.Option(False, "--llm", help="Открыть LLM API URL вместо SwarmUI"),
|
||||
) -> None:
|
||||
"""Открыть браузер на SwarmUI :17801 (или --llm на Ollama/llama.cpp)."""
|
||||
cfg = load_config(require_auth=False)
|
||||
port = cfg.swarmui_local_port
|
||||
if llm:
|
||||
from gpu_rent.llm_runtime import normalize_runtime
|
||||
|
||||
runtime = normalize_runtime(cfg.llm_runtime)
|
||||
state = load_state()
|
||||
if (state.notes or {}).get("llm_runtime"):
|
||||
try:
|
||||
runtime = normalize_runtime(str(state.notes["llm_runtime"]))
|
||||
except ValueError:
|
||||
pass
|
||||
if runtime == "ollama":
|
||||
port = cfg.ollama_local_port
|
||||
elif runtime == "llamacpp":
|
||||
port = cfg.llamacpp_local_port
|
||||
else:
|
||||
console.print("[red]LLM не выбран[/red] (LLM_RUNTIME / gpu-rent setup)")
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
port = cfg.swarmui_local_port
|
||||
if not _port_open(port):
|
||||
console.print(
|
||||
f"[red]localhost:{port} молчит.[/red] Сначала `gpu-rent tunnel`, потом open."
|
||||
@@ -285,6 +323,43 @@ def open() -> None:
|
||||
console.print(url)
|
||||
|
||||
|
||||
@app.command()
|
||||
def setup(
|
||||
llm: Optional[str] = typer.Option(None, "--llm", help="none|ollama|llamacpp"),
|
||||
ollama_preset: Optional[str] = typer.Option(
|
||||
None, "--ollama-preset", help="recommended|light|stock|alt|empty"
|
||||
),
|
||||
watchdog: Optional[bool] = typer.Option(
|
||||
None, "--watchdog/--no-watchdog", help="Поставить local-watchdog"
|
||||
),
|
||||
yes: bool = typer.Option(False, "--yes", help="Без вопросов (дефолты)"),
|
||||
) -> None:
|
||||
"""Интерактивная установка: файлы конфига, LLM, опционально watchdog."""
|
||||
try:
|
||||
from gpu_rent.setup_wizard import run_setup
|
||||
|
||||
def confirm(msg: str) -> bool:
|
||||
if yes:
|
||||
return False if watchdog is False else bool(watchdog)
|
||||
return typer.confirm(msg)
|
||||
|
||||
def ask(msg: str, default: str) -> str:
|
||||
if yes:
|
||||
return default
|
||||
return typer.prompt(msg, default=default)
|
||||
|
||||
run_setup(
|
||||
llm=llm if llm is not None else ("none" if yes else None),
|
||||
ollama_preset=ollama_preset if ollama_preset is not None else ("recommended" if yes else None),
|
||||
install_watchdog=watchdog if watchdog is not None else (False if yes else None),
|
||||
confirm=None if yes and watchdog is None else confirm,
|
||||
ask=None if yes and llm is not None else ask,
|
||||
log=lambda m: console.print(m),
|
||||
)
|
||||
except Exception as exc:
|
||||
_die(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def up(
|
||||
no_spot: bool = typer.Option(False, "--no-spot", help="Обычный сервер, не preemptible"),
|
||||
@@ -306,15 +381,67 @@ def up(
|
||||
"--no-update",
|
||||
help="Не делать git pull SwarmUI и установленных extensions",
|
||||
),
|
||||
llm: Optional[str] = typer.Option(
|
||||
None, "--llm", help="none|ollama|llamacpp (override LLM_RUNTIME)"
|
||||
),
|
||||
ollama: bool = typer.Option(False, "--ollama", help="То же что --llm ollama"),
|
||||
llamacpp: bool = typer.Option(False, "--llamacpp", help="То же что --llm llamacpp"),
|
||||
) -> None:
|
||||
"""Create/unshelve GPU, bootstrap SwarmUI, по умолчанию туннель на :17801."""
|
||||
try:
|
||||
from dataclasses import replace
|
||||
|
||||
from gpu_rent.llm_runtime import (
|
||||
PRESET_HELP,
|
||||
append_vars_llm_runtime,
|
||||
decide_runtime,
|
||||
ensure_ollama_manifest_from_example,
|
||||
write_ollama_models_preset,
|
||||
)
|
||||
from gpu_rent.paths import vars_path
|
||||
|
||||
checks = run_doctor()
|
||||
code = _print_checks(checks)
|
||||
if code != 0:
|
||||
raise typer.Exit(1)
|
||||
cfg = load_config(require_auth=True)
|
||||
|
||||
try:
|
||||
runtime = decide_runtime(
|
||||
flag=llm,
|
||||
ollama_flag=ollama,
|
||||
llamacpp_flag=llamacpp,
|
||||
from_config=cfg.llm_runtime,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise GpuRentError(str(exc)) from exc
|
||||
|
||||
if not yes and runtime == "none" and not llm and not ollama and not llamacpp:
|
||||
choice = typer.prompt(
|
||||
"Поднять LLM рядом со SwarmUI? [none/ollama/llamacpp]",
|
||||
default="none",
|
||||
)
|
||||
try:
|
||||
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 runtime == "ollama":
|
||||
ensure_ollama_manifest_from_example()
|
||||
console.print(PRESET_HELP)
|
||||
preset = typer.prompt("Ollama preset", default="recommended")
|
||||
if preset.strip().lower() not in {"keep", "example"}:
|
||||
write_ollama_models_preset(
|
||||
cfg.ollama_models_manifest, preset.strip().lower()
|
||||
)
|
||||
|
||||
cfg = replace(cfg, llm_runtime=runtime)
|
||||
if runtime != "none":
|
||||
console.print(f"LLM runtime: {runtime}")
|
||||
|
||||
def confirm(msg: str) -> bool:
|
||||
return typer.confirm(msg)
|
||||
|
||||
@@ -368,6 +495,7 @@ def stop(
|
||||
@app.command()
|
||||
def destroy(
|
||||
i_understand_data_loss: bool = typer.Option(False, "--i-understand-data-loss"),
|
||||
no_pull: bool = typer.Option(False, "--no-pull"),
|
||||
) -> None:
|
||||
"""stop + диски."""
|
||||
if not i_understand_data_loss:
|
||||
@@ -375,7 +503,7 @@ def destroy(
|
||||
raise typer.Exit(1)
|
||||
try:
|
||||
cfg = load_config(require_auth=True)
|
||||
cmd_stop(cfg, destroy_disks=True, log=lambda m: console.print(m))
|
||||
cmd_stop(cfg, destroy_disks=True, no_pull=no_pull, log=lambda m: console.print(m))
|
||||
except GpuRentError as exc:
|
||||
_die(exc)
|
||||
|
||||
@@ -395,7 +523,7 @@ def ssh() -> None:
|
||||
|
||||
@app.command()
|
||||
def logs() -> None:
|
||||
"""cloud-init / journalctl на VM."""
|
||||
"""cloud-init / journalctl -u swarmui на VM."""
|
||||
try:
|
||||
cfg = load_config(require_auth=True)
|
||||
state = load_state()
|
||||
@@ -404,9 +532,14 @@ def logs() -> None:
|
||||
out = run_ssh(
|
||||
cfg,
|
||||
state.floating_ip,
|
||||
"sudo -n tail -n 80 /var/log/cloud-init-output.log 2>/dev/null; "
|
||||
"systemctl is-active swarmui 2>/dev/null || true",
|
||||
"echo '=== cloud-init (tail) ==='; "
|
||||
"sudo -n tail -n 60 /var/log/cloud-init-output.log 2>/dev/null || true; "
|
||||
"echo; echo '=== systemctl swarmui ==='; "
|
||||
"systemctl is-active swarmui 2>/dev/null || true; "
|
||||
"echo; echo '=== journalctl -u swarmui ==='; "
|
||||
"sudo -n journalctl -u swarmui -n 80 --no-pager 2>/dev/null || true",
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
console.print(out)
|
||||
except GpuRentError as exc:
|
||||
@@ -538,6 +671,86 @@ def resize_data(gb: int = typer.Option(..., "--gb", help="Новый разме
|
||||
_die(exc)
|
||||
|
||||
|
||||
watchdog_app = typer.Typer(
|
||||
help=(
|
||||
"Локальный сервис: если туннель умер без Ctrl+C / stop — "
|
||||
"через grace удалить compute. Не путать с idle-killer на VM."
|
||||
),
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(watchdog_app, name="watchdog")
|
||||
|
||||
|
||||
def _apply_project_root(project: Optional[str]) -> None:
|
||||
if not project:
|
||||
return
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(project).expanduser().resolve()
|
||||
os.environ["GPU_RENT_ROOT"] = str(root)
|
||||
try:
|
||||
os.chdir(root)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@watchdog_app.command("install")
|
||||
def watchdog_install(
|
||||
interval: int = typer.Option(5, "--interval", help="Минуты между тиками"),
|
||||
) -> None:
|
||||
"""Поставить Task Scheduler / systemd user / launchd."""
|
||||
try:
|
||||
from gpu_rent.local_watchdog import install_watchdog
|
||||
|
||||
install_watchdog(interval_minutes=interval, log=lambda m: console.print(m))
|
||||
except Exception as exc:
|
||||
console.print(f"[red]install fail:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
|
||||
@watchdog_app.command("uninstall")
|
||||
def watchdog_uninstall() -> None:
|
||||
"""Снять локальный watchdog."""
|
||||
try:
|
||||
from gpu_rent.local_watchdog import uninstall_watchdog
|
||||
|
||||
uninstall_watchdog(log=lambda m: console.print(m))
|
||||
except Exception as exc:
|
||||
console.print(f"[red]uninstall fail:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
|
||||
@watchdog_app.command("status")
|
||||
def watchdog_status_cmd() -> None:
|
||||
"""Состояние установки и local lease."""
|
||||
from gpu_rent.local_watchdog import watchdog_status_lines
|
||||
|
||||
for line in watchdog_status_lines():
|
||||
console.print(line)
|
||||
|
||||
|
||||
@watchdog_app.command("tick")
|
||||
def watchdog_tick(
|
||||
project: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--project",
|
||||
help="Корень репо (для планировщика; выставляет GPU_RENT_ROOT)",
|
||||
),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Только решение, без stop"),
|
||||
) -> None:
|
||||
"""Один тик (вызывает планировщик)."""
|
||||
_apply_project_root(project)
|
||||
try:
|
||||
from gpu_rent.local_watchdog import run_tick
|
||||
|
||||
decision = run_tick(dry_run=dry_run, log=lambda m: console.print(m))
|
||||
if decision.kind == "stop" and not dry_run:
|
||||
raise typer.Exit(0)
|
||||
except GpuRentError as exc:
|
||||
_die(exc)
|
||||
|
||||
|
||||
def _port_open(port: int) -> bool:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.4)
|
||||
|
||||
Reference in New Issue
Block a user