granite-decisions / scripts /smoke_native.py
adampippert's picture
Publish Granite Decisions v0.1.0 training starter and evaluation evidence
577b44e verified
Raw History Blame Contribute Delete
6.16 kB
"""Launch real llama.cpp subprocesses and check Granite interfaces on one host.
This is an integration smoke test, not a decision-quality benchmark.
Defaults to ROCm offload for Hyde; --gpu-layers 0 is an explicit CPU reference.
"""
import argparse
import json
import os
import re
import subprocess
import time
from pathlib import Path
import numpy as np
from granite_decisions import DecisionEngine
from granite_decisions.contracts import dumps
from granite_decisions.llamacpp import BackendError, BaselineBackend, LlamaRuntime, NativeClient, bind_runtime, gpu_offload_evidence
from granite_decisions.training import fit_head
parser = argparse.ArgumentParser()
parser.add_argument("--server", required=True)
parser.add_argument("--gguf", required=True)
parser.add_argument("--gpu-layers", type=int, default=99)
parser.add_argument("--out", required=True)
parser.add_argument("--log-dir", help="New directory for retained server logs (default: OUT.logs)")
args = parser.parse_args()
if Path(args.out).exists():
raise SystemExit("Output already exists; choose a new report path.")
log_directory = Path(args.log_dir or (args.out + ".logs"))
log_directory.mkdir(parents=True, exist_ok=False)
server, model = str(Path(args.server).resolve()), str(Path(args.gguf).resolve())
report = {"test": "real_granite_native_interface_smoke", "gpu_layers_requested": args.gpu_layers, "quality_benchmark": False}
devices = subprocess.run([server, "--list-devices"], check=True, capture_output=True, text=True)
report["devices"] = devices.stdout + devices.stderr
if args.gpu_layers > 0 and not re.search(r"(?:ROCm|HIP)\d", report["devices"]):
raise SystemExit("No ROCm device reported; refusing to call this a GPU test.")
try:
for mode, port in (("baseline", 19092), ("embedding", 19091)):
log_path = log_directory / (mode + ".log")
command = [server, "--model", model, "--host", "127.0.0.1", "--port", str(port),
"--ctx-size", "4096", "--batch-size", "4096", "--ubatch-size", "4096",
"--verbosity", "4", "--cors-origins", "localhost",
"--parallel", "1", "--n-gpu-layers", str(args.gpu_layers),
"--threads", "4", "--threads-batch", "4", "--no-context-shift", "--no-webui"]
if mode == "embedding":
command += ["--embedding", "--pooling", "last"]
with log_path.open("w") as log:
proc = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)
try:
client = NativeClient(f"http://127.0.0.1:{port}", timeout=120, api_key=os.environ.get("LLAMA_API_KEY"))
deadline = time.monotonic() + 90
while True:
if proc.poll() is not None:
raise RuntimeError("llama-server exited: " + log_path.read_text()[-3000:])
try:
if client.request("/health").get("status") == "ok":
break
except BackendError:
pass
if time.monotonic() > deadline:
raise RuntimeError("server did not become ready")
time.sleep(.25)
evidence = gpu_offload_evidence(log_path.read_text()) if args.gpu_layers > 0 else {"basis": "explicit_cpu_reference"}
manifest = bind_runtime(client, model, mode=mode)
runtime = LlamaRuntime(client, manifest)
if mode == "baseline":
qs = {
"route": {"type": "choice", "instructions": "What does the request ask to do?", "criteria": {
"code": "Write or fix code.", "retrieve": "Find documentation.", "unknown": "Cannot determine the request."}},
"python_mentioned": {"type": "noul", "instructions": "Does the request explicitly mention Python?"},
"complexity": {"type": "score", "instructions": "How complex is the request?", "criteria": ["Simple task", "Several dependent steps", "Open-ended research"]},
}
response = DecisionEngine(BaselineBackend(runtime)).evaluate({"request": "Write a Python function that adds two integers."}, qs)
assert response["status"] == "abstained", "Uncalibrated scores must abstain by default"
report[mode] = response
else:
data = [{"request": text} for text in (
"Write a Python function to add two integers.",
"Fix the Python function with a syntax error.",
"Find documentation for Python list methods.",
"Find the official API reference for Python dictionaries.")]
start = time.perf_counter()
x = runtime.encode(data)
assert x.shape == (4, 2560), x.shape
assert np.isfinite(x).all()
np.testing.assert_allclose(np.linalg.norm(x, axis=1), 1, atol=1e-3)
w, b = fit_head(x, [0, 0, 1, 1], 2)
z = x @ w + b
assert np.isfinite(z).all() and z.shape == (4, 2)
report[mode] = {"runtime": manifest, "feature_shape": list(x.shape),
"l2_norms": np.linalg.norm(x, axis=1).tolist(),
"training_smoke_logits": z.tolist(), "calibration": "not_fitted",
"elapsed_ms_including_four_encodings_and_head_fit": (time.perf_counter() - start)*1000}
report[mode]["offload_evidence"] = evidence
print(mode + " interface passed", flush=True)
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
except BaseException as exc:
report["status"] = "failed"
report["error"] = str(exc)
report["server_logs"] = str(log_directory)
Path(args.out).write_text(dumps(report) + "\n")
raise
report["status"] = "passed"
report["server_logs"] = str(log_directory)
Path(args.out).write_text(dumps(report) + "\n")
print(json.dumps({"status": "passed", "report": args.out}))