The 2026-10-09 wire aliases become the service names everywhere: systemd units, serve scripts, env files and FRANK_* variables, deploy-plan/deploy-remote service keys, container names, Prometheus job names + alert rules, Grafana dashboard queries, bats seams, runbook filenames, homepage/AGENTS lore, and historical prose. Rollback tags renamed in lockstep. Residue in the tree: zero. Retired frank-medium/small/npu names are history and stay. Known casualty: scripts/patches/*.patch context lines were rewritten; those retired-era vLLM-fork patches no longer apply byte-exact.
151 lines
4.6 KiB
Bash
Executable File
151 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# bench-frank-dragoon.sh — 64k-context benchmark for the frank-default-large stack.
|
|
#
|
|
# Measures generation throughput of Qwen3.6-27B Q8_K_P + DFlash + ngram-mod on
|
|
# a single backend at 64k context. It starts llama-server on a scratch port,
|
|
# drives one generation through the OpenAI-compatible API with a ~64k-token
|
|
# prompt, and prints tokens/sec.
|
|
#
|
|
# Usage:
|
|
# ./scripts/bench-frank-dragoon.sh <ROCm0|Vulkan0> [--generations N]
|
|
#
|
|
# Example:
|
|
# ./scripts/bench-frank-dragoon.sh ROCm0
|
|
# ./scripts/bench-frank-dragoon.sh Vulkan0 --generations 3
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
DEVICE="${1:?usage: bench-frank-dragoon.sh <ROCm0|Vulkan0> [--generations N]}"
|
|
GENERATIONS=1
|
|
if [ "${2:-}" = "--generations" ]; then
|
|
GENERATIONS="${3:?--generations needs a count}"
|
|
fi
|
|
|
|
MODEL_DIR=/home/dank/models
|
|
MODEL_FILE=Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf
|
|
DRAFT_FILE=Qwen3.6-27B-DFlash-Q8_0.gguf
|
|
LLAMA_SERVER=/home/dank/llama.cpp/build/bin/llama-server
|
|
MODEL="$MODEL_DIR/$MODEL_FILE"
|
|
DRAFT="$MODEL_DIR/$DRAFT_FILE"
|
|
|
|
PORT=7890
|
|
CTX=65536
|
|
PARALLEL=1
|
|
BATCH=2048
|
|
UBATCH=512
|
|
OUT_TOKENS=256
|
|
|
|
for f in "$MODEL" "$DRAFT"; do
|
|
if [[ ! -f "$f" ]]; then
|
|
echo "missing required file: $f" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo "=== 64k benchmark: device=$DEVICE model=$MODEL_FILE ==="
|
|
|
|
# --- start the server ----------------------------------------------------
|
|
"$LLAMA_SERVER" \
|
|
--model "$MODEL" \
|
|
--spec-draft-model "$DRAFT" \
|
|
--spec-type "draft-dflash,ngram-mod" \
|
|
--spec-draft-n-max 8 \
|
|
--spec-ngram-mod-n-min 48 \
|
|
--spec-ngram-mod-n-max 64 \
|
|
--spec-ngram-mod-n-match 24 \
|
|
--device "$DEVICE" \
|
|
--gpu-layers all \
|
|
--cache-type-k q8_0 \
|
|
--cache-type-v f16 \
|
|
--ctx-size "$CTX" \
|
|
--parallel "$PARALLEL" \
|
|
--batch-size "$BATCH" \
|
|
--ubatch-size "$UBATCH" \
|
|
--alias bench \
|
|
--host 127.0.0.1 \
|
|
--port "$PORT" \
|
|
> "/tmp/frankbench-$DEVICE.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
echo "server pid $SERVER_PID on 127.0.0.1:$PORT"
|
|
|
|
trap 'kill $SERVER_PID 2>/dev/null; wait $SERVER_PID 2>/dev/null' EXIT
|
|
|
|
# --- wait for ready ------------------------------------------------------
|
|
echo -n "waiting for server"
|
|
ready=0
|
|
for i in $(seq 1 400); do
|
|
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
echo " [ready]"
|
|
ready=1
|
|
break
|
|
fi
|
|
echo -n "."
|
|
sleep 2
|
|
done
|
|
if [ "$ready" != "1" ]; then
|
|
echo ""
|
|
echo "FAIL: server did not become ready"
|
|
tail -25 "/tmp/frankbench-$DEVICE.log"
|
|
exit 1
|
|
fi
|
|
|
|
# --- drive N generations and measure --------------------------------------
|
|
python3 - "$DEVICE" "$CTX" "$OUT_TOKENS" "$GENERATIONS" "$PORT" <<'PY'
|
|
import sys, time, json, urllib.request, statistics
|
|
|
|
device = sys.argv[1]
|
|
ctx = int(sys.argv[2])
|
|
out_tokens = int(sys.argv[3])
|
|
generations = int(sys.argv[4])
|
|
port = sys.argv[5]
|
|
|
|
# ~64k-token prompt. One prose unit is ~110 chars ≈ 28 tokens for this model.
|
|
# We aim for a prompt of ~60k tokens so generation stays within the 64k ctx.
|
|
unit = ("The careful study of natural language reveals patterns of meaning "
|
|
"that emerge from the deliberate arrangement of words and phrases. ")
|
|
# approximate: unit is 160 chars, ~40 tokens; repeat to ~1500 units = ~60k tokens
|
|
prompt = unit * 1500
|
|
|
|
def req(prompt, n):
|
|
body = json.dumps({
|
|
"prompt": prompt,
|
|
"max_tokens": n,
|
|
"temperature": 0.0,
|
|
"stream": False,
|
|
}).encode()
|
|
r = urllib.request.Request(f"http://127.0.0.1:{port}/v1/completions",
|
|
data=body, headers={"Content-Type": "application/json"})
|
|
with urllib.request.urlopen(r, timeout=1800) as resp:
|
|
return json.load(resp)
|
|
|
|
# prefill timing
|
|
t0 = time.time()
|
|
first = req(prompt, 0)
|
|
t_pre = time.time() - t0
|
|
used = first.get("usage", {})
|
|
prompt_tokens = used.get("prompt_tokens", 0)
|
|
print(f"prefill: {prompt_tokens} tokens in {t_pre:.2f}s "
|
|
f"({prompt_tokens/t_pre:.1f} tok/s)")
|
|
|
|
# generation timing
|
|
rates = []
|
|
total_gentok = 0
|
|
total_time = 0.0
|
|
for i in range(generations):
|
|
t0 = time.time()
|
|
out = req(prompt, out_tokens)
|
|
dt = time.time() - t0
|
|
uit = out.get("usage", {})
|
|
comp = uit.get("completion_tokens", 0)
|
|
gt = dt - t_pre # approximate generation time = total - prefill
|
|
r = comp / gt if gt > 0 else 0
|
|
rates.append(r)
|
|
total_gentok += comp
|
|
total_time += gt
|
|
print(f"gen {i+1}: {comp} tokens in {gt:.2f}s ({r:.1f} tok/s)")
|
|
|
|
avg = statistics.mean(rates) if rates else 0
|
|
print(f"\nRESULT device={device} prompt_tokens={prompt_tokens} "
|
|
f"avg_gen_tok_s={avg:.1f} over {generations} runs")
|
|
PY
|