File size: 38,385 Bytes
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534ef49
 
6dd535b
534ef49
 
 
 
 
 
 
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534ef49
 
 
 
 
6dd535b
 
 
 
 
 
 
 
 
 
 
534ef49
6dd535b
 
 
 
 
 
 
534ef49
 
 
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cdc8b
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cdc8b
 
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cdc8b
 
6dd535b
 
 
 
c5cdc8b
6dd535b
 
411dfad
 
 
 
c5cdc8b
 
 
 
 
 
 
 
6dd535b
c5cdc8b
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411dfad
6dd535b
 
 
 
 
411dfad
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411dfad
6dd535b
 
 
 
 
 
 
 
411dfad
6dd535b
 
 
 
411dfad
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411dfad
 
 
 
 
 
 
 
6dd535b
 
 
 
411dfad
 
 
c5cdc8b
 
 
 
 
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cdc8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411dfad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6dd535b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cdc8b
 
411dfad
 
6dd535b
 
 
 
 
411dfad
6dd535b
 
 
 
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "torch",
#   "numpy",
#   "datasets>=2.19",
#   "huggingface_hub>=0.24",
#   "safetensors>=0.4",
# ]
# ///
"""KG-04 variable-rate proof: matched-cost quality-vs-compute frontier.

Runner for the protocol in trident.md Section 0.5.5.

Modes (env ``MODE``):
  * ``grid``       train the whole frontier in ONE job and push a scorecard
                   (recommended: one GPU allocation, atomic verdict).
  * ``train``      train a single point (env-driven) and push its result JSON.
  * ``aggregate``  collect previously pushed point JSONs into a scorecard.

Every point shares an identical architecture (``r_max`` and all dims fixed), so
parameter count is matched by construction; only the *commit policy* differs:

    fixed     R=k              -> exactly k write factors / patch
    adaptive  lambda_rate=lam  -> learned gates, cost pressure = lam

Cost axis = ``mean_factors`` (mean effective write factors = analytic write-FLOP
proxy; see Section 0.5.5). It is NOT wall-clock. Quality = held-out BPB
(TASK=text) or distance-stratified next-value accuracy (TASK=recall).

KG-04 passes only if the adaptive frontier lies strictly below the fixed-rate
frontier at matched mean cost with disjoint 95% CIs. The test can fail.

Nothing here touches Jobs or repos it did not create. Training runs on the
accelerator; the artefact is a CPU-only inference model after parity.
"""
import gc
import json
import math
import os
import sys
import time
from pathlib import Path

import torch


def env(k, d=None):
    v = os.environ.get(k)
    return v if v is not None and v != "" else d


def env_i(k, d):
    return int(env(k, d))


def env_f(k, d):
    return float(env(k, d))


def log(msg):
    print(f"[ablate] {msg}", flush=True)


def import_trident():
    try:
        import trident  # noqa: F401
        return
    except Exception:
        pass
    from huggingface_hub import snapshot_download
    code_repo = env("CODE_REPO", "farguney/trident")
    code_rev = env("CODE_REVISION", "main")
    workdir = Path("/tmp/trident_code")
    log(f"downloading {code_repo}@{code_rev} (src/**)")
    snapshot_download(
        repo_id=code_repo, revision=code_rev, repo_type="model",
        allow_patterns=["src/**"], local_dir=str(workdir),
    )
    sys.path.insert(0, str(workdir / "src"))


# ----------------------------------------------------------------------------
# synthetic distance-stratified recall control (MQAR-as-bytes)
# ----------------------------------------------------------------------------
# Byte ranges are disjoint so keys, values, filler, and the query marker never
# alias. The model must copy the value bound to a queried key from earlier in
# the stream *through the bounded recurrent state* -- the recurrent core has no
# attention over raw history. Distance = answer_pos - value_pos, so accuracy can
# be reported as a function of how far back the binding sits.
KEY_LO, KEY_HI = 1, 65          # keys in [1,65)
VAL_LO, VAL_HI = 65, 129        # values in [65,129)
FILL_LO, FILL_HI = 129, 254     # filler in [129,254)
ANSWER_MARK = 254               # "emit the recalled value next" cue
Q_MARK = 255                    # query marker byte
# Trident reads its associative state with one-patch latency: the decoder at a
# position conditions on the *previous* patch's committed state (causal design).
# So the query key and the scored answer must sit in DIFFERENT patches, else the
# query-conditioned read is never exposed and recall is at chance regardless of
# difficulty (verified empirically). Q_GAP filler bytes between the query key and
# the answer cue force >=1 patch boundary.
Q_GAP = 8


