QC67_cosmo / benchmarks /causality_probe.py
phera-ra's picture
Omega was reading the future; fixed, re-measured, result survives
92b92e6 verified
Raw
History Blame Contribute Delete
6.25 kB
#!/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())