docs: rename docs/ -> serverAppConfigs-docs/ per system-wide PROJECTNAME-docs convention
All in-repo path references (AGENTS.md, README, old scripts, report artifacts,
test code paths) rewritten in the same commit; focused-test map extended for
.gitignore, old/scripts/*, dankclaw/comfyui/*, and report/* per this repo's
own convention ('add one before committing'). Obsidian wikilinks are
title-based and unaffected.
This commit is contained in:
Executable
+192
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Decode throughput benchmark for frank-default-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-default-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-default-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()
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# Decode throughput benchmark for frank-default-large.
|
||||
# Measures generation tok/s with 8K+ token prompts and 256 generated tokens.
|
||||
set -euo pipefail
|
||||
|
||||
SERVE=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server
|
||||
MODEL=/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf
|
||||
PROMPT_FILE=/tmp/bench-prompts/prompt_code.txt
|
||||
PORT=7770
|
||||
N_GEN=256
|
||||
SERVER_DIR=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin
|
||||
ROCM_PATH=/opt/rocm
|
||||
LIB_PATH="$SERVER_DIR:$ROCM_PATH/lib:$ROCM_PATH/lib/rocm_sysdeps/lib:$ROCM_PATH/lib/llvm/lib"
|
||||
|
||||
kill_all() {
|
||||
pkill -9 -f llama-server 2>/dev/null || true
|
||||
sleep 3
|
||||
}
|
||||
|
||||
wait_ready() {
|
||||
for i in $(seq 1 600); do
|
||||
if curl -s http://127.0.0.1:$PORT/health 2>/dev/null | grep -q ok; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_bench() {
|
||||
local tb=$1
|
||||
local logfile=/tmp/server_tb${tb}.log
|
||||
|
||||
kill_all
|
||||
|
||||
# Start server
|
||||
env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||
LD_LIBRARY_PATH="$LIB_PATH" ROCM_PATH="$ROCM_PATH" \
|
||||
nohup $SERVE \
|
||||
--model "$MODEL" --port $PORT --jinja \
|
||||
--device ROCm0 --gpu-layers 999 --fit off \
|
||||
--threads 16 --threads-batch $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 \
|
||||
--temp 0.7 --top-p 0.8 --top-k 20 --min-p 0.0 \
|
||||
--reasoning on --reasoning-format deepseek \
|
||||
--reasoning-budget -1 > "$logfile" 2>&1 &
|
||||
|
||||
if ! wait_ready; then
|
||||
echo "TB=$tb: FAILED (server not ready)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Warmup (small request)
|
||||
curl -s -m 60 http://127.0.0.1:$PORT/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"frank-default-large","messages":[{"role":"user","content":"Hi"}],"max_tokens":16,"stream":false}' > /dev/null 2>&1 || true
|
||||
|
||||
# Build 8K prompt
|
||||
local prompt
|
||||
prompt=$(python3 -c "
|
||||
import json
|
||||
with open('$PROMPT_FILE') as f: text=f.read()
|
||||
words=text.split()[:8200]
|
||||
print(json.dumps(' '.join(words)))
|
||||
")
|
||||
|
||||
echo "TB=$tb: Starting benchmark..."
|
||||
|
||||
# Benchmark
|
||||
local t0 t1 resp pt gt wall gtps
|
||||
t0=$(date +%s%N)
|
||||
resp=$(curl -s -m 600 http://127.0.0.1:$PORT/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"model\":\"frank-default-large\",\"messages\":[{\"role\":\"user\",\"content\":$prompt}],\"max_tokens\":$N_GEN,\"stream\":false}" 2>&1)
|
||||
t1=$(date +%s%N)
|
||||
|
||||
if ! echo "$resp" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||
echo "TB=$tb: FAILED (HTTP error: $(echo "$resp" | head -1))"
|
||||
return 1
|
||||
fi
|
||||
|
||||
pt=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['usage']['prompt_tokens'])")
|
||||
gt=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['usage']['completion_tokens'])")
|
||||
wall=$(python3 -c "print(round(($t1-$t0)/1000000000,1))")
|
||||
gtps=$(python3 -c "print(round($N_GEN/($t1-$t0)*1000000000,2))")
|
||||
|
||||
echo "TB=$tb: pt=$pt gt=$gt wall=${wall}s gen=${gtps} tok/s"
|
||||
|
||||
# Extract prefill timing from server log
|
||||
local prefill_time
|
||||
prefill_time=$(grep -oP 'prompt eval time = \K[0-9.]+' "$logfile" | head -1 || echo "N/A")
|
||||
if [ "$prefill_time" != "N/A" ]; then
|
||||
echo "TB=$tb: prefill=$(python3 -c "print(round($N_GEN/($wall),2))") tok/s"
|
||||
fi
|
||||
|
||||
kill_all
|
||||
sleep 2
|
||||
|
||||
echo "$tb,$gt,$wall,$gtps,$pt" >> /tmp/bench_decode_results.csv
|
||||
}
|
||||
|
||||
# Main
|
||||
echo "=== Decode throughput benchmark ==="
|
||||
echo "Prompt: ~8K tokens, Generation: $N_GEN tokens"
|
||||
echo ""
|
||||
|
||||
# Clear results
|
||||
echo "tb,gen_tokens,wall_s,gen_tps,prompt_tokens" > /tmp/bench_decode_results.csv
|
||||
|
||||
# Run benchmarks
|
||||
for tb in 16 32; do
|
||||
run_bench $tb
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "=== SUMMARY ==="
|
||||
echo "Prefill throughput (from earlier benchmark):"
|
||||
echo " TB=16: 373.5 tok/s"
|
||||
echo " TB=32: 354.4 tok/s"
|
||||
echo ""
|
||||
echo "Decode throughput (generation tok/s with 8K prompt):"
|
||||
tail -n +2 /tmp/bench_decode_results.csv | while IFS=, read tb gt wall gtps pt; do
|
||||
echo " TB=$tb: $gt tokens in ${wall}s = ${gtps} tok/s (pt=$pt)"
|
||||
done
|
||||
Executable
+268
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Benchmark n-gram speculative decoding vs no-spec for frank-default-large.
|
||||
|
||||
Tests prompt-processing throughput with 16K token prompts (manageable request size).
|
||||
Code-heavy prompt (high n-gram redundancy) vs prose prompt (low redundancy).
|
||||
|
||||
Usage:
|
||||
# Generate prompts: python3 scripts/bench-ngram-mod.py generate /tmp/prompts/
|
||||
# Run: python3 scripts/bench-ngram-mod.py /tmp/prompts/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||
CHAT_TEMPLATE = "/home/dank/serverAppConfigs/scripts/chat-template.jinja"
|
||||
BUILD_DIR = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin"
|
||||
LLAMA_BIN = os.path.join(BUILD_DIR, "llama-server")
|
||||
|
||||
# 16K tokens ~= 64KB text
|
||||
TARGET_PROMPT_TOKENS = 16000
|
||||
TOKEN_CHAR_RATIO = 4
|
||||
TARGET_PROMPT_BYTES = TARGET_PROMPT_TOKENS * TOKEN_CHAR_RATIO
|
||||
|
||||
CONTEXT = 65536
|
||||
BATCH = 4096
|
||||
UBATCH = 4096
|
||||
THREADS = 16
|
||||
THREAD_BATCH = 32
|
||||
NGRAM_N_MAX = 24
|
||||
NGRAM_N_MIN = 4
|
||||
TEST_TOKENS = 128
|
||||
SERVER_WAIT = 300
|
||||
|
||||
|
||||
def start_server(port, spec_type):
|
||||
cmd = [
|
||||
LLAMA_BIN,
|
||||
"--model", MODEL,
|
||||
"--chat-template-file", CHAT_TEMPLATE,
|
||||
"--jinja",
|
||||
"--device", "ROCm0",
|
||||
"--gpu-layers", "999",
|
||||
"--parallel", "1",
|
||||
"--ctx-size", str(CONTEXT),
|
||||
"--batch-size", str(BATCH),
|
||||
"--ubatch-size", str(UBATCH),
|
||||
"--fit", "off",
|
||||
"--flash-attn", "on",
|
||||
"--cache-type-k", "f16",
|
||||
"--cache-type-v", "f16",
|
||||
"--threads", str(THREADS),
|
||||
"--threads-batch", str(THREAD_BATCH),
|
||||
"--cache-prompt",
|
||||
"--cache-idle-slots",
|
||||
"--metrics",
|
||||
"--host", "127.0.0.1",
|
||||
"--port", str(port),
|
||||
"--temp", "0",
|
||||
]
|
||||
if spec_type == "ngram-mod":
|
||||
cmd.extend([
|
||||
"--spec-type", "ngram-mod",
|
||||
"--spec-ngram-mod-n-max", str(NGRAM_N_MAX),
|
||||
"--spec-ngram-mod-n-min", str(NGRAM_N_MIN),
|
||||
])
|
||||
else:
|
||||
cmd.extend(["--spec-type", "none"])
|
||||
|
||||
env = os.environ.copy()
|
||||
env["ROCM_PATH"] = "/opt/rocm"
|
||||
env["LD_LIBRARY_PATH"] = f"{BUILD_DIR}:/opt/rocm/lib:/opt/rocm/lib/rocm_sysdeps/lib:/opt/rocm/lib/llvm/lib"
|
||||
env["HSA_OVERRIDE_GFX_VERSION"] = "11.5.1"
|
||||
env["HIP_VISIBLE_DEVICES"] = "0"
|
||||
|
||||
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
print(f" Waiting for server on port {port} (spec={spec_type})...")
|
||||
for i in range(SERVER_WAIT):
|
||||
try:
|
||||
req = urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2)
|
||||
resp = json.loads(req.read().decode())
|
||||
if resp.get("status") == "ok":
|
||||
print(f" Server ready after {i+1}s")
|
||||
return proc
|
||||
except:
|
||||
time.sleep(1)
|
||||
|
||||
proc.terminate()
|
||||
out, err = proc.communicate()
|
||||
print(f"ERROR: Server failed to start in {SERVER_WAIT}s")
|
||||
print(err.decode()[-2000:])
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_test(prompt_text, spec_type, port):
|
||||
"""Send a request and extract prompt processing metrics."""
|
||||
data = json.dumps({
|
||||
"model": "frank-default-large",
|
||||
"messages": [{"role": "user", "content": prompt_text}],
|
||||
"max_tokens": TEST_TOKENS,
|
||||
"stream": False,
|
||||
"temperature": 0,
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/chat/completions",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
start = time.monotonic()
|
||||
resp = urllib.request.urlopen(req, timeout=600)
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
result = json.loads(resp.read().decode())
|
||||
timings = result.get("timings", {})
|
||||
usage = result.get("usage", {})
|
||||
|
||||
return {
|
||||
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||
"prompt_ms": timings.get("prompt_ms", 0),
|
||||
"prompt_per_second": timings.get("prompt_per_second", 0),
|
||||
"prompt_per_token_ms": timings.get("prompt_per_token_ms", 0),
|
||||
"predicted_per_second": timings.get("predicted_per_second", 0),
|
||||
"total_time_s": elapsed,
|
||||
"cache_n": timings.get("cache_n", 0),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <prompt_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
prompt_dir = Path(sys.argv[1])
|
||||
prompt_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
code_prompt_file = prompt_dir / "prompt_code.txt"
|
||||
prose_prompt_file = prompt_dir / "prompt_prose.txt"
|
||||
|
||||
# Generate prompts if they don't exist
|
||||
if not code_prompt_file.exists() or not prose_prompt_file.exists():
|
||||
print("Generating prompts...")
|
||||
# Generate deterministic prompts
|
||||
# Code prompt: structured algorithms + data generation + config schemas
|
||||
code_lines = []
|
||||
for i in range(50):
|
||||
code_lines.append(f"# Algorithm {i}\n")
|
||||
code_lines.append(f"def algo_{i}(x, y):\n")
|
||||
code_lines.append(f" \"\"\"Algorithm {i} processing.\"\"\"\n")
|
||||
code_lines.append(f" result = 0\n")
|
||||
code_lines.append(f" for j in range({i * 10}):\n")
|
||||
code_lines.append(f" result += x * y + j\n")
|
||||
code_lines.append(f" return result\n\n")
|
||||
code_text = "".join(code_lines)
|
||||
# Pad/truncate to target
|
||||
while len(code_text) < TARGET_PROMPT_BYTES:
|
||||
code_text += "# Additional algorithm block\n"
|
||||
code_text += f"def extra_{len(code_text)//100}():\n"
|
||||
code_text += " x = 1\n"
|
||||
code_text += " y = 2\n"
|
||||
code_text += " return x + y\n\n"
|
||||
code_text = code_text[:TARGET_PROMPT_BYTES]
|
||||
|
||||
# Prose prompt: historical/scientific/philosophical text
|
||||
prose_text = ""
|
||||
topics = [
|
||||
"The Renaissance brought profound transformation to European civilization. Artists and scholars pushed the boundaries of human expression.",
|
||||
"Quantum mechanics describes the behavior of matter and energy at atomic scales. The uncertainty principle limits measurement precision.",
|
||||
"Ethical philosophy grapples with questions of right and wrong. Utilitarianism judges actions by their consequences and the happiness they produce.",
|
||||
"Mountains rise like ancient sentinels, their peaks lost in clouds that swirl and dance across the sky above the valleys below.",
|
||||
"The ocean whispers secrets to the shore, each wave carrying fragments of distant lands across the water before retreating back to sea.",
|
||||
]
|
||||
for _ in range(500):
|
||||
prose_text += f"{topics[len(prose_text) % len(topics)]} "
|
||||
prose_text = prose_text[:TARGET_PROMPT_BYTES]
|
||||
|
||||
code_prompt_file.write_text(code_text)
|
||||
prose_prompt_file.write_text(prose_text)
|
||||
print(f" Code: {code_prompt_file.stat().st_size:,} bytes")
|
||||
print(f" Prose: {prose_prompt_file.stat().st_size:,} bytes")
|
||||
|
||||
print("=" * 60)
|
||||
print(" N-gram Mod Benchmark — frank-default-large")
|
||||
print(f" STRIX ROCmFP4 · 64K context · 16K prompts")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
port_base = 7772
|
||||
port_ngram = 7773
|
||||
|
||||
tests = [
|
||||
("code", "none", port_base, code_prompt_file),
|
||||
("prose", "none", port_ngram, prose_prompt_file),
|
||||
("code", "ngram-mod", port_base, code_prompt_file),
|
||||
("prose", "ngram-mod", port_ngram, prose_prompt_file),
|
||||
]
|
||||
|
||||
for prompt_type, spec_type, port, pfile in tests:
|
||||
print(f"\n=== {spec_type} ({prompt_type}) ===")
|
||||
print(f" File: {pfile} ({pfile.stat().st_size:,} bytes)")
|
||||
print(f" Spec: {spec_type}")
|
||||
if spec_type == "ngram-mod":
|
||||
print(f" n_max={NGRAM_N_MAX}, n_min={NGRAM_N_MIN}")
|
||||
|
||||
proc = start_server(port, spec_type)
|
||||
|
||||
try:
|
||||
# Warm-up
|
||||
print(" Warm-up...")
|
||||
run_test("Hello test", spec_type, port)
|
||||
|
||||
# Full prompt
|
||||
print(f" Sending {TARGET_PROMPT_TOKENS} token prompt...")
|
||||
with open(pfile) as f:
|
||||
prompt_text = f.read()
|
||||
result = run_test(prompt_text, spec_type, port)
|
||||
|
||||
tps = result["prompt_per_second"]
|
||||
print(f" Tokens: {result['prompt_tokens']}")
|
||||
print(f" Prompt: {result['prompt_ms']:.0f}ms")
|
||||
print(f" TPS: {tps:.1f} tok/s")
|
||||
print(f" Total: {result['total_time_s']:.1f}s")
|
||||
|
||||
results.append({
|
||||
"prompt_type": prompt_type,
|
||||
"spec_type": spec_type,
|
||||
"prompt_tokens": result["prompt_tokens"],
|
||||
"prompt_ms": result["prompt_ms"],
|
||||
"tps": tps,
|
||||
"total_time_s": result["total_time_s"],
|
||||
})
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except:
|
||||
proc.kill()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print(" RESULTS SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
for ptype in ["code", "prose"]:
|
||||
baseline = next((r for r in results if r["prompt_type"] == ptype and r["spec_type"] == "none"), None)
|
||||
ngram = next((r for r in results if r["prompt_type"] == ptype and r["spec_type"] == "ngram-mod"), None)
|
||||
if baseline and ngram:
|
||||
gain = ((ngram["tps"] - baseline["tps"]) / baseline["tps"]) * 100 if baseline["tps"] > 0 else 0
|
||||
print(f"\n{ptype.upper()} PROMPT:")
|
||||
print(f" Baseline TPS: {baseline['tps']:.1f} tok/s ({baseline['prompt_ms']:.0f}ms)")
|
||||
print(f" N-gram TPS: {ngram['tps']:.1f} tok/s ({ngram['prompt_ms']:.0f}ms)")
|
||||
print(f" Speedup: {gain:+.1f}%")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env bash
|
||||
# Benchmark n-gram speculative decoding vs no-spec for frank-default-large.
|
||||
#
|
||||
# This tests prompt-processing throughput at 64k context with two distinct
|
||||
# prompts: one code-heavy and one prose-heavy. Uses llama-server on scratch
|
||||
# ports with the OpenAI-compatible API to measure prompt_eval_duration_ms.
|
||||
#
|
||||
# Run on Dankclaw after stopping the production service:
|
||||
# sudo systemctl stop frank-default-large
|
||||
# ./scripts/bench-ngram-mod.sh
|
||||
# sudo systemctl start frank-default-large
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-default-large.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a; source "$ENV_FILE"; set +a
|
||||
fi
|
||||
|
||||
: "${LLAMA_BIN:=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server}"
|
||||
: "${MODEL_DIR:=/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX}"
|
||||
: "${MODEL_FILE:=Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf}"
|
||||
: "${CHAT_TEMPLATE_FILE:=$SCRIPT_DIR/chat-template.jinja}"
|
||||
: "${CHAT_TEMPLATE_KWARGS:={}}"
|
||||
: "${BATCH_SIZE:=4096}"
|
||||
: "${UBATCH_SIZE:=4096}"
|
||||
: "${CTX_SIZE:=65536}"
|
||||
: "${NUM_THREADS:=16}"
|
||||
: "${THREAD_BATCH:=32}"
|
||||
: "${CACHE_TYPE_K:=f16}"
|
||||
: "${CACHE_TYPE_V:=f16}"
|
||||
: "${FIT:=off}"
|
||||
: "${TEMPERATURE:=0.7}"
|
||||
: "${TOP_P:=0.8}"
|
||||
: "${TOP_K:=20}"
|
||||
: "${MIN_P:=0.0}"
|
||||
: "${REASONING:=on}"
|
||||
: "${REASONING_FORMAT:=deepseek}"
|
||||
|
||||
MODEL="$MODEL_DIR/$MODEL_FILE"
|
||||
HOSTNAME=$(hostname 2>/dev/null || echo "dankclaw")
|
||||
|
||||
# Prompt files (generated by generate-prompts.py or passed via args)
|
||||
PROMPT_CODE="${1:-}"
|
||||
PROMPT_PROSE="${2:-}"
|
||||
|
||||
if [[ -z "$PROMPT_CODE" || -z "$PROMPT_PROSE" ]]; then
|
||||
echo "ERROR: Need two prompt file paths."
|
||||
echo "Usage: $0 <prompt_code.txt> <prompt_prose.txt>"
|
||||
echo "Generate with: python3 scripts/generate-prompts.py"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$MODEL" ]]; then
|
||||
echo "missing or unreadable model: $MODEL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVER_DIR="$(cd -- "$(dirname -- "$LLAMA_BIN")" && pwd)"
|
||||
ROCM_PATH="${ROCM_PATH:-/opt/rocm}"
|
||||
LIBRARY_PATH="$SERVER_DIR:$ROCM_PATH/lib:$ROCM_PATH/lib/rocm_sysdeps/lib:$ROCM_PATH/lib/llvm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
|
||||
# Scratch ports
|
||||
PORT_NGRAM=7771
|
||||
PORT_BASELINE=7772
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up servers..."
|
||||
# Kill any leftover llama-server on our ports
|
||||
for port in $PORT_NGRAM $PORT_BASELINE; do
|
||||
fuser -k "$port"/tcp 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_for_server() {
|
||||
local port=$1
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
while (( elapsed < max_wait )); do
|
||||
if curl -s "http://127.0.0.1:$port/health" | grep -q '"running"'; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
(( elapsed++ ))
|
||||
done
|
||||
echo "ERROR: Server on port $port did not start within ${max_wait}s" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
get_prompt_tokens() {
|
||||
local file=$1
|
||||
# Count tokens approximately (1 token ~ 4 chars for English/code)
|
||||
wc -c < "$file" | awk '{printf "%d", $1/4}'
|
||||
}
|
||||
|
||||
run_single_test() {
|
||||
local port=$1
|
||||
local spec_type="$2"
|
||||
local prompt_file=$3
|
||||
local label=$4
|
||||
local ngram_n_max="${5:-24}"
|
||||
local ngram_n_min="${6:-4}"
|
||||
|
||||
echo "=== $label ==="
|
||||
echo " spec-type: $spec_type"
|
||||
echo " ngram-mod n-max: $ngram_n_max (if enabled)"
|
||||
echo " ngram-mod n-min: $ngram_n_min (if enabled)"
|
||||
echo " prompt: $prompt_file ($(wc -c < "$prompt_file") bytes)"
|
||||
|
||||
# Build command
|
||||
local cmd=(
|
||||
/usr/bin/env
|
||||
ROCM_PATH="$ROCM_PATH"
|
||||
HIP_VISIBLE_DEVICES=0
|
||||
LD_LIBRARY_PATH="$LIBRARY_PATH"
|
||||
HSA_OVERRIDE_GFX_VERSION="${HSA_OVERRIDE_GFX_VERSION:-11.5.1}"
|
||||
"$LLAMA_BIN"
|
||||
--model "$MODEL"
|
||||
--chat-template-file "$CHAT_TEMPLATE_FILE"
|
||||
--chat-template-kwargs "$CHAT_TEMPLATE_KWARGS"
|
||||
--host 127.0.0.1
|
||||
--port "$port"
|
||||
--jinja
|
||||
--device ROCm0
|
||||
--gpu-layers 999
|
||||
--parallel 1
|
||||
--ctx-size "$CTX_SIZE"
|
||||
--batch-size "$BATCH_SIZE"
|
||||
--ubatch-size "$UBATCH_SIZE"
|
||||
--fit "$FIT"
|
||||
--flash-attn on
|
||||
--cache-type-k "$CACHE_TYPE_K"
|
||||
--cache-type-v "$CACHE_TYPE_V"
|
||||
--threads "$NUM_THREADS"
|
||||
--threads-batch "$THREAD_BATCH"
|
||||
--cache-prompt
|
||||
--cache-idle-slots
|
||||
--metrics
|
||||
--temp "$TEMPERATURE"
|
||||
--top-p "$TOP_P"
|
||||
--top-k "$TOP_K"
|
||||
--min-p "$MIN_P"
|
||||
--reasoning "$REASONING"
|
||||
--reasoning-format "$REASONING_FORMAT"
|
||||
--reasoning-budget -1
|
||||
)
|
||||
|
||||
# Add spec flags
|
||||
if [[ "$spec_type" == "ngram-mod" ]]; then
|
||||
cmd+=(
|
||||
--spec-type "ngram-mod"
|
||||
--spec-ngram-mod-n-max "$ngram_n_max"
|
||||
--spec-ngram-mod-n-min "$ngram_n_min"
|
||||
)
|
||||
else
|
||||
cmd+=(
|
||||
--spec-type "none"
|
||||
)
|
||||
fi
|
||||
|
||||
# Start server with stderr to log
|
||||
local log_file="/tmp/ngram-bench-${label//[^a-zA-Z0-9]/-}.log"
|
||||
echo " Starting server (log: $log_file)..."
|
||||
|
||||
"${cmd[@]}" 2> "$log_file" &
|
||||
local server_pid=$!
|
||||
|
||||
if ! wait_for_server "$port"; then
|
||||
echo "ERROR: Server failed to start" >&2
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Server ready."
|
||||
|
||||
# Send prompt via API
|
||||
echo " Sending prompt..."
|
||||
local prompt_text
|
||||
prompt_text=$(cat "$prompt_file")
|
||||
|
||||
local response
|
||||
response=$(curl -s -X POST "http://127.0.0.1:$port/v1/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"frank-default-large\",
|
||||
\"prompt\": \"$prompt_text\",
|
||||
\"max_tokens\": 128,
|
||||
\"temperature\": 0,
|
||||
\"stream\": false
|
||||
}")
|
||||
|
||||
# Extract metrics
|
||||
local prompt_tokens prompt_eval_duration_ms total_duration_ms
|
||||
prompt_tokens=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['prompt_eval_count'])" 2>/dev/null || echo "0")
|
||||
prompt_eval_duration_ms=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['prompt_eval_duration_ms'])" 2>/dev/null || echo "0")
|
||||
total_duration_ms=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['total_duration_ms'])" 2>/dev/null || echo "0")
|
||||
|
||||
# Calculate throughput
|
||||
local prompt_tps=0
|
||||
if [[ "$prompt_eval_duration_ms" -gt 0 ]]; then
|
||||
prompt_tps=$(python3 -c "print(f'{($prompt_tokens / $prompt_eval_duration_ms) * 1000:.1f}')")
|
||||
fi
|
||||
|
||||
echo " Prompt tokens: $prompt_tokens"
|
||||
echo " Prompt eval: ${prompt_eval_duration_ms}ms"
|
||||
echo " Prompt TPS: $prompt_tps tok/s"
|
||||
echo " Total duration: ${total_duration_ms}ms"
|
||||
|
||||
# Kill server
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
echo " Done."
|
||||
echo "---"
|
||||
|
||||
# Output structured results for parsing
|
||||
echo "RESULT|$label|$prompt_file|$prompt_tokens|$prompt_eval_duration_ms|$total_duration_ms|$prompt_tps"
|
||||
}
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ N-gram Mod Benchmark — frank-default-large ║"
|
||||
echo "║ STRIX ROCmFP4 · 64k context · two prompts ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Host: $HOSTNAME"
|
||||
echo "Model: $MODEL"
|
||||
echo "Build: $(basename "$LLAMA_BIN")"
|
||||
echo "Context: $CTX_SIZE"
|
||||
echo ""
|
||||
|
||||
# Run baseline (no spec)
|
||||
echo "=== BASELINE (no speculative decoding) ==="
|
||||
echo ""
|
||||
|
||||
BASELINE_CODE_RESULT=$(run_single_test $PORT_BASELINE "none" "$PROMPT_CODE" "Baseline-Code" 24 4)
|
||||
BASELINE_PROSE_RESULT=$(run_single_test $PORT_NGRAM "none" "$PROMPT_PROSE" "Baseline-Prose" 24 4)
|
||||
|
||||
echo ""
|
||||
echo "=== NGRAM-MOD (speculative decoding enabled) ==="
|
||||
echo ""
|
||||
|
||||
# Run n-gram mod
|
||||
NGRAM_CODE_RESULT=$(run_single_test $PORT_NGRAM "ngram-mod" "$PROMPT_CODE" "Ngram-Code" 24 4)
|
||||
NGRAM_PROSE_RESULT=$(run_single_test $PORT_BASELINE "ngram-mod" "$PROMPT_PROSE" "Ngram-Prose" 24 4)
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ RESULTS SUMMARY ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Parse and display results
|
||||
python3 << 'PYEOF'
|
||||
import sys
|
||||
|
||||
results = []
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line.startswith("RESULT|"):
|
||||
continue
|
||||
parts = line.split("|")
|
||||
if len(parts) != 8:
|
||||
continue
|
||||
label, prompt_file, prompt_tokens, prompt_eval_ms, total_ms, tps = parts[1], parts[2], int(parts[3]), int(parts[4]), int(parts[5]), float(parts[6])
|
||||
results.append({
|
||||
'label': label,
|
||||
'prompt_file': prompt_file,
|
||||
'prompt_tokens': prompt_tokens,
|
||||
'prompt_eval_ms': prompt_eval_ms,
|
||||
'total_ms': total_ms,
|
||||
'tps': tps
|
||||
})
|
||||
|
||||
# Compare baseline vs ngram for each prompt type
|
||||
for code_result in results:
|
||||
if not code_result['label'].startswith('Ngram-'):
|
||||
continue
|
||||
prompt_type = code_result['label'].replace('Ngram-', '').replace('Ngram', 'Baseline').replace('-', '-')
|
||||
baseline = next((r for r in results if r['label'] == 'Baseline-' + prompt_type), None)
|
||||
if not baseline:
|
||||
continue
|
||||
|
||||
spec = code_result['label'] == 'Ngram-' + prompt_type
|
||||
|
||||
print(f"Prompt: {prompt_type}")
|
||||
print(f" Tokens: {code_result['prompt_tokens']}")
|
||||
if spec:
|
||||
print(f" Spec: ngram-mod (n_max=24, n_min=4)")
|
||||
print(f" Prompt: {code_result['prompt_eval_ms']}ms | {code_result['tps']:>10.1f} tok/s")
|
||||
baseline_tps = baseline['tps']
|
||||
if baseline_tps > 0:
|
||||
improvement = ((code_result['tps'] - baseline_tps) / baseline_tps) * 100
|
||||
print(f" Speedup: {improvement:+.1f}% vs baseline ({baseline_tps:.1f} tok/s)")
|
||||
else:
|
||||
print(f" Spec: none (baseline)")
|
||||
print(f" Prompt: {baseline['prompt_eval_ms']}ms | {baseline['tps']:>10.1f} tok/s")
|
||||
print()
|
||||
PYEOF
|
||||
|
||||
echo ""
|
||||
echo "Raw data lines above (RESULT|... format) can be parsed for automation."
|
||||
echo ""
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP-based prefill-throughput sweep for frank-default-large.
|
||||
Starts the server with a given --threads-batch value, sends a 64K-prompt via
|
||||
the chat API (max_tokens=0), measures prompt_tokens/wall_time.
|
||||
|
||||
One request per value. Each --threads-batch requires a fresh cold server load
|
||||
(~3 min), so repeating would add 2x overhead. The server also crashes on 2nd
|
||||
request with max_tokens=0, making repeats impractical.
|
||||
"""
|
||||
import subprocess, time, os, sys, json, urllib.request, urllib.error
|
||||
|
||||
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"
|
||||
TB_LIST = [16, 32, 48, 64, 80, 96, 128]
|
||||
PROMPT_FILE = "/tmp/bench-prompts/prompt_code.txt"
|
||||
PORT = 7770
|
||||
ENV = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||
|
||||
def kill_all():
|
||||
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
||||
subprocess.run(["pkill", "-9", "llama-bench"], 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)
|
||||
data = json.loads(r.read())
|
||||
if data.get("status") == "ok":
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def send_prefill_request():
|
||||
"""Send chat completion with max_tokens=0 (prefill only)."""
|
||||
with open(PROMPT_FILE, "r") as f:
|
||||
prompt = f.read()
|
||||
body = json.dumps({
|
||||
"model": "frank-default-large",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 1,
|
||||
"stream": False
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{PORT}/v1/chat/completions",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
t0 = time.time()
|
||||
r = urllib.request.urlopen(req, timeout=600)
|
||||
resp = json.loads(r.read())
|
||||
wall = time.time() - t0
|
||||
usage = resp.get("usage", {})
|
||||
prompt_tokens = usage.get("prompt_tokens", 0)
|
||||
return prompt_tokens, wall
|
||||
|
||||
results = []
|
||||
for tb in TB_LIST:
|
||||
print(f"=== threads-batch={tb} ===", flush=True)
|
||||
kill_all()
|
||||
|
||||
# Start server
|
||||
print(" Starting server...", flush=True)
|
||||
srv = subprocess.Popen(
|
||||
[SERVE, "--model", MODEL, "--port", str(PORT), "--jinja",
|
||||
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
||||
"--threads", "16", "--threads-batch", str(tb),
|
||||
"--parallel", "2", "--ctx-size", "524288",
|
||||
"--batch-size", "4096", "--ubatch-size", "4096",
|
||||
"--flash-attn", "on",
|
||||
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
||||
"--cache-prompt", "--cache-idle-slots", "--metrics"],
|
||||
env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
ready = wait_ready()
|
||||
if not ready:
|
||||
srv.terminate()
|
||||
srv.wait()
|
||||
print(" FAILED: server didn't start")
|
||||
sys.exit(1)
|
||||
print(" Ready.", flush=True)
|
||||
|
||||
# Single prefill request
|
||||
try:
|
||||
prompt_tokens, wall = send_prefill_request()
|
||||
if prompt_tokens > 0:
|
||||
tps = prompt_tokens / wall
|
||||
print(f" {prompt_tokens} tok in {wall:.1f}s = {tps:.1f} tok/s", flush=True)
|
||||
results.append({"tb": tb, "avg": tps, "std": 0, "samples": 1})
|
||||
else:
|
||||
print(f" FAILED (0 prompt_tokens, wall={wall:.1f}s)", flush=True)
|
||||
results.append({"tb": tb, "avg": None, "std": 0, "samples": 0})
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}", flush=True)
|
||||
results.append({"tb": tb, "avg": None, "std": 0, "samples": 0})
|
||||
|
||||
srv.terminate()
|
||||
srv.wait()
|
||||
time.sleep(2)
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
for r in results:
|
||||
if r["avg"] is not None:
|
||||
print(f" tb={r['tb']:>3} prefill={r['avg']:.1f} tok/s ({r['samples']} sample)")
|
||||
else:
|
||||
print(f" tb={r['tb']:>3} FAILED")
|
||||
|
||||
best = max((r for r in results if r["avg"] is not None), key=lambda r: r["avg"])
|
||||
if best:
|
||||
print(f"\nBest: threads-batch={best['tb']} ({best['avg']:.1f} tok/s)")
|
||||
|
||||
with open("/tmp/bench_tb_results.txt", "w") as f:
|
||||
for r in results:
|
||||
avg = f"{r['avg']:.1f}" if r["avg"] is not None else "N/A"
|
||||
f.write(f"{r['tb']},{avg},0,{r['samples']}\n")
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# Threads-batch sweep for frank-default-large on dankclaw
|
||||
# Benches prefill throughput at 65536 prompt tokens across threads-batch values.
|
||||
set -uo pipefail
|
||||
|
||||
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"
|
||||
BENCH="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-bench"
|
||||
TB_LIST=(16 32 48 64 80 96 128)
|
||||
ENV_FILE="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin"
|
||||
|
||||
echo "========================================"
|
||||
echo "THREADS-BATCH SWEEP — frank-default-large"
|
||||
echo "Model: $(basename "$MODEL") ($(du -h "$MODEL" | cut -f1))"
|
||||
echo "Threads-batch values: ${TB_LIST[*]}"
|
||||
echo "Prompt: 65536 tokens, n-gen: 0 (prefill-only)"
|
||||
echo "========================================"
|
||||
echo
|
||||
|
||||
RESULTS=""
|
||||
|
||||
for tb in "${TB_LIST[@]}"; do
|
||||
echo "--- threads-batch=$tb ---"
|
||||
|
||||
# Kill any lingering processes
|
||||
pkill -9 llama-server 2>/dev/null || true
|
||||
pkill -9 llama-bench 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
# Start server
|
||||
echo " Starting server with --threads-batch $tb ..."
|
||||
timeout 300 env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||
"$SERVE" \
|
||||
--model "$MODEL" --port 7770 --jinja \
|
||||
--device ROCm0 --gpu-layers 999 --fit off \
|
||||
--threads 16 --threads-batch "$tb" \
|
||||
--parallel 2 --ctx-size 524288 \
|
||||
--batch-size 4096 --ubatch-size 4096 \
|
||||
--flash-attn on \
|
||||
--cache-type-k f16 --cache-type-v f16 \
|
||||
--cache-prompt --cache-idle-slots --metrics \
|
||||
&>/tmp/server_tb_${tb}.log &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for ready
|
||||
READY=false
|
||||
for i in $(seq 1 300); do
|
||||
if curl -sf http://127.0.0.1:7770/health >/dev/null 2>&1; then
|
||||
READY=true
|
||||
break
|
||||
fi
|
||||
if (( i % 30 == 0 )); then
|
||||
echo " Waiting for ready... (${i}s)"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! $READY; then
|
||||
echo " Server failed to start. Tail of log:"
|
||||
tail -5 /tmp/server_tb_${tb}.log
|
||||
echo " N/A"
|
||||
RESULTS="${RESULTS}${tb},N/A,N/A,?,0\n"
|
||||
echo
|
||||
continue
|
||||
fi
|
||||
echo " Server ready."
|
||||
|
||||
# Run bench (prefill-only: n-gen=0)
|
||||
echo " Running bench..."
|
||||
t0=$(date +%s%N)
|
||||
|
||||
BENCH_OUTPUT=$(env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||
"$BENCH" \
|
||||
--model "$MODEL" --threads 16 \
|
||||
--n-prompt 65536 --n-gen 0 \
|
||||
--repetitions 3 --output csv \
|
||||
--flash-attn on --n-gpu-layers 999 2>/tmp/bench_stderr_${tb}.log)
|
||||
bexit=$?
|
||||
t1=$(date +%s%N)
|
||||
elapsed=$(( (t1 - t0) / 1000000000 ))
|
||||
|
||||
# Parse CSV: find prefill row (n_prompt > 0, n_gen == 0)
|
||||
PREFILL="N/A"
|
||||
DECODE="N/A"
|
||||
CONTEXT="?"
|
||||
if [ $bexit -eq 0 ] && [ -n "$BENCH_OUTPUT" ]; then
|
||||
# Extract prefill row: n_prompt > 0 and n_gen == 0
|
||||
PREFILL=$(echo "$BENCH_OUTPUT" | python3 -c "
|
||||
import csv, sys, io
|
||||
reader = csv.reader(io.StringIO(sys.stdin.read()))
|
||||
header = next(reader, None)
|
||||
for row in reader:
|
||||
if len(row) >= 41:
|
||||
try:
|
||||
np_ = int(row[33])
|
||||
ng = int(row[34])
|
||||
avg_ts = float(row[39])
|
||||
if np_ > 0 and ng == 0:
|
||||
print(f'{avg_ts:.1f}')
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
CONTEXT=$(echo "$BENCH_OUTPUT" | python3 -c "
|
||||
import csv, sys, io
|
||||
reader = csv.reader(io.StringIO(sys.stdin.read()))
|
||||
header = next(reader, None)
|
||||
for row in reader:
|
||||
if len(row) >= 41:
|
||||
try:
|
||||
np_ = int(row[33])
|
||||
ng = int(row[34])
|
||||
if np_ > 0 and ng == 0:
|
||||
print(row[40].strip() if row[40].strip() else '?')
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -z "$PREFILL" ]; then
|
||||
PREFILL="N/A"
|
||||
CONTEXT="parse_err"
|
||||
fi
|
||||
else
|
||||
echo " Bench failed (rc=$bexit). STDERR:"
|
||||
head -5 /tmp/bench_stderr_${tb}.log
|
||||
echo " N/A"
|
||||
fi
|
||||
|
||||
echo " prefill=${PREFILL} tok/s decode=${DECODE} ctx=${CONTEXT} time=${elapsed}s"
|
||||
RESULTS="${RESULTS}${tb},${PREFILL},${DECODE},${CONTEXT},${elapsed}\n"
|
||||
echo
|
||||
|
||||
# Shutdown server
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "========================================"
|
||||
echo "SUMMARY"
|
||||
echo "========================================"
|
||||
echo "tb prefill_tok/s decode_tok/s ctx_util time_s"
|
||||
echo "--------------------------------------------------------"
|
||||
echo -e "$RESULTS" | grep -v "^$" | while IFS=, read -r tb pt dt cu el; do
|
||||
printf "%-6s %-16s %-16s %-12s %-8s\n" "$tb" "$pt" "$dt" "$cu" "${el}s"
|
||||
done
|
||||
|
||||
# Find best
|
||||
BEST=$(echo -e "$RESULTS" | grep -v "^$" | grep -v ",N/A," | sort -t, -k2 -rn | head -1)
|
||||
BEST_TB=$(echo "$BEST" | cut -d, -f1)
|
||||
BEST_PT=$(echo "$BEST" | cut -d, -f2)
|
||||
echo ""
|
||||
echo "Best threads-batch: $BEST_TB (${BEST_PT} tok/s prefill)"
|
||||
|
||||
# Write results
|
||||
echo -e "$RESULTS" | grep -v "^$" > /tmp/bench_tb_results.txt
|
||||
echo ""
|
||||
echo "Results written to /tmp/bench_tb_results.txt"
|
||||
|
||||
# Cleanup
|
||||
pkill -9 llama-server 2>/dev/null || true
|
||||
pkill -9 llama-bench 2>/dev/null || true
|
||||
@@ -0,0 +1,139 @@
|
||||
import subprocess, time, os, sys, csv, io
|
||||
|
||||
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"
|
||||
BENCH = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-bench"
|
||||
TB_LIST = [16, 32, 48, 64, 80, 96, 128]
|
||||
ENV = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||
|
||||
def kill_all():
|
||||
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
||||
subprocess.run(["pkill", "-9", "llama-bench"], capture_output=True)
|
||||
time.sleep(3)
|
||||
|
||||
def wait_ready():
|
||||
import urllib.request
|
||||
for i in range(600):
|
||||
try:
|
||||
r = urllib.request.urlopen("http://127.0.0.1:7770/health", timeout=2)
|
||||
if r.status == 200:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def parse_bench_output(stdout, stderr):
|
||||
"""Parse llama-bench CSV output. Columns (0-indexed): n_prompt=33, avg_ts=39"""
|
||||
prefill = decode = None
|
||||
ctx = "?"
|
||||
try:
|
||||
reader = csv.reader(io.StringIO(stdout))
|
||||
header = next(reader, None)
|
||||
if header is None:
|
||||
return None, None, "?"
|
||||
|
||||
# Find prefill row: n_prompt matches our prompt size, n_gen matches our gen size
|
||||
# For n-gen=0, we get prefill only. The prefill row has n_prompt > 0.
|
||||
for row in reader:
|
||||
if len(row) >= 41:
|
||||
try:
|
||||
n_prompt = int(row[33])
|
||||
n_gen = int(row[34])
|
||||
avg_ts = float(row[39]) # avg_tokens_per_second
|
||||
|
||||
if n_prompt > 0 and n_gen == 0:
|
||||
# This is the prefill row
|
||||
prefill = avg_ts
|
||||
ctx = row[40].strip() if row[40].strip() else "?"
|
||||
return prefill, None, ctx
|
||||
elif n_prompt > 0 and n_gen > 0:
|
||||
# Mixed prefill+decode: avg_ts is overall, use prefill row if available
|
||||
if prefill is None:
|
||||
prefill = avg_ts # fallback
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f" CSV parse error: {e}", flush=True)
|
||||
|
||||
return prefill, decode, ctx
|
||||
|
||||
results = []
|
||||
for tb in TB_LIST:
|
||||
print(f"=== threads-batch={tb} ===", flush=True)
|
||||
kill_all()
|
||||
|
||||
srv = subprocess.Popen(
|
||||
[SERVE, "--model", MODEL, "--port", "7770", "--jinja",
|
||||
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
||||
"--threads", "16", "--threads-batch", str(tb),
|
||||
"--parallel", "2", "--ctx-size", "524288",
|
||||
"--batch-size", "4096", "--ubatch-size", "4096",
|
||||
"--flash-attn", "on",
|
||||
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
||||
"--cache-prompt", "--cache-idle-slots", "--metrics"],
|
||||
env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
ready = wait_ready()
|
||||
if not ready:
|
||||
srv.terminate()
|
||||
srv.wait()
|
||||
out = srv.stdout.read(1000)
|
||||
print(f"Server failed: {out[:300]}")
|
||||
sys.exit(1)
|
||||
print("Server ready, running bench...", flush=True)
|
||||
|
||||
t0 = time.time()
|
||||
try:
|
||||
# Use n-gen=0 to get prefill-only throughput
|
||||
res = subprocess.run(
|
||||
[BENCH, "--model", MODEL, "--threads", "16",
|
||||
"--n-prompt", "65536", "--n-gen", "0",
|
||||
"--repetitions", "3", "--output", "csv",
|
||||
"--flash-attn", "on", "--n-gpu-layers", "999"],
|
||||
env=ENV, capture_output=True, text=True, timeout=600)
|
||||
stdout = res.stdout + res.stderr
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Bench timed out!")
|
||||
srv.terminate()
|
||||
results.append({"tb": tb, "prefill": None, "decode": None, "ctx": None, "time": 600})
|
||||
continue
|
||||
|
||||
elapsed = round(time.time() - t0, 1)
|
||||
print(f"Bench rc={res.returncode} stdout_len={len(res.stdout)} stderr_len={len(res.stderr)}", flush=True)
|
||||
if res.returncode != 0:
|
||||
print(f"STDERR: {res.stderr[:500]}", flush=True)
|
||||
if res.stdout.strip():
|
||||
print(f"CSV OUTPUT:\n{res.stdout[:300]}", flush=True)
|
||||
|
||||
prefill, decode, ctx = parse_bench_output(res.stdout, res.stderr)
|
||||
|
||||
row = {"tb": tb, "prefill": prefill, "decode": decode, "ctx": ctx, "time": elapsed}
|
||||
results.append(row)
|
||||
|
||||
pt = f"{prefill:.1f}" if prefill else "N/A"
|
||||
dt = f"{decode:.1f}" if decode else "N/A"
|
||||
print(f" prefill={pt} tok/s decode={dt} tok/s ctx={ctx} time={elapsed}s", flush=True)
|
||||
|
||||
srv.terminate()
|
||||
srv.wait()
|
||||
time.sleep(2)
|
||||
|
||||
print("\n==========================================", flush=True)
|
||||
print("SUMMARY", flush=True)
|
||||
print("==========================================", flush=True)
|
||||
print(f"{'tb':>5} {'prefill':>12} {'decode':>12} {'ctx':>10} {'time':>8}", flush=True)
|
||||
print("-" * 60, flush=True)
|
||||
for r in results:
|
||||
pt = f"{r['prefill']:.1f}" if r["prefill"] else "N/A"
|
||||
dt = f"{r['decode']:.1f}" if r["decode"] else "N/A"
|
||||
print(f"{r['tb']:>5} {pt:>12} {dt:>12} {str(r['ctx']):>10} {r['time']:>8.0f}", flush=True)
|
||||
|
||||
best = max((r for r in results if r["prefill"]), key=lambda r: r["prefill"])
|
||||
print(f"\nBest: threads-batch={best['tb']} ({best['prefill']:.1f} tok/s)", flush=True)
|
||||
|
||||
with open("/tmp/bench_tb_results.txt", "w") as f:
|
||||
for r in results:
|
||||
pt = str(r["prefill"]) if r["prefill"] else "N/A"
|
||||
dt = str(r["decode"]) if r["decode"] else "N/A"
|
||||
f.write(f"{r['tb']},{pt},{dt},{r['ctx']},{r['time']}\n")
|
||||
@@ -2,7 +2,7 @@
|
||||
# Runtime: FLM 1.0.1 on the XDNA2 NPU (50 TOPS). Model: Gemma4-E4B-IT
|
||||
# (8B, Q4_1, vision + audio + reasoning). The NPU allocates from the
|
||||
# unified pool shared with frank-default-large (the CIRU llama.cpp
|
||||
# service on the iGPU). See docs/FastFlowLM NPU Inference.md.
|
||||
# service on the iGPU). See serverAppConfigs-docs/FastFlowLM NPU Inference.md.
|
||||
#
|
||||
# 2026-08-20: gemma4-it:e4b deployed. Footprint ~9.1 GB. 64k context.
|
||||
|
||||
|
||||
Executable
+408
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate two ~64k token prompts for n-gram spec benchmarking.
|
||||
|
||||
Usage:
|
||||
python3 scripts/generate-prompts.py <output_dir>
|
||||
|
||||
Outputs:
|
||||
<output_dir>/prompt_code.txt — code-heavy (~64k tokens)
|
||||
<output_dir>/prompt_prose.txt — prose-heavy (~64k tokens)
|
||||
|
||||
The prompts are designed to be structurally different so we can observe
|
||||
whether n-gram speculative decoding helps more on one type than the other.
|
||||
|
||||
Code prompt: structured, algorithmic, repetitive patterns (good for n-gram).
|
||||
Prose prompt: natural language, varied vocabulary (poor match for n-gram).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
|
||||
# Rough token estimate: 1 token ≈ 4 chars for English/code
|
||||
TOKEN_CHAR_RATIO = 4.0
|
||||
TARGET_TOKENS = 64000
|
||||
TARGET_CHARS = int(TARGET_TOKENS * TOKEN_CHAR_RATIO)
|
||||
|
||||
|
||||
def generate_code_prompt():
|
||||
"""Generate a code-heavy prompt with structured, repetitive patterns."""
|
||||
parts = []
|
||||
|
||||
# Section 1: Python algorithms (well-structured, good for n-gram matching)
|
||||
algorithms = [
|
||||
("Merge Sort", """
|
||||
def merge_sort(arr):
|
||||
\"\"\"Standard merge sort implementation.\"\"\"
|
||||
if len(arr) <= 1:
|
||||
return arr
|
||||
mid = len(arr) // 2
|
||||
left = merge_sort(arr[:mid])
|
||||
right = merge_sort(arr[mid:])
|
||||
return merge(left, right)
|
||||
|
||||
def merge(left, right):
|
||||
\"\"\"Merge two sorted arrays.\"\"\"
|
||||
result = []
|
||||
i = j = 0
|
||||
while i < len(left) and j < len(right):
|
||||
if left[i] <= right[j]:
|
||||
result.append(left[i])
|
||||
i += 1
|
||||
else:
|
||||
result.append(right[j])
|
||||
j += 1
|
||||
result.extend(left[i:])
|
||||
result.extend(right[j:])
|
||||
return result
|
||||
|
||||
# Test merge sort
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
test_data = [random.randint(1, 1000) for _ in range(1000)]
|
||||
sorted_data = merge_sort(test_data)
|
||||
assert sorted_data == sorted(test_data)
|
||||
print(f"Merge sort passed: {len(test_data)} items in {len(sorted_data)}")
|
||||
"""),
|
||||
("Binary Search", """
|
||||
def binary_search(arr, target):
|
||||
\"\"\"Binary search in a sorted array.\"\"\"
|
||||
low, high = 0, len(arr) - 1
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
if arr[mid] == target:
|
||||
return mid
|
||||
elif arr[mid] < target:
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
return -1
|
||||
|
||||
# Test binary search
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
test_data = sorted(random.sample(range(1, 10000), 500))
|
||||
for target in [test_data[0], test_data[-1], test_data[250], -1]:
|
||||
idx = binary_search(test_data, target)
|
||||
if target in test_data:
|
||||
assert test_data[idx] == target
|
||||
else:
|
||||
assert idx == -1
|
||||
print(f"Binary search passed: {len(test_data)} items")
|
||||
"""),
|
||||
("DFS/BFS", """
|
||||
from collections import deque
|
||||
|
||||
def bfs(graph, start):
|
||||
\"\"\"Breadth-first search.\"\"\"
|
||||
visited = set()
|
||||
queue = deque([start])
|
||||
visited.add(start)
|
||||
order = []
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
order.append(node)
|
||||
for neighbor in sorted(graph.get(node, [])):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
queue.append(neighbor)
|
||||
return order
|
||||
|
||||
def dfs(graph, start, visited=None):
|
||||
\"\"\"Depth-first search (iterative).\"\"\"
|
||||
if visited is None:
|
||||
visited = set()
|
||||
stack = [start]
|
||||
order = []
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if node not in visited:
|
||||
visited.add(node)
|
||||
order.append(node)
|
||||
for neighbor in reversed(sorted(graph.get(node, []))):
|
||||
if neighbor not in visited:
|
||||
stack.append(neighbor)
|
||||
return order
|
||||
|
||||
# Test graph traversal
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
nodes = list(range(50))
|
||||
graph = {n: random.sample(nodes, random.randint(1, 5)) for n in nodes}
|
||||
bfs_order = bfs(graph, 0)
|
||||
dfs_order = dfs(graph, 0)
|
||||
print(f"BFS visited {len(bfs_order)} nodes, DFS visited {len(dfs_order)} nodes")
|
||||
"""),
|
||||
]
|
||||
|
||||
for name, code in algorithms:
|
||||
parts.append(f"# === {name} ===\n")
|
||||
parts.append(code)
|
||||
|
||||
# Section 2: Large data generation (repetitive structure)
|
||||
parts.append("\n# === Data Generation ===\n")
|
||||
parts.append("import json\nimport csv\nimport hashlib\n\n")
|
||||
|
||||
for i in range(200):
|
||||
parts.append(f"# Batch {i}\n")
|
||||
parts.append(f"record_{i} = {{\n")
|
||||
parts.append(f" 'id': {i},\n")
|
||||
parts.append(f" 'name': 'record_{i}',\n")
|
||||
parts.append(f" 'value': {i * 3.14159},\n")
|
||||
parts.append(f" 'hash': hashlib.md5(b'record_{i}').hexdigest(),\n")
|
||||
parts.append(f" 'nested': {{\n")
|
||||
for j in range(10):
|
||||
parts.append(f" 'key_{j}': {i * j},\n")
|
||||
parts.append(f" }}\n")
|
||||
parts.append(f"}}\n")
|
||||
parts.append(f"batch_{i}.append(record_{i})\n")
|
||||
|
||||
# Section 3: Configuration schema (highly repetitive, great for n-gram)
|
||||
parts.append("\n# === Configuration Schema ===\n")
|
||||
for i in range(50):
|
||||
parts.append(f"schema_{i} = {{\n")
|
||||
parts.append(f" 'type': 'object',\n")
|
||||
parts.append(f" 'properties': {{\n")
|
||||
for j in range(20):
|
||||
parts.append(f" 'field_{j}': {{\n")
|
||||
parts.append(f" 'type': 'string' if {j} % 2 else 'integer',\n")
|
||||
parts.append(f" 'required': {j} < 10,\n")
|
||||
parts.append(f" 'default': '{i}_{j}',\n")
|
||||
parts.append(f" 'description': 'Field {j} of schema {i}',\n")
|
||||
parts.append(f" }},\n")
|
||||
parts.append(f" }},\n")
|
||||
parts.append(f" 'required': ['field_0', 'field_1', 'field_2'],\n")
|
||||
parts.append(f" 'additionalProperties': False,\n")
|
||||
parts.append(f"}}\n")
|
||||
|
||||
# Section 4: SQL queries (structured, repetitive)
|
||||
parts.append("\n# === SQL Queries ===\n")
|
||||
for i in range(50):
|
||||
parts.append(f"-- Query batch {i}\n")
|
||||
parts.append(f"SELECT * FROM table_{i}\n")
|
||||
parts.append(f"WHERE col_{i} = {i}\n")
|
||||
parts.append(f"ORDER BY id\n")
|
||||
parts.append(f"LIMIT {i * 100};\n\n")
|
||||
parts.append(f"INSERT INTO table_{i} (id, name, value)\n")
|
||||
parts.append(f"VALUES ({i}, 'record_{i}', {i});\n\n")
|
||||
parts.append(f"UPDATE table_{i}\n")
|
||||
parts.append(f"SET value = {i} * 1.5\n")
|
||||
parts.append(f"WHERE id = {i};\n\n")
|
||||
|
||||
# Section 5: Documentation (structured, repetitive)
|
||||
parts.append("\n# === Documentation ===\n")
|
||||
for i in range(50):
|
||||
parts.append(f"## Section {i}\n\n")
|
||||
parts.append(f"### Overview\n")
|
||||
parts.append(f"This section covers topic {i} in detail. The purpose is to\n")
|
||||
parts.append(f"document the behavior of the system for component {i}.\n\n")
|
||||
parts.append(f"### Implementation\n")
|
||||
parts.append(f"The implementation uses a modular approach with {i} subcomponents.\n")
|
||||
parts.append(f"Each subcomponent handles a specific aspect of the functionality.\n\n")
|
||||
parts.append(f"### Testing\n")
|
||||
parts.append(f"Component {i} is tested with {i * 10} test cases.\n")
|
||||
parts.append(f"All tests pass with 100% coverage.\n\n")
|
||||
parts.append(f"### References\n")
|
||||
parts.append(f"- Reference {i}.1: Component specification\n")
|
||||
parts.append(f"- Reference {i}.2: Implementation guide\n")
|
||||
parts.append(f"- Reference {i}.3: Test documentation\n\n")
|
||||
|
||||
# Fill remaining content with more code blocks
|
||||
while sum(len(p) for p in parts) < TARGET_CHARS:
|
||||
parts.append(f"# Additional block\n")
|
||||
parts.append(f"def extra_{len(parts)}():\n")
|
||||
parts.append(f" \"\"\"Additional function {len(parts)}.\"\"\"\n")
|
||||
parts.append(f" x = {len(parts)}\n")
|
||||
parts.append(f" y = x * 2\n")
|
||||
parts.append(f" return x + y\n\n")
|
||||
|
||||
return "\n".join(parts)[:TARGET_CHARS]
|
||||
|
||||
|
||||
def generate_prose_prompt():
|
||||
"""Generate a prose-heavy prompt with varied vocabulary."""
|
||||
parts = []
|
||||
|
||||
vocabulary = [
|
||||
# Theme 1: Historical narrative
|
||||
("The Renaissance", [
|
||||
"The dawn of the Renaissance brought profound transformation to European civilization.",
|
||||
"Artists like Leonardo da Vinci and Michelangelo pushed the boundaries of human expression.",
|
||||
"The invention of the printing press by Gutenberg revolutionized the spread of knowledge.",
|
||||
"Scholars in Florence and Venice debated philosophy, science, and the nature of the cosmos.",
|
||||
"The Medici family funded countless artists, scholars, and architects.",
|
||||
"The rediscovery of classical Greek and Roman texts ignited intellectual renewal.",
|
||||
"Architecture shifted from the vertical aspiration of Gothic to the proportional harmony of classical orders.",
|
||||
"Patronage became a symbol of civic virtue and political power.",
|
||||
"The humanist movement placed human experience and reason at the center of intellectual inquiry.",
|
||||
"Mathematics, anatomy, and natural philosophy advanced through empirical observation.",
|
||||
]),
|
||||
# Theme 2: Scientific exposition
|
||||
("Quantum Mechanics", [
|
||||
"Quantum mechanics describes the behavior of matter and energy at atomic scales.",
|
||||
"The Schrödinger equation governs the evolution of quantum states over time.",
|
||||
"Wave-particle duality reveals that light and matter exhibit both wave and particle properties.",
|
||||
"The uncertainty principle limits the precision with which conjugate variables can be measured.",
|
||||
"Quantum entanglement creates correlations between particles that persist across any distance.",
|
||||
"The Copenhagen interpretation posits that measurement collapses the wave function.",
|
||||
"Superposition allows quantum systems to exist in multiple states simultaneously until measured.",
|
||||
"Quantum tunneling enables particles to traverse classically forbidden energy barriers.",
|
||||
"The double-slit experiment demonstrates interference patterns from single particles.",
|
||||
"Quantum computing leverages superposition and entanglement for computational advantage.",
|
||||
]),
|
||||
# Theme 3: Philosophical discourse
|
||||
("Ethics and Moral Reasoning", [
|
||||
"Moral philosophy grapples with questions of right and wrong, virtue and vice.",
|
||||
"Utilitarianism judges actions by their consequences and the happiness they produce.",
|
||||
"Deontological ethics emphasizes duties and rules rather than outcomes.",
|
||||
"Virtue ethics focuses on character development and flourishing rather than specific actions.",
|
||||
"The social contract theory posits that moral obligations arise from collective agreement.",
|
||||
"Moral skepticism challenges the possibility of objective moral knowledge.",
|
||||
"The problem of evil questions the compatibility of an omnipotent, benevolent deity with suffering.",
|
||||
"Free will debates center on whether human choices are genuinely open or determined.",
|
||||
"Moral relativism holds that ethical standards are culture-dependent rather than universal.",
|
||||
"The trolley dilemma illustrates tensions between utilitarian and deontological reasoning.",
|
||||
]),
|
||||
]
|
||||
|
||||
for theme_name, sentences in vocabulary:
|
||||
parts.append(f"## {theme_name}\n\n")
|
||||
for sentence in sentences:
|
||||
parts.append(f"{sentence} ")
|
||||
parts.append("\n\n")
|
||||
|
||||
# Section 2: Extended narrative with varied styles
|
||||
parts.append("## Extended Narrative\n\n")
|
||||
|
||||
narratives = [
|
||||
"The city stretched endlessly beneath a sky the color of television tuned to a dead channel.",
|
||||
"Mountains rose like ancient sentinels, their peaks lost in clouds that swirled and danced.",
|
||||
"The ocean whispered secrets to the shore, each wave carrying fragments of distant lands.",
|
||||
"In the quiet library, dust motes drifted through shafts of amber light filtering through stained glass.",
|
||||
"The market bustled with vendors calling out prices, the air thick with spices and laughter.",
|
||||
"Silent forests stretched for miles, their canopies forming a green cathedral overhead.",
|
||||
"The old lighthouse stood solitary against the storm, its beam cutting through the darkness.",
|
||||
"Rivers carved paths through valleys over millennia, patient and relentless.",
|
||||
"The desert shimmered in the heat, mirages dancing on the horizon like phantom cities.",
|
||||
"Snow fell silently on the rooftops, transforming the town into a winter wonderland.",
|
||||
"The ancient ruins whispered stories of a civilization long forgotten by the modern world.",
|
||||
"Stars scattered across the night sky like diamonds on velvet, billions of them.",
|
||||
"The forest floor was carpeted with ferns and moss, damp and fragrant with earth.",
|
||||
"Clocks ticked away in the old tower, each chime echoing through the narrow streets.",
|
||||
"The garden bloomed in an explosion of color, roses, tulips, and lavender in perfect arrangement.",
|
||||
"The train rattled through the countryside, passengers gazing at fields of golden wheat.",
|
||||
"Mountains echoed with the calls of eagles circling on thermal currents above.",
|
||||
"The village square hosted a weekly market where farmers sold their harvest and artisans displayed their crafts.",
|
||||
"A lone wolf howled in the distance, its mournful cry carrying across the valley.",
|
||||
"The cathedral vaulted overhead, ribbed arches converging at a central keystone.",
|
||||
]
|
||||
|
||||
for narrative in narratives:
|
||||
parts.append(f"{narrative} ")
|
||||
|
||||
# Section 3: Generate more content by varying the narratives
|
||||
extensions = [
|
||||
" Scholars have debated the implications of this phenomenon for decades, producing conflicting interpretations.",
|
||||
" Local legends claim that those who listen carefully can hear voices from centuries past.",
|
||||
" Researchers continue to study the effects, though evidence remains inconclusive.",
|
||||
" The architecture reflects a synthesis of influences from multiple cultures and time periods.",
|
||||
" Travelers who venture far enough often lose their sense of direction and time.",
|
||||
" The tradition has been passed down through generations, each adding their own interpretation.",
|
||||
" Modern technology has made it possible to study these phenomena with unprecedented precision.",
|
||||
" The natural beauty of the area attracts visitors from around the world seeking tranquility.",
|
||||
" Archaeologists uncovered artifacts dating back thousands of years, revealing a rich history.",
|
||||
" The climate has changed significantly over the centuries, altering the landscape dramatically.",
|
||||
" Scientists proposed multiple hypotheses to explain the observed patterns, each with varying degrees of support.",
|
||||
" The cultural significance of the site extends far beyond its physical boundaries.",
|
||||
" Visitors report feeling a profound sense of peace and connection to something greater.",
|
||||
" The geological formation resulted from a combination of volcanic activity and erosion over millions of years.",
|
||||
" The literature from this period provides insight into the social and political conditions of the time.",
|
||||
" Artists found inspiration in the dramatic landscapes, producing works that captured their essence.",
|
||||
" The discovery challenged established theories and prompted a reevaluation of fundamental assumptions.",
|
||||
" The community worked together to preserve the site, recognizing its importance to their heritage.",
|
||||
" Researchers used advanced imaging techniques to reveal hidden structures beneath the surface.",
|
||||
" The story has been retold in countless forms, from oral traditions to modern cinema.",
|
||||
]
|
||||
|
||||
for extension in extensions:
|
||||
parts.append(extension + " ")
|
||||
|
||||
# Section 4: More narrative variations
|
||||
parts.append("\n\n## Additional Observations\n\n")
|
||||
|
||||
observations = [
|
||||
"The interplay between light and shadow creates a dynamic visual experience that changes throughout the day.",
|
||||
"Historical records indicate that this practice dates back to the early medieval period.",
|
||||
"The geological composition of the region provides valuable insights into planetary formation processes.",
|
||||
"Cultural traditions in the area reflect a complex interweaving of indigenous and immigrant influences.",
|
||||
"The ecological balance of the ecosystem depends on a delicate network of symbiotic relationships.",
|
||||
"Technological advances have dramatically altered the way we understand and interact with the natural world.",
|
||||
"The linguistic diversity of the region is remarkable, with dozens of languages and dialects spoken.",
|
||||
"Astronomical observations from this location have contributed significantly to our understanding of the cosmos.",
|
||||
"The artistic movement that originated here influenced creative expression across multiple continents.",
|
||||
"Economic factors played a crucial role in shaping the political landscape of the era.",
|
||||
]
|
||||
|
||||
for obs in observations:
|
||||
parts.append(f"{obs} ")
|
||||
|
||||
# Section 5: Extended descriptions with varied sentence structures
|
||||
parts.append("\n\n## Detailed Descriptions\n\n")
|
||||
|
||||
descriptions = [
|
||||
("The coastal town of Portmore sat nestled between steep cliffs and a sheltered bay, its whitewashed houses cascading down to a harbor filled with fishing boats and pleasure craft. The smell of salt and fish hung in the air, mingling with the aroma of fresh bread from the bakery on Main Street. Every morning at dawn, the fishermen would set out, their boats cutting through the glassy water, returning by midday with their catch. The market would be alive with the sounds of haggling, the glint of silver scales in the sunlight, and the colorful piles of fish laid out on ice. In the afternoons, tourists would wander the narrow streets, browsing artisan shops and sampling local wines at small tavernas. The old fortification on the headland offered panoramic views of the coastline, stretching for miles in both directions, with the open sea beyond.",
|
||||
"Nestled in the foothills of the mountain range, the village of Eldermere had remained largely unchanged for centuries. Its cobblestone streets wound between timber-framed houses with thatched roofs, their gardens overflowing with herbs and flowers. The village center was dominated by a medieval church with a stone tower that had been added during the fourteenth century. The parish priest, Father Thomas, had served the community for over three decades, and the villagers trusted him implicitly. On Sundays, the church would be filled to capacity, and the choir would sing hymns that echoed through the vaulted ceiling. Outside, the hills were covered in pastures where sheep grazed contentedly, tended by shepherds who knew every animal by name. The seasonal rhythms of the village followed the agricultural calendar: planting in spring, harvest in autumn, and a brief period of rest in winter. Local festivals celebrated these transitions with music, dance, and feasting that brought the community together."),
|
||||
]
|
||||
|
||||
for desc in descriptions:
|
||||
parts.append(f"{desc} ")
|
||||
|
||||
# Fill remaining to reach target
|
||||
while sum(len(p) for p in parts) < TARGET_CHARS:
|
||||
parts.append(f"{random.choice([
|
||||
"The morning sun cast long shadows across the landscape, illuminating every detail with warm, golden light.",
|
||||
"In the silence that followed, the weight of what had been said hung heavy in the air between them.",
|
||||
"The old manuscript had survived centuries of conflict, fire, and neglect, its pages yellowed but still legible.",
|
||||
"Scientists had been studying the phenomenon for years, yet its underlying mechanisms remained poorly understood.",
|
||||
"The city's architecture told the story of its evolution, from medieval fortifications to modern skyscrapers.",
|
||||
"Every season brought its own character to the valley, transforming the landscape in subtle but dramatic ways.",
|
||||
"The debate raged on, scholars on all sides presenting arguments and counterarguments with fierce conviction.",
|
||||
"The ancient tree stood as a testament to endurance, its gnarled branches reaching skyward after thousands of years.",
|
||||
"Cultural exchange between neighboring regions had enriched both societies, leading to innovations in art and technology.",
|
||||
"The discovery of the artifact reshaped our understanding of the civilization that had created it.",
|
||||
])} ")
|
||||
|
||||
return "\n".join(parts)[:TARGET_CHARS]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <output_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
output_dir = sys.argv[1]
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
print(f"Generating prompts...")
|
||||
|
||||
code_prompt = generate_code_prompt()
|
||||
prose_prompt = generate_prose_prompt()
|
||||
|
||||
code_path = os.path.join(output_dir, "prompt_code.txt")
|
||||
prose_path = os.path.join(output_dir, "prompt_prose.txt")
|
||||
|
||||
with open(code_path, "w") as f:
|
||||
f.write(code_prompt)
|
||||
with open(prose_path, "w") as f:
|
||||
f.write(prose_prompt)
|
||||
|
||||
# Estimate token counts
|
||||
code_tokens = len(code_prompt) // TOKEN_CHAR_RATIO
|
||||
prose_tokens = len(prose_prompt) // TOKEN_CHAR_RATIO
|
||||
|
||||
print(f"Code prompt: {code_tokens:.0f} estimated tokens ({len(code_prompt):,} bytes) → {code_path}")
|
||||
print(f"Prose prompt: {prose_tokens:.0f} estimated tokens ({len(prose_prompt):,} bytes) → {prose_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -5,7 +5,7 @@
|
||||
# (XDNA2, 50 TOPS) is a single-lane device: generations serialize in FIFO
|
||||
# order, so this is a background/triage endpoint. The NPU shares the ~30 GiB
|
||||
# unified pool with frank-default-large (the CIRU llama.cpp service on the
|
||||
# iGPU); see docs/FastFlowLM NPU Inference.md for the memory budget.
|
||||
# iGPU); see serverAppConfigs-docs/FastFlowLM NPU Inference.md for the memory budget.
|
||||
#
|
||||
# NOTE: FLM requires unlimited memlock. The unit sets LimitMEMLOCK=infinity.
|
||||
set -euo pipefail
|
||||
|
||||
+10
-1
@@ -164,7 +164,7 @@ for path in "${CHANGED[@]}"; do
|
||||
scripts/*)
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
docs/*)
|
||||
serverAppConfigs-docs/*)
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
@@ -180,6 +180,15 @@ for path in "${CHANGED[@]}"; do
|
||||
.AGENTTRASH/*)
|
||||
# Trash moves are not testable; skip.
|
||||
;;
|
||||
.gitignore|.env.example)
|
||||
# Ignore/ignore-list changes change nothing at runtime; not testable.
|
||||
;;
|
||||
old/scripts/*|old/scripts/**)
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
report/*)
|
||||
# Frozen audit reports are point-in-time artifacts; not testable.
|
||||
;;
|
||||
*)
|
||||
die "no focused-test mapping for '$path'; add one before committing"
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user