File size: 7,338 Bytes
7f02c5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/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()