Загрузка данных
#!/usr/bin/env python3
"""
deploy_zcode.py — разворачивает GGUF на Lium, автоматически выбирая
САМЫЙ ДЕШЁВЫЙ подходящий узел (RTX 5090), и регистрирует провайдера на zcode.
Только stdlib. Настройка:
export LIUM_API_KEY=sk_... # ключ показывается один раз на https://lium.io
export SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" # или оставить — возьмёт из ~/.ssh
Использование:
python3 deploy_zcode.py # dry-run: показать выбранный узел и цену
python3 deploy_zcode.py --rent # реально арендовать и развернуть
python3 deploy_zcode.py --rent --hours 2
python3 deploy_zcode.py --rent --quant Q5_K_M # другой quant
python3 deploy_zcode.py --rent --gpu "RTX 4090" # другая модель GPU (по умолчанию 5090)
"""
import argparse, json, os, ssl, subprocess, sys, time, urllib.request, urllib.error
API_BASE = "https://lium.io/api"
TEMPLATE_ID = "587dd7ff-d68e-43bd-a54e-b7c9bb261f0c" # light sshd, проверен
HF_REPO = "DavidAU/Qwen3.8-27B-TURBO-Fable-Cold-Fusion-735-882-Heretic-Uncensored-NEO-CODER-MAX-MTP-GGUF"
DEFAULT_QUANT = "Q4_K_M"
DEFAULT_GPU = "RTX 5090"
# ---------- helpers ----------
def api_key():
k = os.environ.get("LIUM_API_KEY")
if k:
return k.strip()
here = os.path.dirname(os.path.abspath(__file__))
for name in ("deploy.env", ".env"):
p = os.path.join(here, name)
if os.path.exists(p):
for line in open(p):
if line.strip().startswith("LIUM_API_KEY="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
return None
def api(method, path, payload=None):
url = API_BASE + "/" + path.lstrip("/")
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(
url, data=data, method=method,
headers={"Content-Type": "application/json", "X-API-Key": api_key()})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read().decode())
except urllib.error.URLError as e:
# fallback для хостов со stale CA bundle
if isinstance(getattr(e, "reason", None), ssl.SSLError):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with urllib.request.urlopen(req, timeout=30, context=ctx) as r:
return r.status, json.loads(r.read().decode())
return 0, str(e)
except urllib.error.HTTPError as e:
return e.code, e.read().decode()[:300]
def ssh(host, port, cmd, timeout=60):
try:
r = subprocess.run(
["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=15",
"-p", str(port), "root@%s" % host, cmd],
capture_output=True, text=True, timeout=timeout)
return (r.stdout + r.stderr)[-3000:]
except Exception as e:
return "ERR %s" % e
# ---------- 1. авто-выбор самого дешёвого узла ----------
def pick_cheapest_executor(gpu_name):
"""
Возвращает (executor_id, info) — самый дешёвый узел, удовлетворяющий:
* доступен хотя бы 1 GPU нужного типа,
* узел single-GPU (gpu_count == 1) ИЛИ полностью свободен
(available_gpu_count == gpu_count) — иначе Lium отдаёт 400 "GPU splitting".
Сортировка — по price_per_gpu (возрастание).
"""
st, execs = api("GET", "executors?size=1000")
if st != 200 or not isinstance(execs, list):
print("Не удалось получить список executors:", st, execs); sys.exit(1)
target = gpu_name.lower()
candidates = []
for e in execs:
gname = (e.get("gpu_name") or e.get("gpu_type") or "").lower()
if target not in gname:
continue
avail = int(e.get("available_gpu_count") or 0)
total = int(e.get("gpu_count") or 0)
if avail < 1:
continue
# правило против 400 "GPU splitting"
if not (total == 1 or avail == total):
continue
price = float(e.get("price_per_gpu") or 1e9)
# бонус за быстрые гео (US/CA) — но выбираем именно по цене
country = (e.get("location") or {}).get("country_code", "??")
speed = e.get("effective_download_speed_mbps") or 0
candidates.append({
"id": e.get("id"),
"machine": e.get("machine_name"),
"country": country,
"price": price,
"speed": speed,
"total": total,
"avail": avail,
"gpu": e.get("gpu_name") or e.get("gpu_type"),
})
if not candidates:
print("Нет доступных узлов с GPU '%s'." % gpu_name); sys.exit(1)
candidates.sort(key=lambda c: (c["price"], -c["speed"]))
return candidates[0], candidates[:8]
# ---------- 2. рент ----------
def rent(exec_id, pubkey, hours):
st, body = api("POST", "executors/%s/rent" % exec_id, {
"pod_name": "zcode-deploy",
"template_id": TEMPLATE_ID,
"gpu_count": 1,
"user_public_key": pubkey, # ВАЖНО: строка, не массив
"termination_hours": hours, # авто-удаление; продлить нельзя
"initial_port_count": 2, # 22 (ssh) + 11434 (ollama)
"enable_jupyter": False,
"enable_volume_encryption": False,
})
print("rent ->", st, str(body)[:200])
if st in (200, 201) and isinstance(body, dict):
return body.get("pod_id")
return None
def wait_running(pod_id):
for i in range(36):
st, body = api("GET", "pods/%s" % pod_id)
if st != 200:
print("pod resp", st, str(body)[:120]); return None, None, None
status = body.get("status")
print("[%3ds] %s" % (i * 10, status))
if status == "RUNNING":
raw = body.get("ssh_connect_cmd", "")
# "ssh root@1.2.3.4 -p 50009"
parts = raw.replace("ssh root@", "").split(" -p ")
host, port = parts[0].strip(), int(parts[1].strip())
return host, port, body
if status in ("CREATION_FAILED", "ERROR", "REMOVED"):
print("FAILED:", status); return None, None, None
time.sleep(10)
return None, None, None
# ---------- 3. zcode settings.yaml ----------
def register_zcode(model_id, context, provider_key="lium-zcode-qwen"):
zcode_dir = os.path.expanduser("~/.zcode")
os.makedirs(zcode_dir, exist_ok=True)
path = os.path.join(zcode_dir, "settings.yaml")
block = f"""
{provider_key}:
displayName: Qwen3.8-27B TURBO Heretic (Lium cheapest 5090)
api: openai-completions
baseURL: http://127.0.0.1:11435/v1
streamIdleTimeoutMs: 900000
timeoutMs: 1800000
reasoning: max
headers:
Authorization: Bearer local
compat:
thinkingFormat: qwen-chat-template
supportsReasoningEffort: false
models:
- id: '{model_id}'
name: Qwen3.8-27B TURBO NEO-CODER (RTX 5090)
contextWindow: {context}
maxTokens: 32768
reasoningEfforts:
'off': null
low: low
medium: medium
high: high
xhigh: xhigh
max: max
"""
existing = open(path).read() if os.path.exists(path) else ""
if provider_key in existing:
print("zcode: провайдер '%s' уже есть — пропускаю запись." % provider_key)
return path
with open(path, "a") as f:
if "providers:" not in existing:
f.write("llm-pi-ai:\n providers:\n")
f.write(block)
return path
# ---------- main ----------
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--rent", action="store_true", help="реально арендовать (тратит деньги)")
ap.add_argument("--hours", type=int, default=1)
ap.add_argument("--context", type=int, default=200000)
ap.add_argument("--quant", default=DEFAULT_QUANT, help="Q4_K_M / Q5_K_M / Q6_K ...")
ap.add_argument("--gpu", default=DEFAULT_GPU, help="имя GPU, напр. 'RTX 5090'")
ap.add_argument("--model", default=None, help="полный HF-репо (по умолчанию DavidAU/...)")
o = ap.parse_args()
if not api_key():
print("Нет LIUM_API_KEY. Экспортируйте переменную или создайте deploy.env рядом со скриптом.")
sys.exit(1)
repo = o.model or HF_REPO
model_id = f"hf.co/{repo}:{o.quant}"
# --- выбор узла ---
best, top = pick_cheapest_executor(o.gpu)
print("\n=== Топ-8 самых дешёвых узлов '%s' (с учётом правил splitting) ===" % o.gpu)
for c in top:
print(" $%5.3f/h %-30s %-3s %4.0f MB/s avail=%d/%d id=%s"
% (c["price"], c["machine"][:30], c["country"], c["speed"],
c["avail"], c["total"], c["id"][:8]))
print("\nВЫБРАН: $%.3f/h %s (%s) id=%s"
% (best["price"], best["machine"], best["country"], best["id"]))
print("Модель: %s" % model_id)
print("Контекст: %d | Время аренды: %d ч | Оценка стоимости: ~$%.2f"
% (o.context, o.hours, best["price"] * o.hours))
if not o.rent:
print("\n[dry-run] Ничего не арендовано. Запусти с --rent, чтобы выполнить.")
return
pub = os.environ.get("SSH_PUBKEY")
if not pub:
for k in ("id_ed25519.pub", "id_rsa.pub"):
p = os.path.expanduser("~/.ssh/" + k)
if os.path.exists(p):
pub = open(p).read().strip(); break
if not pub:
print("Нет SSH-ключа (~/.ssh/id_ed25519.pub) и не задан SSH_PUBKEY."); sys.exit(1)
# --- рент ---
pod_id = rent(best["id"], pub, o.hours)
if not pod_id:
print("Рент не удался — попробуй следующий узел из топа вручную."); sys.exit(1)
print("POD %s" % pod_id)
host, port, _ = wait_running(pod_id)
if not host:
print("Под не достиг RUNNING."); sys.exit(1)
print("SSH root@%s -p %d" % (host, port))
# --- ollama ---
print("\n--- install ollama ---")
print(ssh(host, port,
"apt-get update -qq >/dev/null 2>&1 || true; "
"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq zstd curl >/dev/null 2>&1 || true; "
"curl -fsSL https://ollama.com/install.sh -o /tmp/ol.sh; sh /tmp/ol.sh >/tmp/ol_inst.log 2>&1; "
"ollama --version", timeout=300)[-400:])
# --- pull (не прерывать!) ---
print("\n--- ollama pull (sha256-gated, НЕ ПРЕРЫВАТЬ) ---")
print(ssh(host, port, "nohup ollama pull %s >/tmp/pull.log 2>&1 & echo BG" % model_id,
timeout=30)[-120:])
for _ in range(150):
time.sleep(20)
out = ssh(host, port,
"grep -q success /tmp/pull.log && echo DONE || echo RUN; "
"tail -1 /tmp/pull.log", timeout=30)
if "DONE" in out:
print("PULL SUCCESS:", out.strip().splitlines()[0]); break
if out.startswith("ERR"):
print("ssh err:", out[-200:]); break
else:
print("Pull не завершился за отведённое время — проверь /tmp/pull.log вручную.")
# --- serve ---
print("\n--- serve 0.0.0.0:%d ---" % 11434)
print(ssh(host, port,
"pkill -x ollama 2>/dev/null; sleep 1; "
"setsid env OLLAMA_HOST=0.0.0.0 OLLAMA_CONTEXT_LENGTH=%d "
"ollama serve >>/tmp/ol.log 2>&1 </dev/null & "
"sleep 4; curl -s http://localhost:11434/api/version" % o.context, timeout=40)[-300:])
# --- zcode ---
path = register_zcode(model_id, o.context)
print("\nzcode settings: %s" % path)
print("\n=== ТУННЕЛЬ (держать запущенным) ===")
print(" ssh -N -L 11435:localhost:11434 -p %d root@%s" % (port, host))
print("\nГотово. Открой zcode GUI — модель появится в списке. "
"Туннель должен быть активен всё время работы.")
if __name__ == "__main__":
main()