Complete DDS training workflow and delivery package
This commit is contained in:
parent
68dd83c7c2
commit
4c4b91064f
229 changed files with 11969 additions and 1024 deletions
118
backend/scripts/local_llms.py
Normal file
118
backend/scripts/local_llms.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Запуск двух локальных GGUF-серверов через llama.cpp на Windows/macOS/Linux.
|
||||
|
||||
Никаких загрузок при старте: GGUF предварительно кладутся в models/ через
|
||||
download_local_models.py. Путь к llama-server задаётся LLAMA_SERVER_BIN либо
|
||||
берётся из PATH. Ctrl+C останавливает дочерние процессы.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILES = {
|
||||
"dialogue": ("qwen3-1.7b/Qwen3-1.7B-Q8_0.gguf", "Qwen3-1.7B", 18080),
|
||||
"russian_control": ("vikhr-1b/Vikhr-Llama-3.2-1B-Q4_K_M.gguf", "Vikhr-1B", 18081),
|
||||
}
|
||||
|
||||
|
||||
def binary_path() -> str:
|
||||
bundled_mac = ROOT / "models" / "bin" / "llama-b10934" / "llama-server"
|
||||
binary = (os.environ.get("LLAMA_SERVER_BIN") or shutil.which("llama-server")
|
||||
or shutil.which("llama-server.exe")
|
||||
# The checked-out helper is Mach-O arm64. Do not select it on a
|
||||
# Windows machine merely because the whole models/ folder was
|
||||
# copied there for its platform-neutral GGUF weights.
|
||||
or (str(bundled_mac) if sys.platform == "darwin" and bundled_mac.is_file() else None))
|
||||
if not binary:
|
||||
raise RuntimeError("нет llama-server; установите бинарник llama.cpp и задайте LLAMA_SERVER_BIN")
|
||||
if not Path(binary).is_file():
|
||||
raise RuntimeError(f"llama-server не найден: {binary}")
|
||||
return binary
|
||||
|
||||
|
||||
def command(binary: str, profile: str, threads: int) -> list[str]:
|
||||
relative, alias, port = PROFILES[profile]
|
||||
model = ROOT / "models" / relative
|
||||
if not model.is_file():
|
||||
raise RuntimeError(f"нет модели: {model} — запустите make local-models")
|
||||
result = [binary, "-m", str(model), "--alias", alias,
|
||||
"--host", "127.0.0.1", "--port", str(port),
|
||||
"--ctx-size", "2048", "--threads", str(threads), "--parallel", "1",
|
||||
"--cors-origins", "localhost"]
|
||||
if profile == "dialogue":
|
||||
result += ["--reasoning-budget", "0"]
|
||||
return result
|
||||
|
||||
|
||||
def ready(port: int) -> bool:
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=1) as response:
|
||||
return response.status == 200
|
||||
except (OSError, urllib.error.HTTPError):
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--mode", choices=["dialogue", "russian_control", "both"], default="dialogue")
|
||||
parser.add_argument("--threads", type=int, default=4)
|
||||
args = parser.parse_args()
|
||||
if args.threads < 1:
|
||||
parser.error("--threads должен быть положительным")
|
||||
selected = list(PROFILES) if args.mode == "both" else [args.mode]
|
||||
try:
|
||||
binary = binary_path()
|
||||
commands = [command(binary, profile, args.threads) for profile in selected]
|
||||
except RuntimeError as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
return 2
|
||||
processes: list[subprocess.Popen] = []
|
||||
try:
|
||||
for profile, argv in zip(selected, commands):
|
||||
port = PROFILES[profile][2]
|
||||
if ready(port):
|
||||
raise RuntimeError(f"порт {port} уже занят сервером — не запускаю дубликат")
|
||||
print(f"запускаю {profile} на 127.0.0.1:{port}", flush=True)
|
||||
# Ctrl+C должен достаться управляющему процессу один раз: он сам
|
||||
# остановит дочерний сервер. Иначе llama.cpp получает двойной
|
||||
# SIGINT и на Metal иногда падает во время освобождения памяти.
|
||||
flags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
||||
processes.append(subprocess.Popen(
|
||||
argv, cwd=ROOT, creationflags=flags, start_new_session=os.name != "nt"
|
||||
))
|
||||
deadline = time.monotonic() + 120
|
||||
while time.monotonic() < deadline:
|
||||
if any(process.poll() is not None for process in processes):
|
||||
raise RuntimeError("один из серверов модели завершился до готовности")
|
||||
if all(ready(PROFILES[profile][2]) for profile in selected):
|
||||
print("локальные модели готовы; Ctrl+C остановит их", flush=True)
|
||||
while all(process.poll() is None for process in processes):
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("один из серверов модели неожиданно завершился")
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("модели не стали готовы за 120 секунд")
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except RuntimeError as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
for process in processes:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
for process in processes:
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in a new issue