File size: 7,479 Bytes
28c70af | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | #!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""0.3.8: the single-slot contract. `bankml serve --native` has one slot, as llama-server run with `-np 1` (Savante's
flags): requests are served one at a time, and each reuses whatever prefix of its prompt the slot still holds from the
request before it — another conversation's, when sessions interleave.
1. Interleaved sessions: three Savante-style conversations take turns (A1 B1 C1 A2 B2 C2 A3 B3 C3) through
llama-server b11192 with its prompt cache on, so every turn finds the previous conversation's tokens in the slot.
Recorded: the text, the counts and `timings.cache_n`. bankML must give the same, turn by turn.
2. The queue (bankML alone; llama-server's order among simultaneous arrivals is not deterministic): four requests sent
at once are all answered, none refused or mixed, each with the text it gets when asked alone.
BANKML_GGML_LIB=<b11192 release dir> python3 testing/session_oracle.py --record Bonsai-1.7B-Q1_0
python3 testing/session_oracle.py --bankml Bonsai-1.7B-Q1_0
→ .models/oracle-sessions/sessions-<model>.jsonl"""
import json, os, subprocess, sys, threading, urllib.error, urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from json_oracle import SAVANTE, wait # noqa: E402
root = Path(__file__).resolve().parents[1]
out = root / ".models" / "oracle-sessions"
CONVERSATIONS = [
["What is a bonsai?", "How often should I water one?", "And in winter?"],
["Name a prime number.", "Is 91 prime?", "Why not?"],
["What is the moon made of?", "How far away is it?", "Could we live there?"],
]
QUEUE = ["Say one word for blue.", "Name a fruit.", "What is 3 times 4?", "Name a river."]
def post(base, path, body):
req = urllib.request.Request(base + path, json.dumps(body).encode(), {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=1800) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, {"raw": (e.read() or b"").decode("utf-8", "replace")}
def body(history, seed):
return {"messages": [{"role": "system", "content": SAVANTE}] + history, "max_tokens": 40, "temperature": 0.3, "seed": seed}
def interleave(ask):
"""The turns in interleaved order; `ask(body)` → (status, response). Each conversation keeps its own history."""
hist = [[] for _ in CONVERSATIONS]
turns = []
for ti in range(3):
for ci, conv in enumerate(CONVERSATIONS):
hist[ci].append({"role": "user", "content": conv[ti]})
b = body(hist[ci], 100 * ci + ti)
code, r = ask(b)
if code != 200:
turns.append({"conversation": ci, "turn": ti, "status": code, "error": r})
continue
text = r["choices"][0]["message"]["content"]
hist[ci].append({"role": "assistant", "content": text})
turns.append({"conversation": ci, "turn": ti, "status": 200, "text": text, "finish_reason": r["choices"][0]["finish_reason"],
"prompt_tokens": r["usage"]["prompt_tokens"], "completion_tokens": r["usage"]["completion_tokens"],
"cache_n": r["timings"]["cache_n"]})
return turns
def record(stem):
lib = Path(os.environ.get("BANKML_GGML_LIB", ""))
model = root / ".models" / f"{stem}.gguf"
if not (lib / "llama-server").exists() or not model.exists():
sys.exit("needs BANKML_GGML_LIB (b11192 release dir) and " + str(model))
base = "http://127.0.0.1:18309"
proc = subprocess.Popen([str(lib / "llama-server"), "-m", str(model), "--host", "127.0.0.1", "--port", "18309", "-t", "3", "-c", "2048",
"-np", "1", "--jinja", "--reasoning", "off", "--no-webui"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
wait(base, proc)
turns = interleave(lambda b: post(base, "/v1/chat/completions", b))
for t in turns:
print(t["conversation"], t["turn"], t["status"], t.get("cache_n"), t.get("prompt_tokens"), repr(t.get("text", ""))[:60], flush=True)
out.mkdir(parents=True, exist_ok=True)
dest = out / f"sessions-{stem}.jsonl"
dest.write_text("".join(json.dumps(t) + "\n" for t in turns))
print(f"{len(turns)} interleaved turns from llama-server b11192 ({stem}, -np 1) → {dest}")
finally:
proc.terminate()
proc.wait(timeout=60)
def live(stem):
binary = root / "target" / "release" / "bankml"
model = root / ".models" / f"{stem}.gguf"
fork = Path(os.environ.get("BANKML_FORKS", Path.home() / ".local/share/bankml/forks")) / f"{stem}.gguf.FORK.json"
rec = out / f"sessions-{stem}.jsonl"
for f in (binary, model, fork, rec):
if not f.exists():
sys.exit(f"needs {f}")
base = "http://127.0.0.1:18207"
proc = subprocess.Popen([str(binary), "serve", str(model), "--fork", str(fork), "--native", "--listen", "127.0.0.1:18207",
"--upstream", "127.0.0.1:18208", "--ctx", "2048"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
n = ok = 0
def check(name, good, detail=""):
nonlocal n, ok
n += 1
ok += bool(good)
if not good:
print(f" FAIL {name} {detail}")
try:
wait(base, proc)
post(base, "/api/generate", {"model": stem.lower(), "keep_alive": 0}) # an empty slot, as the fresh llama-server
want = [json.loads(l) for l in rec.read_text().splitlines()]
got = interleave(lambda b: post(base, "/v1/chat/completions", b))
for g, w in zip(got, want):
check(f"conversation {w['conversation']} turn {w['turn']}", g == w, f"\n got {json.dumps(g)[:300]}\n want {json.dumps(w)[:300]}")
check("as many turns as recorded", len(got) == len(want))
# the queue: each question alone, then all at once
ask = lambda q: post(base, "/v1/chat/completions", {"messages": [{"role": "system", "content": SAVANTE}, {"role": "user", "content": q}],
"max_tokens": 24, "temperature": 0})
alone = {}
for q in QUEUE:
code, r = ask(q)
alone[q] = r["choices"][0]["message"]["content"] if code == 200 else None
together = {}
def go(q):
together[q] = ask(q)
threads = [threading.Thread(target=go, args=(q,)) for q in QUEUE]
for t in threads:
t.start()
for t in threads:
t.join()
for q in QUEUE:
code, r = together[q]
check(f"queued {q!r}: answered, and as when asked alone", code == 200 and r["choices"][0]["message"]["content"] == alone[q],
f"{code} {json.dumps(r)[:200]} vs {alone[q]!r}")
finally:
proc.terminate()
proc.wait()
print(f"session oracle ({stem}): {ok} of {n} checks — interleaved conversations through one slot identical to llama-server b11192 "
f"(text, counts, cache_n), turn by turn; {len(QUEUE)} simultaneous requests queued and each answered as alone")
return ok == n
if __name__ == "__main__":
if len(sys.argv) != 3 or sys.argv[1] not in ("--record", "--bankml"):
sys.exit(__doc__)
if sys.argv[1] == "--record":
record(sys.argv[2])
else:
sys.exit(0 if live(sys.argv[2]) else 1)
|