File size: 9,960 Bytes
fcaf887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""The promotion gate, old readout vs corrected readout, on one checkpoint.

Three things were wrong with how CE(q||p) was measured, all of them inflating
it:

  prefix  data.py applied instance values only when train=True, so eval scored
          p on a sequence whose earlier empty cells held the unique solution.
          The model eliminates candidates from that -- correctly -- and was
          then charged for not matching a q that conditions on nothing.
  depth   evaluater.py sets k_budget = K, so one full-depth prediction was
          compared against every stage's q, including stages training far
          fewer slots.
  q       the stored pool stops at coverage (~5 grids, 1.4 by stage 11) and
          deliberately picks rare candidates, so its histogram is flattened
          toward uniform. The gate spends log|S| - H(q) as headroom, and that
          quantity is meaningless at n = 5: measured H rises from 0.70 at
          n = 3 to 0.99 at n = 7 against log|S| = 1.10, still climbing.

Reports the gate statistic under each combination so the contribution of each
correction is visible.

  python probe_gate_fixed.py CKPT STAGE [NPUZ] [BIGQ_NPZ]
"""

import functools
import os
import sys

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

CKPT = sys.argv[1]
S = int(sys.argv[2])
NPUZ = int(sys.argv[3]) if len(sys.argv) > 3 else 300
BIGQ = (sys.argv[4] if len(sys.argv) > 4
        else "/egr/research-slim/ghoshavr/ckpt_probe/bigq/test_bigq300.npz")
POP = np.array([bin(i).count("1") for i in range(512)], np.int16)

import jax                                                    # noqa: E402
import jax.numpy as jnp                                       # noqa: E402
from jax import random                                        # noqa: E402
from flax.training import checkpoints                         # noqa: E402
from flax import jax_utils                                    # noqa: E402
from train import main as train_main                          # noqa: E402
from train import data, model, trainer, evaluater             # noqa: E402

config = train_main.get_config()
K = int(config.num_latent_slots)
mc = model.TransformerConfig(
    dtype=config.dtype, vocab_size=config.vocab_size, seq_len=config.seq_len,
    num_heads=config.num_heads, num_layers=config.num_layers,
    emb_dim=config.emb_dim, qkv_dim=config.qkv_dim, mlp_dim=config.mlp_dim,
    dropout_rate=config.dropout_rate,
    attention_dropout_rate=config.attention_dropout_rate,
    deterministic=False, num_latent_slots=K,
    inject_latents=bool(int(getattr(config, "recurrent_latent", 1))))
rng = jax.random.PRNGKey(config.seed)
rng, ir, _ = random.split(rng, 3)
rng, dr = jax.random.split(rng)
net = model.TransformerLMHeadModel(mc)
_, iv = jax.jit(net.init_with_output)(
    {"params": ir, "dropout": dr},
    jnp.ones((config.minibatch_size, config.seq_len), jnp.int32),
    jnp.zeros((config.minibatch_size, K, config.emb_dim), mc.dtype),
    jnp.zeros((config.minibatch_size, K), jnp.int32),
    jnp.zeros((config.minibatch_size, K), bool))
st, _ = trainer.get_state(config, net, iv)
rs = checkpoints.restore_checkpoint(os.path.abspath(CKPT), st)
print(f"restored step {int(rs.step)}  stage {S}", flush=True)
state = jax_utils.replicate(rs)
pes = jax.pmap(functools.partial(
    evaluater.eval_step, config=mc.replace(deterministic=True)),
    axis_name="batch")

cur = data.CurriculumState(stage=S, max_stage=K)
ds = data.SudokuDataset(config, train=False, curriculum=cur)
cm = np.load(config.test_cand_masks_path, mmap_mode="r")
z = np.load(BIGQ)
bigq, biggrid = z["counts"], z["grids"]
sidx = S - 1
BS = config.minibatch_size
nd = jax.local_device_count()


def fwd(sq, lv, act, sp):
    def sh(x):
        x = np.asarray(x)
        return x.reshape((nd, x.shape[0] // nd) + x.shape[1:])
    lg, hd, _ = pes(state, sh(sq), sh(lv), sh(sp), sh(act))
    lg, hd = np.array(lg), np.array(hd)
    return lg.reshape(-1, *lg.shape[2:]), hd.reshape(-1, *hd.shape[2:])


def probs(seqs, si3, depth):
    """Digit probabilities with `depth` latent slots active and built."""
    n = len(seqs)
    sq = np.stack(seqs)
    s3 = np.asarray(si3)
    pad = (-n) % BS
    if pad:
        sq = np.concatenate([sq, np.zeros((pad, config.seq_len), sq.dtype)])
        s3 = np.concatenate([s3, np.full(pad, s3[0])])
    out = []
    for b in range(0, len(sq), BS):
        x = sq[b:b + BS].astype(np.int32)
        t3 = s3[b:b + BS]
        sp = t3[:, None] + np.arange(K)[None, :]
        slots = np.arange(K)[None, :]
        af = np.ones((len(x), K), bool)
        bi = np.arange(len(x))
        lv = np.zeros((len(x), K, config.emb_dim), np.float32)
        mk = x.copy()
        for j in range(len(x)):
            mk[j, t3[j] + K:] = 0
        for j in range(depth):
            _, hd = fwd(mk, lv, af & (slots < j), sp)
            lv[:, j] = hd[bi, t3 - 1 + j]
        lg, _ = fwd(x, lv, af & (slots < depth), sp)
        zz = lg - lg.max(-1, keepdims=True)
        p = np.exp(zz)
        p /= p.sum(-1, keepdims=True)
        out.append(p[:, :, 1:10].astype(np.float64))
    return np.concatenate(out)[:n]


# ---- Build every sequence variant once -------------------------------------
R = int(biggrid.shape[2])
rows, meta, sel = [], [], []
for idx in range(min(NPUZ, bigq.shape[0])):
    n5 = int(ds.inst_counts[idx, sidx])
    base = np.asarray(ds.eval_inputs[idx])
    si = int(ds.eval_start_index[idx, 0])
    rows.append(ds.insert_latent_slots(base, si))
    meta.append((idx, "sol", 0))
    for r in range(R):
        g = biggrid[idx, sidx, r]
        rows.append(ds.insert_latent_slots(ds._apply_values(base, g), si))
        meta.append((idx, "big", r))
    lo = int(ds.inst_starts[idx, sidx])
    n5c = max(n5, 0)
    for r in range(min(n5c, 4)):
        rows.append(ds.insert_latent_slots(
            ds.apply_instance(base, idx, sidx, inst_row=lo + r), si))
        meta.append((idx, "pool", r))
    sel.append((idx, si, min(n5c, 4)))

si3 = np.array([3 * int(ds.eval_start_index[m[0], 0]) for m in meta])
store = {}
for depth, tag in ((S, "d"), (K, "K")):
    P = probs(rows, si3, depth)
    for i, (idx, kind, r) in enumerate(meta):
        store.setdefault((tag, idx, kind), {})[r] = P[i]

# ---- Score ------------------------------------------------------------------
# Each combination names (which q, which prefix, which depth).
COMBO = [
    ("OLD  gate as it runs today", "pool5", "sol", "K"),
    ("  + stage depth only", "pool5", "sol", "d"),
    ("  + instance prefix", "pool5", "big", "d"),
    ("NEW  + large-sample q", "big", "big", "d"),
    ("     (large q, solution prefix)", "big", "sol", "d"),
]
agg = {c[0]: dict(ce=0.0, lk=0.0, hq=0.0, n=0.0, pt=0.0, un=0.0, spr=0.0,
                  mass=0.0, qt=0.0, puz=[]) for c in COMBO}

for idx, si, n5c in sel:
    seq = np.asarray(ds.insert_latent_slots(ds.eval_inputs[idx], si))
    bits = np.asarray(cm[idx, sidx], np.int64)
    tb = np.asarray(cm[idx, -1], np.int64)
    q5 = np.zeros((81, 9), np.float64)
    lo = int(ds.inst_starts[idx, sidx])
    n5 = int(ds.inst_counts[idx, sidx])
    if n5 > 0:
        A = np.asarray(ds.instances[lo:lo + n5], np.int64)
        for c in range(81):
            col = A[:, c]
            v = (col >= 1) & (col <= 9)
            np.add.at(q5[c], col[v] - 1, 1)
    qb = bigq[idx, sidx].astype(np.float64)
    per = {c[0]: dict(ce=0.0, lk=0.0, n=0.0) for c in COMBO}
    base0 = 3 * si + K
    for t in range(81 - si):
        vp = base0 + 3 * t + 2
        if vp >= config.seq_len:
            break
        r_, c_ = int(seq[vp - 2]), int(seq[vp - 1])
        if not (0 <= r_ < 9 and 0 <= c_ < 9):
            continue
        cell = r_ * 9 + c_
        k = int(POP[int(bits[cell])])
        if k < 2:
            continue
        inS = np.array([(int(bits[cell]) >> d) & 1 for d in range(9)], bool)
        true = int(tb[cell]).bit_length()
        lk = np.log(k)
        for name, qsrc, psrc, tag in COMBO:
            qc = q5[cell] if qsrc == "pool5" else qb[cell]
            tot = qc.sum()
            if tot <= 0:
                continue
            q = qc / tot
            if psrc == "sol":
                dig = store[(tag, idx, "sol")][0][vp - 1]
            else:
                src = "big" if psrc == "big" else "pool"
                d = store[(tag, idx, src)]
                dig = np.mean([d[r] for r in sorted(d)], axis=0)[vp - 1]
            ce = -float((q * np.log(np.maximum(dig, 1e-12))).sum())
            qn = q[q > 0]
            T = agg[name]
            T["ce"] += ce
            T["lk"] += lk
            T["hq"] += -float((qn * np.log(qn)).sum())
            T["n"] += 1
            T["pt"] += float(dig[true - 1])
            T["qt"] += float(q[true - 1])
            T["un"] += 1.0 / k
            m = float(dig[inS].sum())
            pin = dig[inS] / max(m, 1e-30)
            T["mass"] += m
            T["spr"] += float(-(pin * np.log(np.maximum(pin, 1e-30))).sum()) / lk
            per[name]["ce"] += ce
            per[name]["lk"] += lk
            per[name]["n"] += 1
    for name in per:
        if per[name]["n"] > 0:
            agg[name]["puz"].append(max(
                0.0, per[name]["ce"] / per[name]["n"]
                - per[name]["lk"] / per[name]["n"]))

BAR = 0.05
print(f"\n{len(sel)} held-out puzzles, stage {S}, bar hinge <= {BAR}")
print(f"{'readout':34s} {'q(true)':>8s} {'p(true)':>8s} {'1/|S|':>7s} "
      f"{'spread':>7s} {'H(q)':>7s} {'log|S|':>7s} {'CE':>7s} {'hinge':>7s}  verdict")
for name, _, _, _ in COMBO:
    T = agg[name]
    n = max(T["n"], 1)
    v = np.array(T["puz"])
    h = v.mean() if len(v) else -1.0
    print(f"{name:34s} {T['qt']/n:8.3f} {T['pt']/n:8.3f} {T['un']/n:7.3f} "
          f"{T['spr']/n:7.3f} {T['hq']/n:7.3f} {T['lk']/n:7.3f} "
          f"{T['ce']/n:7.3f} {h:7.4f}  "
          f"{'PASS' if 0 <= h <= BAR else 'FAIL'}")
print("\nhinge = mean over puzzles of max(0, mean CE(q||p) - mean log|S|)")