def make_recall_batch(B, seq_len, n_pairs, rng, device, pack="dense"):
    """Return bytes_in (B,L), answer_pos (B,), answer_val (B,), distance (B,).

    ``pack`` controls whether the R=1-vs-R>1 write bottleneck is exercised:

      * ``dense``  -- the ``n_pairs`` (key,val) bindings are laid *contiguously*
        in one block, so several bindings land inside a single entropy patch
        (~3-5 bytes). A patch commits one rank-1 delta write per factor, so with
        R=1 multiple co-patch bindings must share a single write and interfere;
        R>1 supplies extra independent writes. This is the controlled stimulus
        that can discriminate the commit rate. The block is placed so the query
        is a fixed distance away (retention held roughly constant across R).
      * ``sparse`` -- bindings scattered (mostly one per patch); R=1 usually
        suffices, so this is the negative control.
    """
    x = torch.from_numpy(rng.integers(FILL_LO, FILL_HI, size=(B, seq_len))).long()
    answer_pos = torch.zeros(B, dtype=torch.long)
    answer_val = torch.zeros(B, dtype=torch.long)
    distance = torch.zeros(B, dtype=torch.long)
    # query key at seq_len-Q_GAP, then filler (forces a patch boundary), then the
    # answer cue at seq_len-2; the value is scored as the byte predicted after it.
    q_mark_pos = seq_len - Q_GAP
    qk_pos = q_mark_pos + 1
    ans_pos = seq_len - 2
    for b in range(B):
        keys = rng.choice(range(KEY_LO, KEY_HI), size=n_pairs, replace=False)
        vals = rng.integers(VAL_LO, VAL_HI, size=n_pairs)
        if pack == "dense":
            base = 4 + int(rng.integers(0, max(1, seq_len // 16)))
            slots = [base + 2 * i for i in range(n_pairs)]
        else:
            region = max(4, int(seq_len * 2 // 3) - 2)
            slots = sorted(rng.choice(range(region), size=n_pairs, replace=False))
        val_by_key, val_pos = {}, {}
        for (k, v, s) in zip(keys, vals, slots):
            if s + 1 >= q_mark_pos - 1:  # keep bindings clear of the query block
                continue
            x[b, s] = int(k)
            x[b, s + 1] = int(v)
            val_pos[int(k)] = s + 1
            val_by_key[int(k)] = int(v)
        placed = list(val_by_key.keys())
        qk = int(rng.choice(placed))
        x[b, q_mark_pos] = Q_MARK
        x[b, qk_pos] = qk
        x[b, ans_pos] = ANSWER_MARK  # model predicts x[ans_pos+1]; we score that logit
        answer_pos[b] = ans_pos
        answer_val[b] = val_by_key[qk]
        distance[b] = ans_pos - val_pos[qk]
    return (x.to(device), answer_pos.to(device), answer_val.to(device), distance.to(device))


@torch.no_grad()
def eval_recall(model, device, seq_len, n_pairs, n_batches, B, amp_dtype, pack="dense"):
    import numpy as np
    rng = np.random.default_rng(1234)
    buckets = {}
    mf_sum, aR_sum, nb = 0.0, 0.0, 0
    model.eval()
    for _ in range(n_batches):
        x, apos, aval, dist = make_recall_batch(B, seq_len, n_pairs, rng, device, pack)
        with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=device == "cuda"):
            out = model(x, return_logits=True)
        logits = out["logits"].float()
        pred = logits[torch.arange(x.shape[0]), apos].argmax(-1)
        correct = (pred == aval)
        mf_sum += out["mean_factors"].item(); aR_sum += out["active_R"].item(); nb += 1
        for c, d in zip(correct.tolist(), dist.tolist()):
            bkt = "<=64" if d <= 64 else ("65-256" if d <= 256 else ">256")
            slot = buckets.setdefault(bkt, [0, 0])
            slot[0] += int(c); slot[1] += 1
    model.train()
    acc = {k: (v[0] / v[1] if v[1] else 0.0) for k, v in buckets.items()}
    tot = sum(v[0] for v in buckets.values()) / max(1, sum(v[1] for v in buckets.values()))
    return {"recall_acc_overall": tot, "recall_acc_by_distance": acc,
            "mean_factors": mf_sum / max(1, nb), "active_R": aR_sum / max(1, nb)}


# ----------------------------------------------------------------------------
# config
# ----------------------------------------------------------------------------
def make_cfg(rate_mode, fixed_r, lambda_rate, task, extra_over=None):
    from trident import TridentConfig
    profile = env("PROFILE", "micro")
    over = {}
    for key, cast in [
        ("d_model", int), ("n_blocks", int), ("n_heads", int), ("d_k", int),
        ("d_v", int), ("n_dec_layers", int), ("r_max", int), ("b_max", int),
        ("tau_b", float), ("max_patches", int), ("exact_ring", int),
        ("d_code", int), ("chunk_size", int), ("rho_min", float),
        ("gate_bias_init", float), ("factor_emb_init", float),
        ("lambda_fast_head", float),
    ]:
        val = env(key.upper())
        if val is not None:
            over[key] = cast(val)
    ula = env("USE_LOCAL_ATTN")
    if ula is not None:
        over["use_local_attn"] = ula not in ("0", "false", "False", "")
    if rate_mode == "fixed":
        over["fixed_r"] = int(fixed_r)
        over["lambda_rate"] = 0.0
    elif rate_mode == "adaptive":
        over["fixed_r"] = None
        over["lambda_rate"] = float(lambda_rate)
    else:
        raise ValueError(f"rate_mode must be fixed|adaptive, got {rate_mode}")
    if task == "recall":
        over.setdefault("lambda_fast_head", 0.0)
    if extra_over:  # per-point overrides (e.g. operator pool config)
        over.update(extra_over)
    if profile in ("micro", "scout"):
        return getattr(TridentConfig, profile)(**over)
    over.setdefault("profile", profile)
    return TridentConfig(**over)


def read_hp():
    return dict(
        max_steps=env_i("MAX_STEPS", 3000), warmup=env_i("WARMUP", 200),
        lr=env_f("LR", 6e-4), min_lr=env_f("MIN_LR", 6e-5),
        wd=env_f("WEIGHT_DECAY", 0.1), grad_clip=env_f("GRAD_CLIP", 1.0),
        batch=env_i("BATCH", 16), grad_accum=env_i("GRAD_ACCUM", 1),
        seq_len=env_i("SEQ_LEN", 2048), n_pairs=env_i("N_PAIRS", 8),
        val_windows=env_i("VAL_WINDOWS", 64), val_batch=env_i("VAL_BATCH", 8),
        val_batch_n=env_i("VAL_BATCH_N", 16), log_every=env_i("LOG_EVERY", 200),
        eval_every=env_i("EVAL_EVERY", 0), pack=env("RECALL_PACK", "dense"),
    )


# ----------------------------------------------------------------------------
# train a single frontier point (pure: returns a result dict, no Hub I/O)
# ----------------------------------------------------------------------------
def train_one(task, rate_mode, fixed_r, lambda_rate, seed, hp, device, amp_dtype,
              extra_over=None, point_id=None):
    import numpy as np
    from trident import Trident

    torch.manual_seed(seed); np.random.seed(seed)
    cfg = make_cfg(rate_mode, fixed_r, lambda_rate, task, extra_over=extra_over)
    model = Trident(cfg).to(device)
    nparams = sum(p.numel() for p in model.parameters())
    # GLR-1 refiner: sample a refinement budget per step (Universal-Transformer
    # depth-by-iteration). None => no refiner (budget arg ignored by the model).
    refine_budgets = ([int(b) for b in cfg.refine_budgets]
                      if getattr(cfg, "use_refiner", False) else None)
    if point_id is None:
        point_id = f"fixed_R{fixed_r}" if rate_mode == "fixed" else f"adaptive_L{lambda_rate}"
    # active payload FLOPs (constant in pool size N) for the operator experiment
    active_flops = 0
    stored_op = 0
    if getattr(cfg, "use_operators", False) and model.decoder.operators is not None:
        active_flops = sum(op.active_payload_flops() for op in model.decoder.operators)
        stored_op = sum(op.stored_operator_params() for op in model.decoder.operators)
    log(f"== point={point_id} seed={seed} task={task} params={nparams/1e6:.3f}M "
        f"stored_op={stored_op/1e6:.3f}M active_op_flops={active_flops} "
        f"identity={cfg.identity_hash()[:12]}")

    decay = [p for _, p in model.named_parameters() if p.ndim >= 2]
    no_decay = [p for _, p in model.named_parameters() if p.ndim < 2]
    opt = torch.optim.AdamW(
        [{"params": decay, "weight_decay": hp["wd"]}, {"params": no_decay, "weight_decay": 0.0}],
        lr=hp["lr"], betas=(0.9, 0.95), eps=1e-8,
    )

    def lr_at(s):
        if s < hp["warmup"]:
            return hp["lr"] * (s + 1) / hp["warmup"]
        t = min(1.0, (s - hp["warmup"]) / max(1, hp["max_steps"] - hp["warmup"]))
        return hp["min_lr"] + 0.5 * (hp["lr"] - hp["min_lr"]) * (1 + math.cos(math.pi * t))

    if task == "text":
        from trident.data import ByteWindowIterable, collate, load_fixed_byte_windows
        train_ds = ByteWindowIterable(
            dataset=env("DATASET", "HuggingFaceFW/fineweb-edu"), split=env("SPLIT", "train"),
            seq_len=hp["seq_len"], text_field=env("TEXT_FIELD", "text"),
            name=env("DATASET_NAME", "sample-10BT"),
            shuffle_buffer=env_i("SHUFFLE_BUFFER", 10000), seed=seed,
        )
        loader = torch.utils.data.DataLoader(
            train_ds, batch_size=hp["batch"], collate_fn=collate,
            num_workers=env_i("NUM_WORKERS", 2), drop_last=True,
        )
        data_iter = iter(loader)
        val_windows = load_fixed_byte_windows(
            dataset=env("VAL_DATASET", "Salesforce/wikitext"), split=env("VAL_SPLIT", "validation"),
            name=env("VAL_NAME", "wikitext-103-raw-v1"), text_field=env("VAL_TEXT_FIELD", "text"),
            seq_len=hp["seq_len"], num_windows=hp["val_windows"], add_eod=False,
        ).to(device)

        @torch.no_grad()
        def evaluate(budget=None):
            model.eval()
            tot_nll, tot_bytes, mf, aR, nb = 0.0, 0, 0.0, 0.0, 0
            for i in range(0, val_windows.shape[0], hp["val_batch"]):
                chunk = val_windows[i:i + hp["val_batch"]]
                with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=device == "cuda"):
                    out = model(chunk, return_logits=True, refine_budget=budget)
                lg = out["logits"].float()
                tgt = chunk.clamp(0, lg.shape[-1] - 1)
                nll = torch.nn.functional.cross_entropy(
                    lg.reshape(-1, lg.shape[-1]), tgt.reshape(-1), reduction="sum")
                tot_nll += nll.item(); tot_bytes += tgt.numel()
                mf += out["mean_factors"].item(); aR += out["active_R"].item(); nb += 1
            model.train()
            return {"val_bpb": tot_nll / tot_bytes / math.log(2),
                    "mean_factors": mf / nb, "active_R": aR / nb}
    else:
        rng = np.random.default_rng(seed)

        def evaluate():
            return eval_recall(model, device, hp["seq_len"], hp["n_pairs"],
                               hp["val_batch_n"], hp["val_batch"], amp_dtype, hp["pack"])

    model.train()
    skipped, t0, last_mf = 0, time.time(), 0.0
    for step in range(hp["max_steps"]):
        for g in opt.param_groups:
            g["lr"] = lr_at(step)
        opt.zero_grad(set_to_none=True)
        loss_val = 0.0
        for _ in range(hp["grad_accum"]):
            rb = int(np.random.choice(refine_budgets)) if refine_budgets else None
            if task == "text":
                try:
                    bd = next(data_iter)
                except StopIteration:
                    data_iter = iter(loader); bd = next(data_iter)
                bytes_in = bd["bytes_in"].to(device, non_blocking=True)
                valid = bd["valid"].to(device, non_blocking=True)
                with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=device == "cuda"):
                    out = model(bytes_in, valid=valid, refine_budget=rb)
                    loss = out["loss"] / hp["grad_accum"]
            else:
                x, apos, aval, _ = make_recall_batch(hp["batch"], hp["seq_len"], hp["n_pairs"], rng, device, hp["pack"])
                with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=device == "cuda"):
                    out = model(x, return_logits=True, refine_budget=rb)
                    lg = out["logits"].float()
                    ans_logits = lg[torch.arange(x.shape[0]), apos]
                    ce = torch.nn.functional.cross_entropy(ans_logits, aval)
                    loss = (ce + cfg.lambda_rate * out["mean_factors"]) / hp["grad_accum"]
            loss.backward()
            loss_val += loss.item()
        last_mf = out["mean_factors"].item()
        gnorm = torch.nn.utils.clip_grad_norm_(model.parameters(), hp["grad_clip"])
        if torch.isfinite(gnorm):
            opt.step()
        else:
            opt.zero_grad(set_to_none=True); skipped += 1
        if step % hp["log_every"] == 0:
            log(f"   step {step}/{hp['max_steps']} loss {loss_val:.4f} mean_factors {last_mf:.3f} lr {lr_at(step):.2e}")
        if hp["eval_every"] and (step + 1) % hp["eval_every"] == 0:
            ev = evaluate()
            qk = "val_bpb" if task == "text" else "recall_acc_overall"
            log(f"   [eval] {point_id} step {step + 1} {qk}={ev[qk]:.4f} mf={ev['mean_factors']:.3f}")

    metrics = evaluate()
    # GLR-1 depth test: test-time budget sweep on the SAME trained refiner model
    # (perfectly param-matched). If BPB falls as budget grows, iteration buys
    # quality -- the core "test-time compute buys depth" claim, on real text.
    val_bpb_by_budget = None
    if refine_budgets and task == "text":
        val_bpb_by_budget = {str(b): round(evaluate(b)["val_bpb"], 5)
                             for b in sorted(set(refine_budgets))}
        log(f"   BUDGET_SWEEP {point_id} seed{seed} :: {json.dumps(val_bpb_by_budget)}")
    result = {
        "point_id": point_id, "task": task, "rate_mode": rate_mode,
        "fixed_r": cfg.fixed_r, "lambda_rate": cfg.lambda_rate, "seed": seed,
        "params": nparams, "identity": cfg.identity_hash(),
        "use_refiner": bool(getattr(cfg, "use_refiner", False)),
        "refine_budgets": list(getattr(cfg, "refine_budgets", ())),
        "val_bpb_by_budget": val_bpb_by_budget,
        "use_operators": bool(getattr(cfg, "use_operators", False)),
        "n_operators": int(getattr(cfg, "n_operators", 0)),
        "n_active": int(getattr(cfg, "n_active", 0)),
        "operator_mode": getattr(cfg, "operator_mode", "learned"),
        "active_op_flops": active_flops, "stored_op_params": stored_op,
        "max_steps": hp["max_steps"], "batch": hp["batch"], "grad_accum": hp["grad_accum"],
        "seq_len": hp["seq_len"], "tokens": hp["max_steps"] * hp["batch"] * hp["grad_accum"] * hp["seq_len"],
        "skipped": skipped, "wall_s": round(time.time() - t0, 1), **metrics,
    }
    log(f"   RESULT {json.dumps(result)}")
    del model, opt
    gc.collect()
    if device == "cuda":
        torch.cuda.empty_cache()
    return result


# ----------------------------------------------------------------------------
# aggregate frontier points -> verdict (pure)
# ----------------------------------------------------------------------------
_T95 = {1: 12.706, 2: 4.303, 3: 3.182, 4: 2.776, 5: 2.571, 6: 2.447,
        7: 2.365, 8: 2.306, 9: 2.262, 10: 2.228}


def _mean_ci(xs):
    n = len(xs)
    m = sum(xs) / n
    if n < 2:
        return m, float("nan")
    var = sum((x - m) ** 2 for x in xs) / (n - 1)
    t = _T95.get(n - 1, 1.96)
    return m, t * math.sqrt(var) / math.sqrt(n)


def build_scorecard(pts, task):
    qkey = "val_bpb" if task == "text" else "recall_acc_overall"
    pts = [p for p in pts if p.get("task") == task]
    groups = {}
    for p in pts:
        groups.setdefault(p["point_id"], []).append(p)
    rows = []
    for pid, ps in groups.items():
        q_m, q_ci = _mean_ci([p[qkey] for p in ps])
        c_m, c_ci = _mean_ci([p["mean_factors"] for p in ps])
        rows.append({"point_id": pid, "rate_mode": ps[0]["rate_mode"],
                     "cost_mean_factors": c_m, "cost_ci": c_ci,
                     "quality": q_m, "quality_ci": q_ci, "n_seeds": len(ps),
                     "active_R": sum(p["active_R"] for p in ps) / len(ps)})
    rows.sort(key=lambda r: r["cost_mean_factors"])
    fixed = [r for r in rows if r["rate_mode"] == "fixed"]
    adaptive = [r for r in rows if r["rate_mode"] == "adaptive"]

    def fixed_quality_at(cost):
        if not fixed:
            return None
        below = [r for r in fixed if r["cost_mean_factors"] <= cost]
        above = [r for r in fixed if r["cost_mean_factors"] >= cost]
        if not below:
            return fixed[0]["quality"], fixed[0]["quality_ci"]
        if not above:
            return fixed[-1]["quality"], fixed[-1]["quality_ci"]
        lo = max(below, key=lambda r: r["cost_mean_factors"])
        hi = min(above, key=lambda r: r["cost_mean_factors"])
        if hi["cost_mean_factors"] == lo["cost_mean_factors"]:
            return lo["quality"], lo["quality_ci"]
        w = (cost - lo["cost_mean_factors"]) / (hi["cost_mean_factors"] - lo["cost_mean_factors"])
        q = lo["quality"] + w * (hi["quality"] - lo["quality"])
        return q, max(lo["quality_ci"], hi["quality_ci"])

    wins = []
    for a in adaptive:
        fq = fixed_quality_at(a["cost_mean_factors"])
        if fq is None:
            continue
        fqm, fqci = fq
        if task == "text":  # lower bpb better
            strictly = (a["quality"] + (a["quality_ci"] or 0)) < (fqm - (fqci or 0))
        else:               # higher recall better
            strictly = (a["quality"] - (a["quality_ci"] or 0)) > (fqm + (fqci or 0))
        wins.append({"adaptive_point": a["point_id"], "cost": a["cost_mean_factors"],
                     "adaptive_quality": a["quality"], "fixed_quality_at_cost": fqm,
                     "strict_win": bool(strictly)})

    passed = any(w["strict_win"] for w in wins)
    return {
        "task": task, "quality_metric": qkey, "n_points": len(rows),
        "fixed_frontier": fixed, "adaptive_frontier": adaptive,
        "matched_cost_comparisons": wins, "KG04_pass": passed,
        "verdict": ("PASS: adaptive beats fixed at matched cost (disjoint CIs)" if passed
                    else "NOT PROVEN: adaptive frontier does not strictly beat fixed within CIs"),
    }


# ----------------------------------------------------------------------------
# Hub push helpers
# ----------------------------------------------------------------------------
def _push(local_path, path_in_repo, msg):
    from huggingface_hub import HfApi
    try:
        HfApi().upload_file(
            path_or_fileobj=str(local_path), path_in_repo=path_in_repo,
            repo_id=env("CODE_REPO", "farguney/trident"), repo_type="model",
            commit_message=msg,
        )
        log(f"pushed {path_in_repo}")
    except Exception as e:  # noqa
        log(f"push failed for {path_in_repo} ({e})")


# ----------------------------------------------------------------------------
# entry points
# ----------------------------------------------------------------------------
def device_amp():
    device = "cuda" if torch.cuda.is_available() else "cpu"
    if device == "cuda":
        log(f"gpu {torch.cuda.get_device_name(0)}  torch {torch.__version__}")
    else:
        log("WARNING: no CUDA device; running on CPU")
    return device, (torch.bfloat16 if device == "cuda" else torch.float32)


def run_grid():
    device, amp = device_amp()
    hp = read_hp()
    task = env("TASK", "text")
    fixed_rs = [int(x) for x in env("FIXED_RS", "1,2,4").split(",") if x != ""]
    lam_env = os.environ.get("LAMBDAS", "0.02,0.1,0.5")
    lambdas = ([] if lam_env.strip().lower() in ("", "none", "skip")
               else [float(x) for x in lam_env.split(",") if x != ""])
    seeds = [int(x) for x in env("SEEDS", "0,1,2").split(",") if x != ""]
    run = env("RUN", "vr")
    results_dir = env("RESULTS_DIR", "ablation/variable_rate")
    log(f"GRID task={task} fixed_rs={fixed_rs} lambdas={lambdas} seeds={seeds} "
        f"points={(len(fixed_rs)+len(lambdas))*len(seeds)} steps/pt={hp['max_steps']}")

    outdir = Path("/tmp/vr_grid"); outdir.mkdir(exist_ok=True)
    results = []

    def flush(tag):
        # push after every point so a timeout/preemption never loses completed
        # (expensive) points; MODE=aggregate can rebuild the scorecard any time.
        (outdir / "results.jsonl").write_text("\n".join(json.dumps(r) for r in results))
        _push(outdir / "results.jsonl", f"{results_dir}/{run}/results_{task}.jsonl",
              f"vr grid results ({task}, {len(results)} pts, {tag})")

    plan = [("fixed", r, 0.0) for r in fixed_rs] + [("adaptive", None, lam) for lam in lambdas]
    for (mode, r, lam) in plan:
        for s in seeds:
            results.append(train_one(task, mode, r, lam, s, hp, device, amp))
            flush(f"{mode}:{r if mode == 'fixed' else lam}:s{s}")

    score = build_scorecard(results, task)
    (outdir / "scorecard.json").write_text(json.dumps(score, indent=2))
    _push(outdir / "scorecard.json", f"{results_dir}/{run}/scorecard_{task}.json",
          f"vr grid scorecard ({task})")
    log(f"VERDICT [{task}] KG04_pass={score['KG04_pass']} :: {score['verdict']}")
    log(f"SCORECARD {json.dumps(score, indent=2)}")


def build_operator_scorecard(pts):
    """KG-05 (operator value) + KG-08 (stored-capacity slope) on text BPB.

    All points share one fixed-R1 backbone and identical active payload FLOPs;
    only the operator pool differs. KG-08 asks whether BPB *falls* as the stored
    pool N grows at constant active compute. KG-05 asks whether the learned pool
    beats the no-operator baseline AND the anti-fooling controls (random-frozen
    payloads, matched-active dense) with disjoint 95% CIs. Both can fail.
    """
    pts = [p for p in pts if p.get("task") == "text"]
    groups = {}
    for p in pts:
        groups.setdefault(p["point_id"], []).append(p)
    rows = {}
    for pid, ps in groups.items():
        bpb_m, bpb_ci = _mean_ci([p["val_bpb"] for p in ps])
        rows[pid] = {
            "point_id": pid, "n_seeds": len(ps), "val_bpb": bpb_m, "val_bpb_ci": bpb_ci,
            "n_operators": ps[0].get("n_operators", 0),
            "operator_mode": ps[0].get("operator_mode", "-"),
            "use_operators": ps[0].get("use_operators", False),
            "active_op_flops": ps[0].get("active_op_flops", 0),
            "stored_op_params": ps[0].get("stored_op_params", 0),
            "params": ps[0].get("params", 0),
        }

    def lb(r):  # upper edge of the better (lower) direction
        return r["val_bpb"] - (r["val_bpb_ci"] or 0.0)

    def ub(r):
        return r["val_bpb"] + (r["val_bpb_ci"] or 0.0)

    learned = sorted([r for r in rows.values()
                      if r["use_operators"] and r["operator_mode"] == "learned"],
                     key=lambda r: r["n_operators"])
    # KG-08: BPB strictly lower at the largest pool vs the smallest, CIs disjoint.
    slope_ok, slope_desc = False, "insufficient learned points for a slope"
    if len(learned) >= 2:
        lo, hi = learned[0], learned[-1]
        gain = lo["val_bpb"] - hi["val_bpb"]
        disjoint = ub(hi) < lb(lo)
        slope_ok = gain > 0 and disjoint
        slope_desc = (f"N {lo['n_operators']}->{hi['n_operators']}: BPB "
                      f"{lo['val_bpb']:.4f}->{hi['val_bpb']:.4f} (gain {gain:+.4f}, "
                      f"CIs {'disjoint' if disjoint else 'overlap'})")

    best = min(learned, key=lambda r: r["val_bpb"]) if learned else None
    comparisons, kg05_ok = {}, best is not None
    ctrl_map = {"baseline": [r for r in rows.values() if not r["use_operators"]],
                "random_frozen": [r for r in rows.values() if r["operator_mode"] == "random_frozen"],
                "dense": [r for r in rows.values() if r["operator_mode"] == "dense"]}
    for name, cs in ctrl_map.items():
        if best and cs:
            c = cs[0]
            win = ub(best) < lb(c)  # best learned strictly better (lower BPB)
            comparisons[name] = {"ctrl_bpb": c["val_bpb"], "ctrl_ci": c["val_bpb_ci"],
                                 "best_bpb": best["val_bpb"], "best_beats_ctrl": bool(win)}
            kg05_ok = kg05_ok and win
        elif name == "baseline":
            kg05_ok = False  # no baseline => cannot assert value
    return {
        "task": "text",
        "rows": sorted(rows.values(), key=lambda r: (not r["use_operators"], r["n_operators"])),
        "best_learned": best["point_id"] if best else None,
        "KG08_slope_ok": slope_ok, "KG08_verdict": slope_desc,
        "KG05_comparisons": comparisons, "KG05_pass": bool(kg05_ok),
        "KG05_verdict": ("PASS: learned operators beat the no-operator baseline and every "
                         "anti-fooling control at matched active compute (disjoint 95% CIs)"
                         if kg05_ok else
                         "NOT PROVEN: learned operators do not strictly beat all controls within CIs"),
    }


def run_operators():
    device, amp = device_amp()
    hp = read_hp()
    task = "text"  # operator value is measured on real-text held-out BPB
    op_ns = [int(x) for x in env("OP_NS", "512,2048,8192").split(",") if x != ""]
    seeds = [int(x) for x in env("SEEDS", "0,1").split(",") if x != ""]
    n_active = env_i("N_ACTIVE", 4)
    d_operator = env_i("D_OPERATOR", 0)
    n_op_layers = env_i("N_OPERATOR_LAYERS", 2)
    include_baseline = env("BASELINE", "1") not in ("0", "false", "")
    include_controls = env("CONTROLS", "1") not in ("0", "false", "")
    run = env("RUN", "ops")
    results_dir = env("RESULTS_DIR", "ablation/operators")

    def op_over(**kw):
        base = dict(use_operators=True, n_active=n_active, n_operator_layers=n_op_layers)
        if d_operator > 0:
            base["d_operator"] = d_operator
        base.update(kw)
        return base

    plan = []
    if include_baseline:
        plan.append(("baseline", {"use_operators": False}))
    for N in op_ns:
        plan.append((f"learned_N{N}", op_over(n_operators=N, operator_mode="learned")))
    if include_controls and op_ns:
        maxN = max(op_ns)
        plan.append((f"randfrozen_N{maxN}",
                     op_over(n_operators=maxN, operator_mode="random_frozen")))
        plan.append(("dense_matched", op_over(n_operators=maxN, operator_mode="dense")))

    log(f"OPERATORS points={len(plan)} seeds={seeds} n_active={n_active} "
        f"n_op_layers={n_op_layers} op_ns={op_ns} steps/pt={hp['max_steps']}")

    outdir = Path("/tmp/op_grid"); outdir.mkdir(exist_ok=True)
    results = []

    def flush(tag):
        (outdir / "results.jsonl").write_text("\n".join(json.dumps(r) for r in results))
        _push(outdir / "results.jsonl", f"{results_dir}/{run}/results.jsonl",
              f"operator results ({len(results)} pts, {tag})")

    for (pid, over) in plan:
        for s in seeds:
            results.append(train_one(task, "fixed", 1, 0.0, s, hp, device, amp,
                                     extra_over=over, point_id=pid))
            flush(f"{pid}:s{s}")

    score = build_operator_scorecard(results)
    (outdir / "scorecard.json").write_text(json.dumps(score, indent=2))
    _push(outdir / "scorecard.json", f"{results_dir}/{run}/scorecard.json",
          "operator scorecard (KG-05/KG-08)")
    log(f"VERDICT KG05_pass={score['KG05_pass']} :: {score['KG05_verdict']}")
    log(f"VERDICT KG08_slope_ok={score['KG08_slope_ok']} :: {score['KG08_verdict']}")
    log(f"SCORECARD {json.dumps(score, indent=2)}")


def build_refiner_scorecard(pts):
    """GLR-1 iterative-refiner value on real-text held-out BPB.

    Two falsifiable questions, both able to fail:
      * refiner_beats_baseline -- Trident+refiner (eval at max budget) has strictly
        lower BPB than the no-refiner baseline at matched steps/data, disjoint 95%
        CIs. (Params differ: the refiner adds one weight-shared block; reported.)
      * budget_monotone -- on the SAME trained refiner model, test-time BPB falls as
        the refinement budget grows (perfectly param-matched depth test). This is
        the core GLR "test-time compute buys depth" claim on real bytes.
    """
    pts = [p for p in pts if p.get("task") == "text"]
    groups = {}
    for p in pts:
        groups.setdefault(p["point_id"], []).append(p)
    rows = {}
    for pid, ps in groups.items():
        m, ci = _mean_ci([p["val_bpb"] for p in ps])
        rows[pid] = {"point_id": pid, "n_seeds": len(ps), "val_bpb": m, "val_bpb_ci": ci,
                     "params": ps[0].get("params", 0),
                     "use_refiner": ps[0].get("use_refiner", False),
                     "refine_budgets": ps[0].get("refine_budgets", []),
                     "val_bpb_by_budget": ps[0].get("val_bpb_by_budget")}

    def lb(r):
        return r["val_bpb"] - (r["val_bpb_ci"] or 0.0)

    def ub(r):
        return r["val_bpb"] + (r["val_bpb_ci"] or 0.0)

    base = next((r for r in rows.values() if not r["use_refiner"]), None)
    ref = next((r for r in rows.values() if r["use_refiner"]), None)
    beats, verdict = False, "no baseline/refiner pair to compare"
    if base and ref:
        beats = ub(ref) < lb(base)
        disj = ub(ref) < lb(base) or ub(base) < lb(ref)
        verdict = (f"{'PASS' if beats else 'NOT PROVEN'}: refiner {ref['val_bpb']:.4f} vs "
                   f"baseline {base['val_bpb']:.4f} (delta {ref['val_bpb']-base['val_bpb']:+.4f}, "
                   f"CIs {'disjoint' if disj else 'overlap'}); params "
                   f"{ref['params']/1e6:.2f}M vs {base['params']/1e6:.2f}M")

    mono, bdesc = False, "no budget sweep recorded"
    if ref and ref.get("val_bpb_by_budget"):
        sweep = ref["val_bpb_by_budget"]
        bs = sorted(sweep, key=lambda k: int(k))
        vals = [sweep[b] for b in bs]
        mono = all(vals[i] >= vals[i + 1] - 1e-4 for i in range(len(vals) - 1)) and (vals[0] - vals[-1] > 0)
        bdesc = (" ".join(f"b{b}={sweep[b]:.4f}" for b in bs)
                 + f" (gain b{bs[0]}->b{bs[-1]}: {vals[0]-vals[-1]:+.4f})")

    return {
        "task": "text",
        "rows": sorted(rows.values(), key=lambda r: r["use_refiner"]),
        "refiner_beats_baseline": bool(beats), "verdict": verdict,
        "budget_monotone": bool(mono), "budget_desc": bdesc,
    }


def run_refiner():
    device, amp = device_amp()
    hp = read_hp()
    task = env("TASK", "text")
    seeds = [int(x) for x in env("SEEDS", "0,1").split(",") if x != ""]
    budgets = tuple(int(x) for x in env("REFINE_BUDGETS", "1,2,4,8").split(",") if x != "")
    ffn_mult = env_i("REFINER_FFN_MULT", 4)
    dsw = env_f("DEEP_SUP_W", 0.1)
    hkw = env_f("HALT_KL_W", 0.01)
    hprior = env_f("HALT_PRIOR", 0.2)
    include_baseline = env("BASELINE", "1") not in ("0", "false", "")
    run = env("RUN", "refiner")
    results_dir = env("RESULTS_DIR", "ablation/refiner")

    ref_over = dict(use_refiner=True, refine_budgets=budgets, refiner_ffn_mult=ffn_mult,
                    deep_supervision_weight=dsw, halt_kl_weight=hkw, halt_prior=hprior)
    plan = []
    if include_baseline:
        plan.append(("baseline", {"use_refiner": False}))
    plan.append((f"refiner_b{max(budgets)}", ref_over))

    log(f"REFINER points={len(plan)} seeds={seeds} budgets={budgets} ffn_mult={ffn_mult} "
        f"dsw={dsw} hkw={hkw} steps/pt={hp['max_steps']}")
    outdir = Path("/tmp/ref_grid"); outdir.mkdir(exist_ok=True)
    results = []

    def flush(tag):
        (outdir / "results.jsonl").write_text("\n".join(json.dumps(r) for r in results))
        _push(outdir / "results.jsonl", f"{results_dir}/{run}/results.jsonl",
              f"refiner results ({len(results)} pts, {tag})")

    for (pid, over) in plan:
        for s in seeds:
            results.append(train_one(task, "fixed", 1, 0.0, s, hp, device, amp,
                                     extra_over=over, point_id=pid))
            flush(f"{pid}:s{s}")

    score = build_refiner_scorecard(results)
    (outdir / "scorecard.json").write_text(json.dumps(score, indent=2))
    _push(outdir / "scorecard.json", f"{results_dir}/{run}/scorecard.json",
          "refiner scorecard (GLR-1 port)")
    log(f"VERDICT refiner_beats_baseline={score['refiner_beats_baseline']} :: {score['verdict']}")
    log(f"VERDICT budget_monotone={score['budget_monotone']} :: {score['budget_desc']}")
    log(f"SCORECARD {json.dumps(score, indent=2)}")


def train_point():
    device, amp = device_amp()
    hp = read_hp()
    task = env("TASK", "text")
    rate_mode = env("RATE_MODE", "fixed")
    fixed_r = env_i("FIXED_R", 1)
    lambda_rate = env_f("LAMBDA_RATE", 0.0)
    seed = env_i("SEED", 0)
    res = train_one(task, rate_mode, fixed_r if rate_mode == "fixed" else None,
                    lambda_rate, seed, hp, device, amp)
    run = env("RUN", "vr")
    results_dir = env("RESULTS_DIR", "ablation/variable_rate")
    fname = f"{task}__{res['point_id']}__seed{seed}.json"
    outdir = Path("/tmp/vr_result"); outdir.mkdir(exist_ok=True)
    (outdir / fname).write_text(json.dumps(res, indent=2))
    _push(outdir / fname, f"{results_dir}/{run}/{fname}", f"vr point {res['point_id']} seed{seed}")


def aggregate():
    from huggingface_hub import HfApi, hf_hub_download
    api = HfApi()
    repo = env("CODE_REPO", "farguney/trident")
    run = env("RUN", "vr")
    results_dir = env("RESULTS_DIR", "ablation/variable_rate")
    task = env("TASK", "text")
    prefix = f"{results_dir}/{run}/"
    files = [f for f in api.list_repo_files(repo, repo_type="model")
             if f.startswith(prefix) and f.endswith(".json") and "scorecard" not in f]
    pts = []
    for f in files:
        pts.append(json.loads(Path(hf_hub_download(repo, f, repo_type="model")).read_text()))
    # also fold any results_*.jsonl
    for f in api.list_repo_files(repo, repo_type="model"):
        if f.startswith(prefix) and f.endswith(".jsonl"):
            for line in Path(hf_hub_download(repo, f, repo_type="model")).read_text().splitlines():
                if line.strip():
                    pts.append(json.loads(line))
    log(f"aggregating {len(pts)} points for task={task}")
    score = build_scorecard(pts, task)
    outdir = Path("/tmp/vr_score"); outdir.mkdir(exist_ok=True)
    (outdir / "scorecard.json").write_text(json.dumps(score, indent=2))
    _push(outdir / "scorecard.json", f"{prefix}scorecard_{task}.json", f"vr scorecard ({task})")
    log(f"SCORECARD {json.dumps(score, indent=2)}")


def main():
    import_trident()
    mode = env("MODE", "grid")
    if mode == "grid":
        run_grid()
    elif mode == "operators":
        run_operators()
    elif mode == "refiner":
        run_refiner()
    elif mode == "train":
        train_point()
    elif mode == "aggregate":
        aggregate()
    else:
        raise ValueError(f"MODE must be grid|operators|refiner|train|aggregate, got {mode}")


if __name__ == "__main__":
    main()