File size: 4,032 Bytes
6184f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""PyTorch fp16 reference for the browser read-out, at deployment scale.

The published anchor table was measured on an earlier 4,000-image head against
a 1,000-image pool. This scores the SHIPPED head against the SHIPPED gallery so
the recovery fraction quoted for the deployment is the deployment's own.

Projection matches the runtime exactly (rust/srt-geometry/src/head.rs):
out = W (v - mu) + b, then L2 normalise. Pooling is mean over tokens, tap L28.

    python browser_anchor_reference.py \
        --head ../BlackWindow/wasm/dist/head.safetensors \
        --index artifacts/local/browser/gallery_123k_v3.srtidx \
        --replay artifacts/local/browser/replay_123k.json \
        --out artifacts/nla/q4/browser_fp16_reference_123k.json
"""
from __future__ import annotations

import argparse
import json
import struct

import numpy as np
import torch
from safetensors.torch import load_file


def load_index(path: str) -> tuple[np.ndarray, list[str]]:
    raw = open(path, "rb").read()
    dim, n = struct.unpack_from("<II", raw, 8)
    off = 16
    scales = np.frombuffer(raw, "<f4", n, off); off += 4 * n
    q = np.frombuffer(raw, np.int8, n * dim, off).reshape(n, dim); off += n * dim
    keys = []
    for _ in range(n):
        (l,) = struct.unpack_from("<I", raw, off); off += 4
        keys.append(raw[off:off + l].decode()); off += l
    z = q.astype(np.float32) * scales[:, None]
    z /= np.linalg.norm(z, axis=1, keepdims=True) + 1e-8
    return z, keys


def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--head", required=True)
    p.add_argument("--index", required=True)
    p.add_argument("--replay", required=True)
    p.add_argument("--model", default="Qwen/Qwen3-0.6B")
    p.add_argument("--layer", type=int, default=28)
    p.add_argument("--batch", type=int, default=32)
    p.add_argument("--out", required=True)
    a = p.parse_args()

    from transformers import AutoModelForCausalLM, AutoTokenizer

    replay = json.load(open(a.replay))
    caps = replay["captions"]
    gal, keys = load_index(a.index)
    print(f"gallery {gal.shape}, captions {len(caps)}", flush=True)

    h = load_file(a.head)
    W = h["txt.weight"].float().numpy()
    b = h["txt.bias"].float().numpy()
    mu = h["mu_txt"].float().numpy()

    dev = "mps" if torch.backends.mps.is_available() else "cpu"
    tok = AutoTokenizer.from_pretrained(a.model)
    model = AutoModelForCausalLM.from_pretrained(a.model, dtype=torch.float16).to(dev).eval()

    qs = np.zeros((len(caps), W.shape[0]), np.float32)
    with torch.no_grad():
        for i in range(0, len(caps), a.batch):
            chunk = [c["text"] for c in caps[i:i + a.batch]]
            enc = tok(chunk, return_tensors="pt", padding=True, truncation=True,
                      max_length=64).to(dev)
            out = model(**enc, output_hidden_states=True)
            hs = out.hidden_states[a.layer].float()
            m = enc["attention_mask"].unsqueeze(-1).float()
            pooled = ((hs * m).sum(1) / m.sum(1)).cpu().numpy()
            v = (pooled - mu) @ W.T + b
            v /= np.linalg.norm(v, axis=1, keepdims=True) + 1e-8
            qs[i:i + len(chunk)] = v
            if i % (a.batch * 20) == 0:
                print(f"  {i}/{len(caps)}", flush=True)

    gold = np.array([c["gold"] for c in caps])
    ranks = np.empty(len(caps), np.int64)
    for i in range(0, len(caps), 256):
        s = qs[i:i + 256] @ gal.T
        g = s[np.arange(len(s)), gold[i:i + 256]]
        ranks[i:i + 256] = (s > g[:, None]).sum(1)
    res = {
        "runtime": f"pytorch fp16 {a.model} L{a.layer} on {dev}",
        "head": a.head, "index": a.index,
        "n_images": len(keys), "n_captions": len(caps),
        "t2i": {f"r@{k}": round(float((ranks < k).mean()), 4) for k in (1, 5, 10)},
        "median_rank": float(np.median(ranks)) + 1.0,
    }
    print(json.dumps(res, indent=1))
    json.dump(res, open(a.out, "w"), indent=1)
    print(f"wrote {a.out}")


if __name__ == "__main__":
    main()