Dankclaw's endpoints are re-split. frank-large is the halogen-flash-server quadlet again -- the 2026-09-13 unit file was never touched by the two-day ik_llama.cpp interlude, so the runtime was still in the repo and its image is still in local storage -- retuned to the shape asked for: HALOGEN_CTX 200192 with a 400,384-position pool and two slots, exactly two 200k conversations. frank-small is new: mainline llama.cpp on the RTX 3090 serving ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF at IQ3_S, using the -mtp build (it carries blk.64.nextn.*, which is what lets --spec-type draft-mtp run on the main model's own weights instead of a second 12 GiB copy) plus the BF16 vision projector, as one 131,072-token slot with f16 KV on port 7771. The host is shared now, and that is what the halogen retune pays for: HALOGEN_MAX_TOK 24576 (arena 20.6 -> 16.4 GiB, ~4-5% prefill) and the vision tower dropped (1.97 GiB) hand ~6 GiB back to the host, and the reserve rises to 20 to keep the fit check honest. Vision moved wholesale to frank-small. Retired to scripts/.AGENTTRASH/: the ik trio (env, service, serve script), the Qwen-Sharp template only that runtime read, and four benchmark harnesses bound to retired runtimes. Because retiring a file makes any test that greps it pass by absence, the seams assert the archive exists AND the live path does not.
193 lines
6.4 KiB
Python
Executable File
193 lines
6.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Decode throughput benchmark for frank-large.
|
|
Starts the server with a given --threads-batch value, sends an 8K+ token prompt,
|
|
generates 256 tokens, measures generation tok/s.
|
|
|
|
Each --threads-batch value requires a cold server load (~3 min).
|
|
"""
|
|
import json, time, urllib.request, urllib.error, subprocess, os, sys
|
|
|
|
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
|
SERVE = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
|
PORT = 7770
|
|
N_GEN = 256
|
|
PROMPT_FILE = "/tmp/bench-prompts/prompt_code.txt"
|
|
ENV_FILE = os.path.join(os.path.dirname(__file__), "frank-large.env")
|
|
SERVER_DIR = os.path.dirname(SERVE)
|
|
ROCM_PATH = "/opt/rocm"
|
|
|
|
def make_env():
|
|
"""Build the same env as serve-frank-large.sh."""
|
|
env = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
|
|
|
# Source env file for tunables
|
|
if os.path.exists(ENV_FILE):
|
|
with open(ENV_FILE) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith("#") and "=" in line:
|
|
k, v = line.split("=", 1)
|
|
env[k] = v
|
|
|
|
# Build LD_LIBRARY_PATH
|
|
lib_path = f"{SERVER_DIR}:{ROCM_PATH}/lib:{ROCM_PATH}/lib/rocm_sysdeps/lib:{ROCM_PATH}/lib/llvm/lib"
|
|
if os.environ.get("LD_LIBRARY_PATH"):
|
|
lib_path += f":{os.environ['LD_LIBRARY_PATH']}"
|
|
env["LD_LIBRARY_PATH"] = lib_path
|
|
env["ROCM_PATH"] = ROCM_PATH
|
|
|
|
return env
|
|
|
|
ENV = make_env()
|
|
|
|
def kill_all():
|
|
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
|
time.sleep(3)
|
|
|
|
def wait_ready():
|
|
for i in range(600):
|
|
try:
|
|
r = urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2)
|
|
if json.loads(r.read()).get("status") == "ok":
|
|
return True
|
|
except:
|
|
pass
|
|
time.sleep(1)
|
|
return False
|
|
|
|
def send_request(prompt, max_tokens=N_GEN):
|
|
"""Send chat completion, measure generation throughput."""
|
|
body = json.dumps({
|
|
"model": "frank-large",
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"max_tokens": max_tokens,
|
|
"stream": False,
|
|
"temperature": 0.7,
|
|
"top_p": 0.8
|
|
}).encode()
|
|
|
|
t0 = time.time()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{PORT}/v1/chat/completions",
|
|
data=body,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST"
|
|
)
|
|
r = urllib.request.urlopen(req, timeout=1200)
|
|
resp = json.loads(r.read())
|
|
wall = time.time() - t0
|
|
|
|
usage = resp.get("usage", {})
|
|
pt = usage.get("prompt_tokens", 0)
|
|
gt = usage.get("completion_tokens", 0)
|
|
gtps = gt / wall if wall > 0 else 0
|
|
return pt, gt, wall, gtps
|
|
|
|
def load_and_extend_prompt():
|
|
"""Load prompt and extend to ~8K tokens if needed."""
|
|
with open(PROMPT_FILE) as f:
|
|
text = f.read()
|
|
|
|
# Count approximate tokens (rough: ~4 chars per token for code)
|
|
approx_tokens = len(text) / 4.0
|
|
needed = 8200
|
|
|
|
if approx_tokens < needed:
|
|
repeats = needed // max(int(approx_tokens), 1) + 1
|
|
text = (text + "\n") * repeats
|
|
words = text.split()[:needed]
|
|
text = " ".join(words)
|
|
|
|
return text
|
|
|
|
def main():
|
|
tb_values = [int(x) for x in sys.argv[1:]] if len(sys.argv) > 1 else [16, 32, 64, 128]
|
|
results = []
|
|
|
|
prompt = load_and_extend_prompt()
|
|
n_words = len(prompt.split())
|
|
print(f"Prompt: {n_words} words (~{n_words} tokens), {len(prompt)} chars\n", flush=True)
|
|
|
|
for tb in tb_values:
|
|
print(f"\n{'='*60}")
|
|
print(f"threads-batch = {tb}")
|
|
print(f"{'='*60}", flush=True)
|
|
|
|
kill_all()
|
|
print(" Starting server...", flush=True)
|
|
|
|
lib_path = ENV.get("LD_LIBRARY_PATH", "")
|
|
cmd = [
|
|
"/usr/bin/env",
|
|
"HSA_OVERRIDE_GFX_VERSION=11.5.1",
|
|
"HIP_VISIBLE_DEVICES=0",
|
|
f"LD_LIBRARY_PATH={lib_path}",
|
|
f"ROCM_PATH={ROCM_PATH}",
|
|
SERVE, "--model", MODEL, "--port", str(PORT), "--jinja",
|
|
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
|
"--threads", "16", "--threads-batch", str(tb),
|
|
"--parallel", "2", "--ctx-size", "131072",
|
|
"--batch-size", "4096", "--ubatch-size", "4096",
|
|
"--flash-attn", "on",
|
|
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
|
"--cache-prompt", "--cache-idle-slots", "--metrics"
|
|
]
|
|
|
|
srv = subprocess.Popen(cmd, env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
|
|
ready = wait_ready()
|
|
if not ready:
|
|
print(" FAILED: server did not become ready", flush=True)
|
|
srv.terminate()
|
|
srv.wait()
|
|
for line in (srv.stdout.readlines() or [])[-20:]:
|
|
print(f" LOG: {line.rstrip()}", flush=True)
|
|
continue
|
|
|
|
print(" Server ready.", flush=True)
|
|
|
|
# Warmup (small request)
|
|
try:
|
|
send_request("Hi", 16)
|
|
except Exception as e:
|
|
print(f" Warmup failed: {e}", flush=True)
|
|
srv.terminate()
|
|
srv.wait()
|
|
continue
|
|
|
|
# Benchmark
|
|
print(f" Generating {N_GEN} tokens...", flush=True)
|
|
try:
|
|
pt, gt, wall, gtps = send_request(prompt, N_GEN)
|
|
print(f" {gt}/{N_GEN} gen tokens in {wall:.1f}s = {gtps:.1f} tok/s")
|
|
print(f" Prompt tokens: {pt}")
|
|
results.append({"tb": tb, "wall": wall, "gtps": gtps, "gt": gt, "pt": pt})
|
|
except Exception as e:
|
|
print(f" FAILED: {e}", flush=True)
|
|
|
|
srv.terminate()
|
|
srv.wait()
|
|
time.sleep(2)
|
|
|
|
# Summary
|
|
print(f"\n{'='*60}")
|
|
print("SUMMARY")
|
|
print(f"{'='*60}")
|
|
for r in results:
|
|
print(f" tb={r['tb']:>3}: {r['gt']} gen tokens, {r['wall']:.1f}s, {r['gtps']:.1f} tok/s")
|
|
|
|
if results:
|
|
best = max(results, key=lambda r: r['gtps'])
|
|
print(f"\nBest: tb={best['tb']} at {best['gtps']:.1f} tok/s")
|
|
|
|
with open("/tmp/bench_decode_results.txt", "w") as f:
|
|
f.write("tb,gen_tokens,wall_s,gen_tps,prompt_tokens\n")
|
|
for r in results:
|
|
f.write(f"{r['tb']},{r['gt']},{r['wall']:.1f},{r['gtps']:.1f},{r['pt']}\n")
|
|
|
|
print(f"\nResults: /tmp/bench_decode_results.txt")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|