r-4ti0's picture
Add README, host runtime, and device staging scripts
7f02c5a verified
Raw
History Blame Contribute Delete
7.34 kB
#!/usr/bin/env python3
"""Host orchestrator for the A16W8 v79 correctness gate.
Runs the autoregressive decode loop by driving qnn-net-run on-device once per token.
KV (~288MB) stays resident on device as files; only tiny per-step tensors cross adb.
Prereqs on device (staged by push_gate.sh):
/data/local/tmp/gemma/{bin,lib,dsp,artifacts,step,kv,out}
Host math from hostlib.py (gemma3n scaling, tied softcapped lm_head).
Usage:
python run_gate.py --prompt "The capital of France is" --ntokens 15 [--adb-serial X]
Optionally --hf-check to compare against HF greedy on host (needs full model; heavy).
"""
import argparse, os, subprocess, sys, time, pathlib, numpy as np
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import hostlib
BASE = "/data/local/tmp/gemma"
STEP = f"{BASE}/step"
KV = f"{BASE}/kv"
OUTR = f"{BASE}/out/Result_0"
LOCAL_STEP = pathlib.Path("/tmp/gemma_step")
CTX = hostlib.CTX
H = hostlib.H
KV_HD = hostlib.KV_HD
def adb(args, serial=None, **kw):
cmd = ["adb"] + (["-s", serial] if serial else []) + args
return subprocess.run(cmd, capture_output=True, text=True, **kw)
def adb_shell(script, serial=None, timeout=600):
return adb(["shell", script], serial=serial, timeout=timeout)
def push(local, remote, serial=None):
r = adb(["push", str(local), remote], serial=serial, timeout=300)
if r.returncode != 0:
raise RuntimeError(f"push failed {local}->{remote}: {r.stderr}")
def pull(remote, local, serial=None):
r = adb(["pull", remote, str(local)], serial=serial, timeout=300)
if r.returncode != 0:
raise RuntimeError(f"pull failed {remote}->{local}: {r.stderr}")
def seed_kv(serial, wgqa=False):
"""Zero the 15 past_k/v buffers on device.
Created ON DEVICE with dd rather than pushed: they are ~144MB of zeros, and pushing
them over the QDC tunnel took minutes and was the flakiest part of the run (a reset
mid-seed leaves a partial buffer and silently corrupts the whole generation).
WGQA uses a WIN-entry ring for sliding layers, so buffer depth is per-layer.
"""
LOCAL_STEP.mkdir(exist_ok=True)
adb_shell(f"mkdir -p {KV} {STEP} {BASE}/out", serial=serial)
depths = hostlib.KV_BUF if wgqa else [CTX] * hostlib.NC
cmds = [f"rm -f {KV}/*.raw"]
for i in range(hostlib.NC):
nbytes = depths[i] * KV_HD[i] * 4 # [1,1,depth,hd] float32
for kind in ("k", "v"):
cmds.append(f"dd if=/dev/zero of={KV}/past_{kind}_{i}.raw bs=4096 "
f"count={nbytes // 4096} 2>/dev/null")
r = adb_shell(" && ".join(cmds), serial=serial, timeout=600)
if r.returncode != 0:
raise RuntimeError(f"on-device KV seed failed: {r.stderr}")
def write_step_inputs(m, token_id, pos, serial, wgqa=False):
ie, ple = m.embeds(token_id)
full, slide = m.masks_wgqa(pos) if wgqa else m.masks(pos)
files = {
"inputs_embeds": ie.astype(np.float32),
"per_layer_inputs": ple.astype(np.float32),
"position_ids": np.array([[pos]], np.int32),
"cache_position": np.array([pos], np.int32),
"full_mask": full.astype(np.float32),
"sliding_mask": slide.astype(np.float32),
}
for name, arr in files.items():
p = LOCAL_STEP / f"{name}.raw"
arr.tofile(p)
push(p, f"{STEP}/{name}.raw", serial=serial)
def run_step(serial, script="gate_ondevice.sh"):
r = adb_shell(f"sh {BASE}/{script}", serial=serial, timeout=600)
if "STEP_OK" not in r.stdout:
raise RuntimeError(f"net-run step failed:\nSTDOUT:{r.stdout}\nSTDERR:{r.stderr}")
return r
def fetch_hidden(serial):
pull(f"{OUTR}/hidden.raw", LOCAL_STEP / "hidden.raw", serial=serial)
return np.fromfile(LOCAL_STEP / "hidden.raw", dtype=np.float32).reshape(H)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--prompt", default="The capital of France is")
ap.add_argument("--ntokens", type=int, default=15)
ap.add_argument("--adb-serial", default=os.environ.get("ADB_SERIAL"))
ap.add_argument("--hf-check", action="store_true")
ap.add_argument("--wgqa", action="store_true",
help="target the windowed + broadcast-GQA decode graph (512-entry ring "
"buffers on sliding layers, 512-wide sliding mask)")
ap.add_argument("--script", default=None,
help="override the on-device step script (e.g. gate_ondevice_int8kv.sh)")
ap.add_argument("--chat", action="store_true",
help="wrap the prompt in the Gemma-4 chat template (required for coherent "
"output from the -it model; raw completion format degenerates)")
args = ap.parse_args()
print("loading host model (embeddings + tokenizer)...", flush=True)
m = hostlib.HostModel()
ids = m.encode_chat(args.prompt) if args.chat else m.encode(args.prompt)
print(f"prompt: {args.prompt!r} (chat_template={args.chat})\nids: {ids}", flush=True)
print("seeding KV buffers on device...", flush=True)
seed_kv(args.adb_serial, wgqa=args.wgqa)
seq = ids[:]
gen_ids = []
pos = 0
t_steps = []
# prefill+decode: feed prompt tokens one-by-one (pos advances), then greedy-generate
total = len(seq) + args.ntokens
nxt = None
for step in range(total):
t = seq[step] if step < len(seq) else nxt
write_step_inputs(m, t, pos, args.adb_serial, wgqa=args.wgqa)
t0 = time.time()
run_step(args.adb_serial, (args.script or ("gate_ondevice_wgqa.sh" if args.wgqa else "gate_ondevice.sh")))
dt = time.time() - t0
t_steps.append(dt)
hidden = fetch_hidden(args.adb_serial)
pos += 1
if step >= len(seq) - 1: # last prompt token onward -> predict next
nxt = m.argmax_next(hidden)
gen_ids.append(nxt)
print(f" step {step:2d} pos {pos-1:2d} {dt*1000:7.1f}ms -> id {nxt:6d} {m.decode([nxt])!r}", flush=True)
if nxt in hostlib.STOP_IDS:
print(" (stop token reached)", flush=True)
break
text = m.decode([t for t in gen_ids if t not in hostlib.STOP_IDS])
print("\n=== GENERATION ===")
print("continuation:", repr(text))
print(f"per-step wall (incl adb+netrun init): mean {1000*np.mean(t_steps):.0f}ms min {1000*min(t_steps):.0f}ms")
print("NOTE: this wall time is NOT throughput (net-run reloads context each step). Coherence/accuracy only.")
if args.hf_check:
hf_compare(m, ids, gen_ids)
def hf_compare(m, ids, gen_ids):
print("\n=== HF greedy reference (host, CPU) ===", flush=True)
import torch
from transformers import AutoModelForCausalLM
tok = (pathlib.Path.home() / ".cache/huggingface/token").read_text().strip()
mdl = AutoModelForCausalLM.from_pretrained("google/gemma-4-E2B-it", token=tok,
torch_dtype=torch.float32, device_map="cpu").eval()
with torch.no_grad():
out = mdl.generate(torch.tensor([ids]), max_new_tokens=len(gen_ids), do_sample=False)
hf = out[0][len(ids):].tolist()
print("HF :", repr(m.decode(hf)))
print("NPU :", repr(m.decode(gen_ids)))
match = sum(a == b for a, b in zip(hf, gen_ids))
print(f"token match: {match}/{len(gen_ids)}")
if __name__ == "__main__":
main()