File size: 6,254 Bytes
92b92e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Does dyn12 leak the future into the past?



Omega is defined in section 3.4 as attention RECEIVED -- summed over the QUERY axis:



    omega = a.mean(1).sum(-2)        # [B, T], indexed by KEY



Under a causal mask a_ij is nonzero only for j <= i, so



    Omega_j = sum over i >= j of a_ij



which includes queries from tokens AFTER j. That Omega updates state_j, state_j feeds

the next layer's Hebbian kernel, the kernel shapes attention, and attention shapes the

logits at position j. If that chain is live, position j can see its own future and the

held-out loss is measuring a leak rather than a mechanism.



THE TEST, which does not care about any of that reasoning:

  Run the model on a sequence. Change ONLY the LAST token. If the logits at an EARLY

  position move, information flowed backwards. A correct causal LM cannot do this --

  its prediction at position j is a function of tokens 0..j alone.



A standard transformer is included as the control, so a positive result cannot be

blamed on the harness.



  python tools/causality_probe.py

"""
from __future__ import annotations

import os
import sys
from pathlib import Path

import torch

_root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_root / 'architecture'))
sys.path.insert(0, str(_root))
sys.stdout.reconfigure(encoding="utf-8", errors="replace")

import cosmos_state_ladder as L  # noqa: E402

DEV = "cpu"
torch.manual_seed(0)


def logits_for(model, ids):
    model.eval()
    with torch.no_grad():
        out = model(ids)
        return out[0] if isinstance(out, tuple) else out


def probe(rung: str, T: int = 24, trials: int = 3, gate: float | None = None,

          seed: int | None = None) -> dict:
    """gate=None leaves the gate at init. PHOS's trained layer-0 gate is 0.562, and the

    leak is proportional to it -- Omega reaches the logits only through g*H, so measuring

    at initialisation (g near zero) understates a trained model by orders of magnitude.



    seed=None draws a FRESH RANDOM seed, and that is the default on purpose. A causality

    result that only holds for one hand-picked seed is worth nothing -- the reader cannot

    tell a real property from a lucky draw. Random weights and random sequences every run

    mean each person who executes this gets independent evidence, and a leak that only

    appears sometimes still gets caught. Pass --seed N when you need to reproduce a

    specific run exactly."""
    if seed is None:
        seed = int.from_bytes(os.urandom(4), "little")
    torch.manual_seed(seed)
    V = 96
    model = L.Ladder(vocab=V, rung=rung, ffn="harmonic").to(DEV)
    if gate is not None:
        import math as _m
        raw = _m.log(gate / (1.0 - gate))          # invert sigmoid
        with torch.no_grad():
            for b in model.blocks:
                if hasattr(b.attn, "gate"):
                    b.attn.gate.fill_(raw)
    worst = 0.0
    for t in range(trials):
        g = torch.Generator().manual_seed(seed + 1 + t)
        ids = torch.randint(0, V, (1, T), generator=g)
        base = logits_for(model, ids)

        alt = ids.clone()
        # change ONLY the final token
        alt[0, -1] = (alt[0, -1] + 1 + t) % V
        moved = logits_for(model, alt)

        # compare every position EXCEPT the last: none may change
        early = (base[:, :-1, :] - moved[:, :-1, :]).abs().max().item()
        worst = max(worst, early)
    return {"rung": rung, "max_early_logit_change": worst}


def main() -> int:
    seed = None
    if "--seed" in sys.argv:
        seed = int(sys.argv[sys.argv.index("--seed") + 1])
    run_seed = seed if seed is not None else int.from_bytes(os.urandom(4), "little")

    print("  Changing ONLY the last token. Logits at earlier positions must not move.")
    print("  The control rungs give EXACTLY 0.0, so any nonzero value is a real")
    print("  dependency, not float noise -- identical ops on identical inputs.")
    print(f"  Omega mode: {'CAUSAL (per-query entropy)' if L.CAUSAL_OMEGA else 'ORIGINAL query-sum'}")
    print(f"  seed: {run_seed}"
          + ("  [fixed]" if seed is not None else "  [random -- your run is independent evidence]"))
    # Derive the path from argv rather than hardcoding it: this file ships inside the
    # kit as benchmarks/causality_probe.py, and telling a reader to run a path that does
    # not exist on their disk is how a reproducible result stops being reproducible.
    print(f"  reproduce this exact run:  python {os.path.relpath(sys.argv[0])} --seed {run_seed}\n")

    cases = [("none", None), ("static54", None), ("dyn12", None),
             ("dyn12", 0.562), ("tri", 0.562)]
    rows = []
    for rung, gate in cases:
        try:
            r = probe(rung, gate=gate, seed=run_seed)
            r["gate"] = gate
            rows.append(r)
        except Exception as e:
            rows.append({"rung": rung, "gate": gate, "error": f"{type(e).__name__}: {e}"})

    for r in rows:
        label = r["rung"] + (f" @g={r['gate']}" if r.get("gate") else " @init")
        if "error" in r:
            print(f"  {label:<20} ERROR {r['error']}")
            continue
        d = r["max_early_logit_change"]
        verdict = "causal" if d == 0.0 else "LEAKS THE FUTURE"
        print(f"  {label:<20} max early-position change: {d:.3e}   {verdict}")

    ok = [r for r in rows if "error" not in r]
    base = next((r for r in ok if r["rung"] == "none"), None)
    if base and base["max_early_logit_change"] != 0.0:
        print("\n  The BASELINE moved too -- the harness is wrong, not the architecture.")
        return 2
    leaky = [r for r in ok if r["max_early_logit_change"] > 0.0]
    print()
    if leaky:
        print("  Omega is summed over QUERIES, so Omega_j counts attention from tokens")
        print("  AFTER j. Position j therefore sees its own future. Held-out loss won")
        print("  this way is not comparable to a rung that cannot do it.")
        return 1
    print("  No rung leaks. Omega's query-sum stays inside the causal boundary.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())