Enhance GPU probing and performance tuning in provisioning

- Introduced GPU probing functionality to gather and store GPU specifications in `/mnt/swarm_data/.gpu-rent-gpu.json`, aiding in performance tuning.
- Updated `install_ollama.sh` and `install_llamacpp.sh` to utilize GPU information for configuring optimal runtime parameters.
- Enhanced `provision.py` to include GPU probing and performance tuning logic, ensuring better resource allocation for LLM operations.
- Improved documentation in `decisions.md`, `llm.md`, and `swarmui.md` to reflect changes in GPU handling and performance tuning processes.
- Added new tests to validate the GPU probing and model resolution logic, ensuring robustness in handling various GPU configurations.
This commit is contained in:
Leonid Pershin
2026-08-21 06:10:24 +03:00
parent 603165a4ba
commit 2ccb03f7d2
16 changed files with 1270 additions and 138 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Probe NVIDIA GPU → /mnt/swarm_data/.gpu-rent-gpu.json (stdlib only)."""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
OUT = Path("/mnt/swarm_data/.gpu-rent-gpu.json")
def tier_for_vram_mib(vram_mib: int) -> str:
gib = vram_mib / 1024.0
if gib < 16:
return "low"
if gib < 24:
return "mid"
if gib < 48:
return "high"
return "ultra"
def run(argv: list[str]) -> str:
try:
return subprocess.check_output(argv, text=True, stderr=subprocess.DEVNULL).strip()
except (OSError, subprocess.CalledProcessError):
return ""
def main() -> int:
if not run(["bash", "-lc", "command -v nvidia-smi"]):
data = {
"ok": False,
"error": "nvidia-smi missing",
"name": "",
"vram_mib": 0,
"compute_cap": "0.0",
"uuid": "",
"tier": "low",
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(json.dumps(data))
return 0
# name, uuid, memory.total [MiB], compute_cap
q = run(
[
"nvidia-smi",
"--query-gpu=name,uuid,memory.total,compute_cap",
"--format=csv,noheader,nounits",
]
)
if not q:
data = {
"ok": False,
"error": "nvidia-smi query failed",
"name": "",
"vram_mib": 0,
"compute_cap": "0.0",
"uuid": "",
"tier": "low",
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(json.dumps(data))
return 0
line = q.splitlines()[0]
# CSV: "NVIDIA A100-SXM4-40GB, GPU-..., 40960, 8.0"
parts = [p.strip() for p in line.split(",")]
name = parts[0] if parts else "unknown"
uuid = parts[1] if len(parts) > 1 else ""
vram_raw = parts[2] if len(parts) > 2 else "0"
cap = parts[3] if len(parts) > 3 else "0.0"
m = re.search(r"(\d+)", vram_raw.replace(" ", ""))
vram_mib = int(m.group(1)) if m else 0
tier = tier_for_vram_mib(vram_mib)
data = {
"ok": True,
"name": name,
"vram_mib": vram_mib,
"compute_cap": cap,
"uuid": uuid,
"tier": tier,
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(json.dumps(data))
return 0
if __name__ == "__main__":
sys.exit(main())
+25 -2
View File
@@ -110,9 +110,32 @@ else
log "нет GGUF в ${MODELS_DIR} — положи файл вручную и systemctl restart ${UNIT}"
fi
# GPU layers: share card with Swarm — full offload on mid+, leave headroom on low.
NGL=99
CTX=8192
if [[ -f "${DATA_ROOT}/.gpu-rent-gpu.json" ]]; then
eval "$(python3 - <<'PY'
import json
from pathlib import Path
gpu=json.loads(Path("/mnt/swarm_data/.gpu-rent-gpu.json").read_text())
vram=int(gpu.get("vram_mib") or 0)
gib=vram/1024.0
if gib < 16:
print("NGL=40"); print("CTX=4096")
elif gib < 24:
print("NGL=99"); print("CTX=8192")
elif gib < 48:
print("NGL=99"); print("CTX=16384")
else:
print("NGL=99"); print("CTX=32768")
PY
)" || true
fi
log "llama.cpp -ngl ${NGL} -c ${CTX}"
cat >/etc/systemd/system/${UNIT}.service <<EOF
[Unit]
Description=gpu-rent llama.cpp server (loopback)
Description=gpu-rent llama.cpp server (loopback, GPU-tuned)
After=network-online.target local-fs.target
Wants=network-online.target
@@ -121,7 +144,7 @@ Type=simple
User=${SWARM_USER}
Group=${SWARM_USER}
WorkingDirectory=${LLAMA_ROOT}
ExecStart=${SERVER_BIN} ${MODEL_ARG} --host 127.0.0.1 --port 8080
ExecStart=${SERVER_BIN} ${MODEL_ARG} --host 127.0.0.1 --port 8080 -ngl ${NGL} -c ${CTX}
Restart=on-failure
RestartSec=8
+91 -3
View File
@@ -1,11 +1,14 @@
#!/usr/bin/env bash
# Install Ollama on the VM (idempotent). Models on data volume.
# GPU-aware systemd env: flash attention, keep-alive, VRAM overhead for SwarmUI.
set -euo pipefail
SWARM_USER="${SWARM_USER:-ubuntu}"
DATA_ROOT="/mnt/swarm_data"
OLLAMA_HOME="${DATA_ROOT}/ollama"
UNIT="gpu-rent-ollama"
GPU_JSON="${DATA_ROOT}/.gpu-rent-gpu.json"
OLLAMA_ENV_FILE="${DATA_ROOT}/.gpu-rent-ollama.env"
log() { echo "[gpu-rent-ollama] $*"; }
@@ -17,6 +20,84 @@ fi
mkdir -p "$OLLAMA_HOME"
chown -R "${SWARM_USER}:${SWARM_USER}" "$OLLAMA_HOME"
# --- GPU probe → tier env (share card with SwarmUI / Krea 2) ---
write_ollama_env() {
python3 - <<'PY'
import json, os
from pathlib import Path
gpu_path = Path("/mnt/swarm_data/.gpu-rent-gpu.json")
out = Path("/mnt/swarm_data/.gpu-rent-ollama.env")
gpu = {}
if gpu_path.is_file():
try:
gpu = json.loads(gpu_path.read_text())
except Exception:
gpu = {}
vram = int(gpu.get("vram_mib") or 0)
gib = vram / 1024.0
cap = str(gpu.get("compute_cap") or "0.0")
name = str(gpu.get("name") or "")
try:
parts = cap.split(".")
maj, mnr = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
ampere = (maj, mnr) >= (8, 0)
except ValueError:
ampere = False
if gib >= 48:
tier, overhead, keep, kv, flash = "ultra", 20 * 1024**3, "30m", "q8_0", True
elif gib >= 24:
tier, overhead, keep, kv, flash = "high", 14 * 1024**3, "15m", "q8_0", True
elif gib >= 16:
tier, overhead, keep, kv, flash = "mid", 10 * 1024**3, "5m", "q8_0", True
else:
tier, overhead, keep, kv, flash = "low", 6 * 1024**3, "2m", "q4_0", False
flash = bool(flash and (ampere or "A100" in name.upper() or "H100" in name.upper() or gib >= 16))
lines = [
f"# auto gpu-rent ollama tune tier={tier} gpu={name!r} vram_mib={vram}",
"OLLAMA_NUM_PARALLEL=1",
"OLLAMA_MAX_LOADED_MODELS=1",
f"OLLAMA_KEEP_ALIVE={keep}",
f"OLLAMA_GPU_OVERHEAD={overhead}",
]
if flash:
lines.append("OLLAMA_FLASH_ATTENTION=1")
if kv:
lines.append(f"OLLAMA_KV_CACHE_TYPE={kv}")
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"tier={tier} flash={int(flash)} keep={keep} overhead_gib={overhead/1024**3:.0f}")
PY
}
# Prefer probe file written by gpu_probe.py; if missing, try nvidia-smi quickly.
if [[ ! -f "$GPU_JSON" ]] && command -v nvidia-smi >/dev/null 2>&1; then
log "нет ${GPU_JSON} — быстрый probe"
python3 - <<'PY' || true
import json, re, subprocess
from pathlib import Path
out = Path("/mnt/swarm_data/.gpu-rent-gpu.json")
try:
q = subprocess.check_output(
["nvidia-smi", "--query-gpu=name,uuid,memory.total,compute_cap", "--format=csv,noheader,nounits"],
text=True, stderr=subprocess.DEVNULL,
).strip().splitlines()[0]
parts = [p.strip() for p in q.split(",")]
vram = int(re.search(r"(\d+)", parts[2]).group(1))
gib = vram / 1024.0
tier = "low" if gib < 16 else "mid" if gib < 24 else "high" if gib < 48 else "ultra"
data = {"ok": True, "name": parts[0], "uuid": parts[1], "vram_mib": vram, "compute_cap": parts[3], "tier": tier}
except Exception as e:
data = {"ok": False, "error": str(e), "name": "", "uuid": "", "vram_mib": 0, "compute_cap": "0.0", "tier": "low"}
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(data, indent=2) + "\n")
print(data)
PY
fi
write_ollama_env
log "ollama env: $(tr '\n' ' ' < "$OLLAMA_ENV_FILE")"
chown "${SWARM_USER}:${SWARM_USER}" "$OLLAMA_ENV_FILE" "$GPU_JSON" 2>/dev/null || true
if ! command -v ollama >/dev/null 2>&1; then
# Supply-chain: prefer a pinned GitHub release. Official install.sh is curl|sh without checksum.
# Override: OLLAMA_VERSION=0.6.5 OLLAMA_SHA256=<sha256 of ollama-linux-amd64.tgz>
@@ -55,9 +136,16 @@ fi
systemctl stop ollama 2>/dev/null || true
systemctl disable ollama 2>/dev/null || true
# Build Environment= lines from env file
ENV_LINES=""
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" =~ ^# ]] && continue
ENV_LINES+="Environment=${line}"$'\n'
done < "$OLLAMA_ENV_FILE"
cat >/etc/systemd/system/${UNIT}.service <<EOF
[Unit]
Description=gpu-rent Ollama (loopback)
Description=gpu-rent Ollama (loopback, GPU-tuned)
After=network-online.target local-fs.target
Wants=network-online.target
@@ -68,7 +156,7 @@ Group=${SWARM_USER}
Environment=HOME=/home/${SWARM_USER}
Environment=OLLAMA_HOST=127.0.0.1:11434
Environment=OLLAMA_MODELS=${OLLAMA_HOME}
ExecStart=$(command -v ollama) serve
${ENV_LINES}ExecStart=$(command -v ollama) serve
Restart=on-failure
RestartSec=5
@@ -81,4 +169,4 @@ systemctl enable "$UNIT"
systemctl restart "$UNIT"
sleep 2
systemctl is-active "$UNIT" >/dev/null
log "ok — OLLAMA_HOST=127.0.0.1:11434 models=${OLLAMA_HOME}"
log "ok — OLLAMA_HOST=127.0.0.1:11434 models=${OLLAMA_HOME} (GPU-tuned)"
+43 -12
View File
@@ -13,7 +13,8 @@ DATA = Path("/mnt/swarm_data")
MODELS = DATA / "Models"
OUT = Path("/tmp/gpu-rent-inventory.json")
WEIGHT_SUFFIXES = {".safetensors", ".ckpt", ".pt", ".pth", ".bin", ".gguf", ".sft", ".onnx"}
WEIGHT_SUFFIXES = {".safetensors", ".ckpt", ".gguf", ".sft", ".onnx"}
# .pt/.pth/.bin намеренно вне capture — слишком много ложных «моделей» (torch scripts).
# SwarmUI folder name → models.yaml kind
FOLDER_TO_KIND = {
@@ -51,19 +52,13 @@ def sha256_file(path: Path, chunk: int = 1024 * 1024) -> str:
return h.hexdigest()
def read_sidecar_ids(weight: Path) -> tuple[int | None, int | None]:
"""Return (version_id, model_id) from {stem}.civitai.json if present."""
sidecar = weight.parent / f"{weight.stem}.civitai.json"
if not sidecar.is_file():
return None, None
try:
data = json.loads(sidecar.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None, None
if not isinstance(data, dict):
return None, None
def _ids_from_dict(data: dict) -> tuple[int | None, int | None]:
vid = data.get("id")
if vid is None:
vid = data.get("modelVersionId") or data.get("versionId")
mid = data.get("modelId")
if mid is None and isinstance(data.get("model"), dict):
mid = data["model"].get("id")
try:
version_id = int(vid) if vid is not None else None
except (TypeError, ValueError):
@@ -75,6 +70,28 @@ def read_sidecar_ids(weight: Path) -> tuple[int | None, int | None]:
return version_id, model_id
def read_sidecar_ids(weight: Path) -> tuple[int | None, int | None]:
"""Return (version_id, model_id) from civitai/swarm sidecars if present."""
candidates = [
weight.parent / f"{weight.stem}.civitai.json",
weight.parent / f"{weight.stem}.swarm.json",
weight.parent / f"{weight.stem}.json",
]
for sidecar in candidates:
if not sidecar.is_file():
continue
try:
data = json.loads(sidecar.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if not isinstance(data, dict):
continue
version_id, model_id = _ids_from_dict(data)
if version_id is not None or model_id is not None:
return version_id, model_id
return None, None
def scan_models() -> list[dict]:
items: list[dict] = []
if not MODELS.is_dir():
@@ -92,6 +109,20 @@ def scan_models() -> list[dict]:
continue
rel = path.relative_to(MODELS).as_posix()
version_id, model_id = read_sidecar_ids(path)
# Full sidecar → skip expensive SHA256 (capture resolves URL locally).
if version_id is not None and model_id is not None:
items.append(
{
"kind": kind,
"rel": rel,
"name": path.name,
"sha256": None,
"version_id": version_id,
"model_id": model_id,
"sha_skipped": True,
}
)
continue
try:
digest = sha256_file(path)
except OSError as exc:
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""First-boot Swarm/Comfy performance tune: sageattention ExtraArgs + pip libs.
Idempotent. Marker: /mnt/swarm_data/.gpu-rent-perf-tuned
Re-runs if GPU uuid changed.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
DATA = Path("/mnt/swarm_data")
GPU_JSON = DATA / ".gpu-rent-gpu.json"
MARKER = DATA / ".gpu-rent-perf-tuned"
BACKENDS = DATA / "Data" / "Backends.fds"
COMFY_VENV_CANDIDATES = [
DATA / "dlbackend" / "comfy" / "venv" / "bin" / "pip",
DATA / "dlbackend" / "comfy" / "ComfyUI" / "venv" / "bin" / "pip",
Path("/opt/swarmui/dlbackend/comfy/venv/bin/pip"),
Path("/opt/swarmui/dlbackend/comfy/ComfyUI/venv/bin/pip"),
]
def load_gpu() -> dict:
if not GPU_JSON.is_file():
return {}
try:
return json.loads(GPU_JSON.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {}
def tier_notes(gpu: dict) -> dict:
"""Mirror gpu_rent.perf_tiers.swarm_tune_for (stdlib-only on VM)."""
vram = int(gpu.get("vram_mib") or 0)
gib = vram / 1024.0
if gib < 16:
tier = "low"
elif gib < 24:
tier = "mid"
elif gib < 48:
tier = "high"
else:
tier = "ultra"
cap = str(gpu.get("compute_cap") or "0.0")
try:
parts = cap.split(".")
maj, mnr = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
ampere = (maj, mnr) >= (8, 0)
except ValueError:
ampere = False
use_sage = ampere and tier in {"mid", "high", "ultra"}
return {
"tier": tier,
"use_sage": use_sage,
"extra_args": "--use-sage-attention" if use_sage else "",
"uuid": str(gpu.get("uuid") or ""),
"name": str(gpu.get("name") or ""),
}
def find_pip() -> Path | None:
for p in COMFY_VENV_CANDIDATES:
if p.is_file():
return p
# glob
for p in DATA.glob("dlbackend/**/venv/bin/pip"):
return p
return None
def patch_backends_extra_args(extra: str) -> bool:
"""Ensure ExtraArgs contains sage flag for Comfy self-start backends."""
if not extra:
return False
if not BACKENDS.is_file():
print(f"no {BACKENDS} yet — skip ExtraArgs (Comfy not registered)")
return False
text = BACKENDS.read_text(encoding="utf-8")
if "--use-sage-attention" in text:
print("Backends.fds already has --use-sage-attention")
return False
lines = text.splitlines()
changed = False
out = []
for line in lines:
if re.match(r"^(\s*)ExtraArgs:\s*$", line) or re.match(r"^(\s*)ExtraArgs:\s*\"\"\s*$", line):
indent = re.match(r"^(\s*)", line).group(1)
out.append(f"{indent}ExtraArgs: {extra}")
changed = True
elif re.match(r"^(\s*)ExtraArgs:\s+", line) and "--use-sage-attention" not in line:
out.append(line.rstrip() + f" {extra}")
changed = True
else:
out.append(line)
if not changed:
print("Backends.fds: no ExtraArgs field patched")
return False
BACKENDS.write_text("\n".join(out) + "\n", encoding="utf-8")
print(f"patched {BACKENDS} ExtraArgs += {extra}")
return True
def pip_install_sage(pip: Path) -> None:
print(f"pip install triton sageattention via {pip}")
env = dict(os.environ)
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# Best-effort: do not fail whole tune if wheels missing for this torch.
cmd = [str(pip), "install", "-U", "triton", "sageattention"]
try:
subprocess.check_call(cmd, env=env)
print("triton + sageattention installed")
except subprocess.CalledProcessError as exc:
print(f"WARN: pip install failed ({exc}) — ExtraArgs may no-op until fixed")
def main() -> int:
gpu = load_gpu()
plan = tier_notes(gpu)
prev = {}
if MARKER.is_file():
try:
prev = json.loads(MARKER.read_text(encoding="utf-8"))
except json.JSONDecodeError:
prev = {}
if prev.get("uuid") and prev.get("uuid") == plan["uuid"] and prev.get("extra_args") == plan["extra_args"]:
if prev.get("pip_ok") or not plan["use_sage"]:
print(f"perf tune already applied for {plan['name']} ({plan['tier']})")
return 0
print(f"perf tune: {plan['name']} tier={plan['tier']} sage={plan['use_sage']}")
pip_ok = False
restarted_needed = False
if plan["use_sage"]:
pip = find_pip()
if pip:
pip_install_sage(pip)
pip_ok = True
else:
print("Comfy venv pip not found yet — will retry next up")
if patch_backends_extra_args(plan["extra_args"]):
restarted_needed = True
marker = {
"uuid": plan["uuid"],
"name": plan["name"],
"tier": plan["tier"],
"extra_args": plan["extra_args"],
"pip_ok": pip_ok or not plan["use_sage"],
"restart_needed": restarted_needed,
}
MARKER.write_text(json.dumps(marker, indent=2) + "\n", encoding="utf-8")
print("wrote", MARKER)
if restarted_needed:
print("RESTART_SWARMUI=1")
return 0
if __name__ == "__main__":
sys.exit(main())