- Столбец VRAM пересчитан на чистое потребление процессов (nvidia-smi --query-compute-apps) - В условиях измерений явно зафиксирован контекст 32k токенов и сопоставлен со штатным режимом 192k (13.6 ток/с) - Добавлен раздел по соответствию 64k порогу Hermes (Qwen3.8-27B, Qwen2.5-Coder-14B, Granite-4.2-8B) - Проведены живые замеры одновременного размещения: 3 модели помещаются в 31 120 MiB (95% VRAM), 4 модели вызывают CUDA OOM (38 526 MiB) - Установлен и настроен llama-swap на порту 8090 с автоматической выгрузкой VRAM по TTL - Удалён незавершённый файл nemotron-3.5-30b
282 lines
12 KiB
Python
282 lines
12 KiB
Python
"""Script to perform live, honest VRAM measurements of solo and concurrent model deployments."""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
import urllib.request
|
|
from typing import Dict, List, Tuple
|
|
|
|
|
|
def get_gpu_compute_apps() -> List[Dict[str, str]]:
|
|
"""Query nvidia-smi for all active compute processes on GPU."""
|
|
try:
|
|
res = subprocess.run(
|
|
["nvidia-smi", "--query-compute-apps=pid,process_name,used_memory", "--format=csv,noheader,nounits"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
apps = []
|
|
for line in res.stdout.strip().split("\n"):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
parts = [p.strip() for p in line.split(",")]
|
|
if len(parts) >= 3:
|
|
apps.append({
|
|
"pid": int(parts[0]),
|
|
"process_name": parts[1],
|
|
"used_memory_mib": int(parts[2]),
|
|
})
|
|
return apps
|
|
except Exception as e:
|
|
print(f"Error querying nvidia-smi: {e}")
|
|
return []
|
|
|
|
|
|
def get_total_vram_used() -> int:
|
|
try:
|
|
res = subprocess.run(
|
|
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
return int(res.stdout.strip().split()[0])
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def ping_health(port: int, timeout: int = 40) -> bool:
|
|
t0 = time.time()
|
|
while time.time() - t0 < timeout:
|
|
try:
|
|
req = urllib.request.Request(f"http://127.0.0.1:{port}/health")
|
|
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
data = json.loads(resp.read().decode())
|
|
if data.get("status") == "ok":
|
|
return True
|
|
except Exception:
|
|
pass
|
|
time.sleep(1)
|
|
return False
|
|
|
|
|
|
def test_completion(port: int, model_path: str) -> Tuple[bool, float, str]:
|
|
req_body = {
|
|
"model": model_path,
|
|
"messages": [{"role": "user", "content": "Respond exact word 'PONG'"}],
|
|
"max_tokens": 10,
|
|
"temperature": 0.1,
|
|
}
|
|
t0 = time.monotonic()
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{port}/v1/chat/completions",
|
|
data=json.dumps(req_body).encode("utf-8"),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
elapsed = time.monotonic() - t0
|
|
raw = json.loads(resp.read().decode())
|
|
content = raw["choices"][0]["message"]["content"]
|
|
return True, elapsed, content.strip()
|
|
except Exception as e:
|
|
return False, time.monotonic() - t0, str(e)
|
|
|
|
|
|
def run_solo_measurement(name: str, model_path: str, ctx: int, port: int = 8089) -> Dict[str, any]:
|
|
print(f"\n[*] Measuring Solo: {name} (ctx={ctx})...", flush=True)
|
|
cmd = [
|
|
"/home/ochenstarik/llama.cpp/build/bin/llama-server",
|
|
"-m", model_path,
|
|
"-ngl", "99",
|
|
"-c", str(ctx),
|
|
"--parallel", "1",
|
|
"--flash-attn", "on",
|
|
"--cache-type-k", "q8_0",
|
|
"--cache-type-v", "q8_0",
|
|
"--reasoning", "off",
|
|
"--temp", "0.2",
|
|
"--host", "127.0.0.1",
|
|
"--port", str(port),
|
|
]
|
|
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
ok = ping_health(port, timeout=45)
|
|
if not ok:
|
|
print(f"[-] Failed to start {name} on port {port}", flush=True)
|
|
return {"name": name, "model_path": model_path, "ctx": ctx, "status": "START_FAILED"}
|
|
|
|
# Warmup
|
|
comp_ok, comp_time, comp_resp = test_completion(port, model_path)
|
|
|
|
apps = get_gpu_compute_apps()
|
|
proc_vram = next((a["used_memory_mib"] for a in apps if a["pid"] == p.pid), 0)
|
|
total_vram = get_total_vram_used()
|
|
|
|
print(f"[+] {name} (ctx={ctx}): Process VRAM = {proc_vram} MiB | Total GPU = {total_vram} MiB | Ping = {comp_time:.2f}s ('{comp_resp}')", flush=True)
|
|
return {
|
|
"name": name,
|
|
"model_path": model_path,
|
|
"ctx": ctx,
|
|
"process_vram_mib": proc_vram,
|
|
"total_gpu_mib": total_vram,
|
|
"status": "OK",
|
|
}
|
|
finally:
|
|
p.terminate()
|
|
try:
|
|
p.wait(timeout=5)
|
|
except Exception:
|
|
p.kill()
|
|
time.sleep(1)
|
|
|
|
|
|
def main():
|
|
print("===================================================================", flush=True)
|
|
print(" LIVE VRAM & CONCURRENCY BENCHMARK ON TESLA V100 32GB", flush=True)
|
|
print("===================================================================", flush=True)
|
|
|
|
models = [
|
|
("Qwen3.8-27B", "/srv/ai/models/qwen3.8-27b/Qwen3.8-27B-Q4_K_M.gguf", 32768),
|
|
("Qwen3.8-27B (192k ctx)", "/srv/ai/models/qwen3.8-27b/Qwen3.8-27B-Q4_K_M.gguf", 196608),
|
|
("Qwen2.5-Coder-32B", "/srv/ai/models/qwen2.5-coder-32b/Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf", 32768),
|
|
("Qwen2.5-Coder-14B", "/srv/ai/models/qwen2.5-coder-14b/qwen2.5-coder-14b-instruct-q4_k_m.gguf", 32768),
|
|
("Qwen2.5-Coder-14B (64k ctx)", "/srv/ai/models/qwen2.5-coder-14b/qwen2.5-coder-14b-instruct-q4_k_m.gguf", 65536),
|
|
("Phi-4-14B", "/srv/ai/models/phi-4-14b/phi-4-Q4_K_M.gguf", 16384),
|
|
("DeepSeek-Coder-V2-Lite", "/srv/ai/models/deepseek-coder-v2-lite/DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf", 32768),
|
|
("DeepSeek-Coder-V2-Lite (64k ctx)", "/srv/ai/models/deepseek-coder-v2-lite/DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf", 65536),
|
|
("Granite-4.2-8B", "/srv/ai/models/granite-4.2-8b/granite-4.2-8b-Q4_K_M.gguf", 32768),
|
|
("Granite-4.2-8B (64k ctx)", "/srv/ai/models/granite-4.2-8b/granite-4.2-8b-Q4_K_M.gguf", 65536),
|
|
("Granite-3.2-8B-Preview", "/srv/ai/models/granite-3.2-8b/granite-3.2-8b-instruct-preview.Q4_K_M.gguf", 32768),
|
|
("Qwen3-4B-Compressor", "/srv/ai/models/qwen3-4b-compressor/Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf", 32768),
|
|
("LFM2.5-2.6B", "/srv/ai/models/lfm2.5-2.6b/LFM2.5-2.6B-Q4_K_M.gguf", 16384),
|
|
]
|
|
|
|
solo_results = []
|
|
for name, path, ctx in models:
|
|
res = run_solo_measurement(name, path, ctx, port=8089)
|
|
solo_results.append(res)
|
|
|
|
with open("benchmarks/solo_vram_results.json", "w", encoding="utf-8") as f:
|
|
json.dump(solo_results, f, indent=2)
|
|
|
|
# -------------------------------------------------------------
|
|
# CONCURRENCY TEST: 3 Models simultaneously
|
|
# (Qwen2.5-Coder-14B + Granite-4.2-8B + Qwen3-4B-Compressor)
|
|
# -------------------------------------------------------------
|
|
print("\n=======================================================", flush=True)
|
|
print(" [*] TEST SCENARIO A: 3 Models Simultaneously on GPU", flush=True)
|
|
print(" 1. Qwen2.5-Coder-14B (32k)")
|
|
print(" 2. Granite-4.2-8B (32k)")
|
|
print(" 3. Qwen3-4B-Compressor (32k)")
|
|
print("=======================================================", flush=True)
|
|
|
|
procs = []
|
|
configs_3 = [
|
|
("qwen2.5-coder-14b", "/srv/ai/models/qwen2.5-coder-14b/qwen2.5-coder-14b-instruct-q4_k_m.gguf", 32768, 8083),
|
|
("granite-4.2-8b", "/srv/ai/models/granite-4.2-8b/granite-4.2-8b-Q4_K_M.gguf", 32768, 8084),
|
|
("qwen3-4b-compressor", "/srv/ai/models/qwen3-4b-compressor/Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf", 32768, 8085),
|
|
]
|
|
|
|
try:
|
|
for name, path, ctx, port in configs_3:
|
|
cmd = [
|
|
"/home/ochenstarik/llama.cpp/build/bin/llama-server",
|
|
"-m", path,
|
|
"-ngl", "99",
|
|
"-c", str(ctx),
|
|
"--parallel", "1",
|
|
"--flash-attn", "on",
|
|
"--cache-type-k", "q8_0",
|
|
"--cache-type-v", "q8_0",
|
|
"--reasoning", "off",
|
|
"--temp", "0.2",
|
|
"--host", "127.0.0.1",
|
|
"--port", str(port),
|
|
]
|
|
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
procs.append((name, p, port, path))
|
|
ok = ping_health(port, timeout=45)
|
|
print(f" - {name} on port {port}: {'STARTED' if ok else 'FAILED'}", flush=True)
|
|
|
|
time.sleep(2)
|
|
apps = get_gpu_compute_apps()
|
|
total_vram = get_total_vram_used()
|
|
print(f"\n[+] 3-MODEL CONCURRENT RESULT (Total GPU VRAM = {total_vram} MiB / 32768 MiB):", flush=True)
|
|
for name, p, port, path in procs:
|
|
proc_mem = next((a["used_memory_mib"] for a in apps if a["pid"] == p.pid), 0)
|
|
comp_ok, comp_time, comp_resp = test_completion(port, path)
|
|
print(f" - {name:22s} (PID {p.pid:7d}): {proc_mem:5d} MiB | Req = {'OK' if comp_ok else 'ERR'} ({comp_time:.2f}s)", flush=True)
|
|
finally:
|
|
for name, p, port, path in procs:
|
|
p.terminate()
|
|
try:
|
|
p.wait(timeout=5)
|
|
except Exception:
|
|
p.kill()
|
|
time.sleep(2)
|
|
|
|
# -------------------------------------------------------------
|
|
# CONCURRENCY TEST: 4 Models simultaneously
|
|
# (+ DeepSeek-Coder-V2-Lite)
|
|
# -------------------------------------------------------------
|
|
print("\n=======================================================", flush=True)
|
|
print(" [*] TEST SCENARIO B: 4 Models Simultaneously on GPU (ctx=32k)", flush=True)
|
|
print(" 1. Qwen2.5-Coder-14B (32k)")
|
|
print(" 2. DeepSeek-Coder-V2-Lite (32k)")
|
|
print(" 3. Granite-4.2-8B (32k)")
|
|
print(" 4. Qwen3-4B-Compressor (32k)")
|
|
print("=======================================================", flush=True)
|
|
|
|
procs_4 = []
|
|
configs_4 = [
|
|
("qwen2.5-coder-14b", "/srv/ai/models/qwen2.5-coder-14b/qwen2.5-coder-14b-instruct-q4_k_m.gguf", 32768, 8083),
|
|
("deepseek-coder-v2-lite", "/srv/ai/models/deepseek-coder-v2-lite/DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf", 32768, 8086),
|
|
("granite-4.2-8b", "/srv/ai/models/granite-4.2-8b/granite-4.2-8b-Q4_K_M.gguf", 32768, 8084),
|
|
("qwen3-4b-compressor", "/srv/ai/models/qwen3-4b-compressor/Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf", 32768, 8085),
|
|
]
|
|
|
|
try:
|
|
for name, path, ctx, port in configs_4:
|
|
cmd = [
|
|
"/home/ochenstarik/llama.cpp/build/bin/llama-server",
|
|
"-m", path,
|
|
"-ngl", "99",
|
|
"-c", str(ctx),
|
|
"--parallel", "1",
|
|
"--flash-attn", "on",
|
|
"--cache-type-k", "q8_0",
|
|
"--cache-type-v", "q8_0",
|
|
"--reasoning", "off",
|
|
"--temp", "0.2",
|
|
"--host", "127.0.0.1",
|
|
"--port", str(port),
|
|
]
|
|
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
procs_4.append((name, p, port, path))
|
|
ok = ping_health(port, timeout=45)
|
|
print(f" - {name} on port {port}: {'STARTED' if ok else 'FAILED'}", flush=True)
|
|
|
|
time.sleep(2)
|
|
apps = get_gpu_compute_apps()
|
|
total_vram = get_total_vram_used()
|
|
print(f"\n[+] 4-MODEL CONCURRENT RESULT (Total GPU VRAM = {total_vram} MiB / 32768 MiB):", flush=True)
|
|
for name, p, port, path in procs_4:
|
|
proc_mem = next((a["used_memory_mib"] for a in apps if a["pid"] == p.pid), 0)
|
|
comp_ok, comp_time, comp_resp = test_completion(port, path)
|
|
print(f" - {name:24s} (PID {p.pid:7d}): {proc_mem:5d} MiB | Req = {'OK' if comp_ok else 'ERR'} ({comp_time:.2f}s)", flush=True)
|
|
finally:
|
|
for name, p, port, path in procs_4:
|
|
p.terminate()
|
|
try:
|
|
p.wait(timeout=5)
|
|
except Exception:
|
|
p.kill()
|
|
time.sleep(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|