Upload CEDL research checkpoint
Browse files- CEDL.py +0 -0
- HF_UPLOAD_CHECKLIST.md +56 -0
- MANIFEST.json +34 -0
- README.md +122 -0
- cedl_config.json +98 -0
- config.json +14 -0
- data_v4c_pairs.py +615 -0
- examples/load_checkpoint.py +91 -0
- probes/probe_memory_causality.py +275 -0
- probes/probe_memory_diagnosis.py +310 -0
- probes/probe_memory_source_readout.py +505 -0
- pytorch_model.bin +3 -0
- requirements.txt +7 -0
CEDL.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
HF_UPLOAD_CHECKLIST.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Upload Checklist
|
| 2 |
+
|
| 3 |
+
Repository:
|
| 4 |
+
|
| 5 |
+
- `Jasonjiao2023/CEDL`
|
| 6 |
+
|
| 7 |
+
The current Hugging Face docs recommend the modern `hf` CLI. For agent-aware local workflows, the optional project setup is:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
hf skills add
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
For normal upload work, authenticate first:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
hf auth login
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
Before upload, `hf_model/` should contain:
|
| 20 |
+
|
| 21 |
+
- `README.md`
|
| 22 |
+
- `config.json`
|
| 23 |
+
- `MANIFEST.json`
|
| 24 |
+
- `requirements.txt`
|
| 25 |
+
- `CEDL.py`
|
| 26 |
+
- `data_v4c_pairs.py`
|
| 27 |
+
- `examples/load_checkpoint.py`
|
| 28 |
+
- `probes/`
|
| 29 |
+
- `pytorch_model.bin`
|
| 30 |
+
- `cedl_config.json`
|
| 31 |
+
|
| 32 |
+
Copy the frozen Colab artifacts into public Hugging Face names:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
cp "$CEDL_FROZEN_CHECKPOINT" pytorch_model.bin
|
| 36 |
+
cp "$CEDL_FROZEN_CONFIG" cedl_config.json
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Verify hashes:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
sha256sum pytorch_model.bin cedl_config.json
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
Expected:
|
| 46 |
+
|
| 47 |
+
```text
|
| 48 |
+
e15a3d6dd38a1f85e2a9c4de409ac3cbd7dece4cf202fca05872ebea8180bfbf pytorch_model.bin
|
| 49 |
+
52ffddf6d95fa20fe4be953d463e5e388c2b161b49096931e10fe09a160f521f cedl_config.json
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
One-line upload after the checkpoint and sidecar are present:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
hf upload Jasonjiao2023/CEDL CEDL-release/hf_model . --repo-type model
|
| 56 |
+
```
|
MANIFEST.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"label": "CEDL",
|
| 3 |
+
"frozen_at_utc": "2026-06-26T12:42:59.251253+00:00",
|
| 4 |
+
"checkpoint": "pytorch_model.bin",
|
| 5 |
+
"config": "cedl_config.json",
|
| 6 |
+
"sha256": {
|
| 7 |
+
"checkpoint": "e15a3d6dd38a1f85e2a9c4de409ac3cbd7dece4cf202fca05872ebea8180bfbf",
|
| 8 |
+
"config": "948eebfc64e66a3622157be781929f1e719b4c8e3f59c4d7dbd116cc2ff1a800"
|
| 9 |
+
},
|
| 10 |
+
"model_interpretation": {
|
| 11 |
+
"architecture": "hippocampal-inspired contextual memory language model",
|
| 12 |
+
"memory_readout": "contextual direct memory readout",
|
| 13 |
+
"status": "research checkpoint",
|
| 14 |
+
"claim": "CEDL contextual memory readout is causally useful for current-vs-stale memory updating; not yet a general downstream SOTA model."
|
| 15 |
+
},
|
| 16 |
+
"mechanism_evidence": {
|
| 17 |
+
"current_vs_stale_causality": [
|
| 18 |
+
{"seed": 1, "N": 158, "active_top1": 0.791, "memory_off_top1": 0.0, "delta_pp": 79.1, "delta_margin": 8.266, "t": 20.756, "cohen_d": 1.656},
|
| 19 |
+
{"seed": 2, "N": 164, "active_top1": 0.732, "memory_off_top1": 0.0, "delta_pp": 73.2, "delta_margin": 7.702, "t": 18.912, "cohen_d": 1.481},
|
| 20 |
+
{"seed": 3, "N": 162, "active_top1": 0.728, "memory_off_top1": 0.0, "delta_pp": 72.8, "delta_margin": 7.756, "t": 18.507, "cohen_d": 1.459}
|
| 21 |
+
],
|
| 22 |
+
"memory_readout_diagnosis": [
|
| 23 |
+
{"seed": 1, "N": 158, "trunk_top1_cur": 0.0, "memory_top1_cur": 0.797, "active_top1_cur": 0.791, "memory_top1_stale": 0.006, "current_rank_median": 1},
|
| 24 |
+
{"seed": 2, "N": 164, "trunk_top1_cur": 0.0, "memory_top1_cur": 0.738, "active_top1_cur": 0.732, "memory_top1_stale": 0.006, "current_rank_median": 1},
|
| 25 |
+
{"seed": 3, "N": 162, "trunk_top1_cur": 0.0, "memory_top1_cur": 0.735, "active_top1_cur": 0.728, "memory_top1_stale": 0.012, "current_rank_median": 1}
|
| 26 |
+
]
|
| 27 |
+
},
|
| 28 |
+
"general_eval": {
|
| 29 |
+
"wikitext103_test_ppl": 30.11,
|
| 30 |
+
"lambada": 0.159,
|
| 31 |
+
"hellaswag": 0.271,
|
| 32 |
+
"arc_easy": 0.281
|
| 33 |
+
}
|
| 34 |
+
}
|
README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
tags:
|
| 5 |
+
- pytorch
|
| 6 |
+
- language-modeling
|
| 7 |
+
- memory
|
| 8 |
+
- hippocampal-inspired
|
| 9 |
+
- research-artifact
|
| 10 |
+
datasets:
|
| 11 |
+
- Salesforce/wikitext
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# CEDL
|
| 15 |
+
|
| 16 |
+
CEDL is a hippocampal-inspired language model with contextual memory readout. It separates context encoding, pattern separation, memory retrieval, and linkage/feedback into distinct computational stages, then uses a contextual memory channel to resolve current-vs-stale information.
|
| 17 |
+
|
| 18 |
+
This repository is a research checkpoint. It is intended for mechanism inspection and reproducible probes, not for production deployment or broad downstream SOTA claims.
|
| 19 |
+
|
| 20 |
+
## Intended Use
|
| 21 |
+
|
| 22 |
+
Use this repository to:
|
| 23 |
+
|
| 24 |
+
- load the CEDL PyTorch checkpoint;
|
| 25 |
+
- inspect the CEDL architecture implementation;
|
| 26 |
+
- reproduce current-vs-stale memory-update probes;
|
| 27 |
+
- study contextual memory readout and memory-off ablations.
|
| 28 |
+
|
| 29 |
+
Do not use this repository to claim:
|
| 30 |
+
|
| 31 |
+
- broad downstream SOTA;
|
| 32 |
+
- production-ready generation quality;
|
| 33 |
+
- biological fidelity beyond computational analogy.
|
| 34 |
+
|
| 35 |
+
## Files
|
| 36 |
+
|
| 37 |
+
Expected checkpoint files:
|
| 38 |
+
|
| 39 |
+
- `pytorch_model.bin`
|
| 40 |
+
- `cedl_config.json`
|
| 41 |
+
- `MANIFEST.json`
|
| 42 |
+
|
| 43 |
+
Code and probes:
|
| 44 |
+
|
| 45 |
+
- `CEDL.py`
|
| 46 |
+
- `data_v4c_pairs.py`
|
| 47 |
+
- `probes/probe_memory_causality.py`
|
| 48 |
+
- `probes/probe_memory_diagnosis.py`
|
| 49 |
+
- `probes/probe_memory_source_readout.py`
|
| 50 |
+
|
| 51 |
+
## Evidence Summary
|
| 52 |
+
|
| 53 |
+
Current-vs-stale causality:
|
| 54 |
+
|
| 55 |
+
| Seed | N | CEDL active top-1 | Memory-off top-1 | Delta acc. | Delta margin | Cohen's d |
|
| 56 |
+
|---:|---:|---:|---:|---:|---:|---:|
|
| 57 |
+
| 1 | 158 | 79.1% | 0.0% | +79.1pp | +8.266 | 1.656 |
|
| 58 |
+
| 2 | 164 | 73.2% | 0.0% | +73.2pp | +7.702 | 1.481 |
|
| 59 |
+
| 3 | 162 | 72.8% | 0.0% | +72.8pp | +7.756 | 1.459 |
|
| 60 |
+
|
| 61 |
+
Memory-readout diagnosis:
|
| 62 |
+
|
| 63 |
+
| Seed | N | Trunk current top-1 | Memory current top-1 | Memory stale top-1 | Current median rank |
|
| 64 |
+
|---:|---:|---:|---:|---:|---:|
|
| 65 |
+
| 1 | 158 | 0.0% | 79.7% | 0.6% | 1 |
|
| 66 |
+
| 2 | 164 | 0.0% | 73.8% | 0.6% | 1 |
|
| 67 |
+
| 3 | 162 | 0.0% | 73.5% | 1.2% | 1 |
|
| 68 |
+
|
| 69 |
+
General evaluation:
|
| 70 |
+
|
| 71 |
+
| Model | Params | WikiText-103 PPL | LAMBADA | HellaSwag | ARC-Easy |
|
| 72 |
+
|---|---:|---:|---:|---:|---:|
|
| 73 |
+
| CEDL | 136.3M | 30.11 | 15.9% | 27.1% | 28.1% |
|
| 74 |
+
| Transformer baseline | 103.2M | 45.93 | 3.9% | 26.5% | 28.2% |
|
| 75 |
+
|
| 76 |
+
## About the Author
|
| 77 |
+
|
| 78 |
+
CEDL was developed by Dian Jiao, a multidisciplinary researcher and technology leader with more than 15 years of experience across digital transformation, neuroscience, and technology innovation. His work connects brain-inspired theory with practical AI systems, with a focus on NeuroAI, digital therapeutics, and human-centered model design.
|
| 79 |
+
|
| 80 |
+
His research interests include AI-enhanced digital therapeutics using EEG, adaptive biofeedback, and machine learning for cognitive enhancement and neurological rehabilitation; and biologically inspired AI models that draw from neural dynamics, memory circuits, and brain mechanisms.
|
| 81 |
+
|
| 82 |
+
Selected publications and contributions:
|
| 83 |
+
|
| 84 |
+
- Jiao, D. (2025). Elliptic cortical networks: A mathematically constrained architecture for biologically-inspired intelligence. Neurocomputing, 658, 131802. doi: 10.1016/j.neucom.2025.131802
|
| 85 |
+
- Jiao, D. (2025). Leveraging neurotechnology for neurodivergent education: a narrative review. Learning: Research and Practice, 1-25. doi: 10.1080/23735082.2025.2517052
|
| 86 |
+
- Jiao, D. (2025). AI-enhanced digital therapeutics for cognitive impairment: Integrating mobile applications, virtual reality, and wearable devices. Discover Artificial Intelligence, 5, Article 69. doi: 10.1007/s44163-025-00325-6
|
| 87 |
+
- Jiao, D. (2025). Advancing personalized digital therapeutics: integrating music therapy, brainwave entrainment methods, and AI-driven biofeedback. Frontiers in Digital Health, 7, 1552396. doi: 10.3389/fdgth.2025.1552396
|
| 88 |
+
- Jiao, D. (2025). From hypoxic pockets to daily routines: linking brain oxygenation and cognitive resilience. Frontiers in Aging Neuroscience, 17, 1534198. doi: 10.3389/fnagi.2025.1534198
|
| 89 |
+
- Cognition and Brain Teaching Technology, a book on brain-based teaching and cognitive technology.
|
| 90 |
+
- Chinese translations of How Breakthroughs Happen (2020), Mastering the Dynamics of Innovation (2022), and Neuroscience of You (2025).
|
| 91 |
+
|
| 92 |
+
LinkedIn: https://www.linkedin.com/in/jason-jiao-2972141a4
|
| 93 |
+
|
| 94 |
+
## Load Example
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
pip install -r requirements.txt
|
| 98 |
+
python examples/load_checkpoint.py --checkpoint pytorch_model.bin
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Probe Example
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python probes/probe_memory_causality.py \
|
| 105 |
+
--checkpoint pytorch_model.bin \
|
| 106 |
+
--sidecar cedl_config.json \
|
| 107 |
+
--n-items 200 \
|
| 108 |
+
--seed 1
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## Citation
|
| 112 |
+
|
| 113 |
+
If you use this checkpoint before the manuscript is published, cite it as:
|
| 114 |
+
|
| 115 |
+
```bibtex
|
| 116 |
+
@misc{jiao2026cedl,
|
| 117 |
+
title = {CEDL: A Hippocampal-Inspired Language Model with Contextual Memory Readout},
|
| 118 |
+
author = {Jiao, Dian},
|
| 119 |
+
year = {2026},
|
| 120 |
+
note = {Research checkpoint for contextual memory-readout probes}
|
| 121 |
+
}
|
| 122 |
+
```
|
cedl_config.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "CEDL",
|
| 3 |
+
"model_type": "cedl",
|
| 4 |
+
"architecture": "hippocampal-inspired contextual memory language model",
|
| 5 |
+
"vocab_size": 50257,
|
| 6 |
+
"max_position_embeddings": 1024,
|
| 7 |
+
"parameters": 136254705,
|
| 8 |
+
"checkpoint_file": "pytorch_model.bin",
|
| 9 |
+
"implementation_file": "CEDL.py",
|
| 10 |
+
"memory_readout": {
|
| 11 |
+
"enabled": true,
|
| 12 |
+
"source": "contextual_memory_state",
|
| 13 |
+
"mode": "contextual_direct",
|
| 14 |
+
"lambda_head": true,
|
| 15 |
+
"lambda_head_hidden": 160,
|
| 16 |
+
"lambda_head_bias_init": -7.0,
|
| 17 |
+
"lambda_head_w_init_std": 0.05,
|
| 18 |
+
"selection_objective": "binary_answer_background",
|
| 19 |
+
"background_target": 0.01,
|
| 20 |
+
"sparsity_weight": 0.05,
|
| 21 |
+
"sparsity_target": 0.05,
|
| 22 |
+
"memory_ce_weight": 1.0,
|
| 23 |
+
"pair_ce_weight": 5.0,
|
| 24 |
+
"source_adapter": true,
|
| 25 |
+
"context_adapter": true,
|
| 26 |
+
"no_injection": true
|
| 27 |
+
},
|
| 28 |
+
"mechanism_evidence": {
|
| 29 |
+
"current_vs_stale_causality": [
|
| 30 |
+
{
|
| 31 |
+
"seed": 1,
|
| 32 |
+
"N": 158,
|
| 33 |
+
"active_top1": 0.791,
|
| 34 |
+
"memory_off_top1": 0.0,
|
| 35 |
+
"delta_pp": 79.1,
|
| 36 |
+
"delta_margin": 8.266,
|
| 37 |
+
"t": 20.756,
|
| 38 |
+
"cohen_d": 1.656
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"seed": 2,
|
| 42 |
+
"N": 164,
|
| 43 |
+
"active_top1": 0.732,
|
| 44 |
+
"memory_off_top1": 0.0,
|
| 45 |
+
"delta_pp": 73.2,
|
| 46 |
+
"delta_margin": 7.702,
|
| 47 |
+
"t": 18.912,
|
| 48 |
+
"cohen_d": 1.481
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"seed": 3,
|
| 52 |
+
"N": 162,
|
| 53 |
+
"active_top1": 0.728,
|
| 54 |
+
"memory_off_top1": 0.0,
|
| 55 |
+
"delta_pp": 72.8,
|
| 56 |
+
"delta_margin": 7.756,
|
| 57 |
+
"t": 18.507,
|
| 58 |
+
"cohen_d": 1.459
|
| 59 |
+
}
|
| 60 |
+
],
|
| 61 |
+
"memory_readout_diagnosis": [
|
| 62 |
+
{
|
| 63 |
+
"seed": 1,
|
| 64 |
+
"N": 158,
|
| 65 |
+
"trunk_top1_cur": 0.0,
|
| 66 |
+
"memory_top1_cur": 0.797,
|
| 67 |
+
"active_top1_cur": 0.791,
|
| 68 |
+
"memory_top1_stale": 0.006,
|
| 69 |
+
"current_rank_median": 1
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"seed": 2,
|
| 73 |
+
"N": 164,
|
| 74 |
+
"trunk_top1_cur": 0.0,
|
| 75 |
+
"memory_top1_cur": 0.738,
|
| 76 |
+
"active_top1_cur": 0.732,
|
| 77 |
+
"memory_top1_stale": 0.006,
|
| 78 |
+
"current_rank_median": 1
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"seed": 3,
|
| 82 |
+
"N": 162,
|
| 83 |
+
"trunk_top1_cur": 0.0,
|
| 84 |
+
"memory_top1_cur": 0.735,
|
| 85 |
+
"active_top1_cur": 0.728,
|
| 86 |
+
"memory_top1_stale": 0.012,
|
| 87 |
+
"current_rank_median": 1
|
| 88 |
+
}
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
"general_eval": {
|
| 92 |
+
"wikitext103_test_ppl": 30.11,
|
| 93 |
+
"lambada": 0.159,
|
| 94 |
+
"hellaswag": 0.271,
|
| 95 |
+
"arc_easy": 0.281
|
| 96 |
+
},
|
| 97 |
+
"claim_boundary": "Research checkpoint for contextual memory readout; not a general downstream SOTA model."
|
| 98 |
+
}
|
config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "cedl",
|
| 3 |
+
"architectures": ["CEDLTwoLoop100M"],
|
| 4 |
+
"model_name": "CEDL",
|
| 5 |
+
"vocab_size": 50257,
|
| 6 |
+
"max_position_embeddings": 1024,
|
| 7 |
+
"parameters": 136254705,
|
| 8 |
+
"checkpoint_file": "pytorch_model.bin",
|
| 9 |
+
"sidecar_config_file": "cedl_config.json",
|
| 10 |
+
"implementation_file": "CEDL.py",
|
| 11 |
+
"public_description": "Hippocampal-inspired language model with contextual memory readout.",
|
| 12 |
+
"claim_boundary": "Research checkpoint for contextual memory readout; not a general downstream SOTA model."
|
| 13 |
+
}
|
| 14 |
+
|
data_v4c_pairs.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""V4c synthetic-pair generator.
|
| 3 |
+
|
| 4 |
+
Generates short belief-revision / contrast / update prompts with EXPLICIT
|
| 5 |
+
token-position spans for query / current / stale / distractor / neutral.
|
| 6 |
+
Resolved against GPT-2 BPE at generation time so spans are guaranteed correct
|
| 7 |
+
when consumed by the V4c contrastive loss.
|
| 8 |
+
|
| 9 |
+
Five item families:
|
| 10 |
+
- but-update : "Alice owns hat. Bob owns pen. But Alice now owns car. What does Alice own now? Answer:"
|
| 11 |
+
- however-revision : "The capital was Bonn until 1990. However the capital is now Berlin. What is the capital? Answer:"
|
| 12 |
+
- temporal-update : "Previously the gate was A12. Currently it is C7. Which gate is current? Answer:"
|
| 13 |
+
- paraphrased-equiv. : update twice (different paraphrases) → multiple positives for SupCon
|
| 14 |
+
- neutral-control : "Carol owns book. Frank owns desk. What does Carol own? Answer:" (no update; supplies negatives only)
|
| 15 |
+
|
| 16 |
+
Critical span rule (causal LM constraint): `query` MUST be the question-side
|
| 17 |
+
entity occurrence (the "Alice" in "What does Alice own now?"), NOT the first
|
| 18 |
+
factual mention. Otherwise the model would be asked to pull an early Alice
|
| 19 |
+
token toward a future "car" token, which is causally unreachable.
|
| 20 |
+
|
| 21 |
+
Returns (ids, span_dict) where span_dict has keys:
|
| 22 |
+
family : str — one of the 5 family names
|
| 23 |
+
query : list[(start, end)] — token-position spans (end-exclusive)
|
| 24 |
+
current : list[(start, end)]
|
| 25 |
+
stale : list[(start, end)]
|
| 26 |
+
distractor : list[(start, end)] | empty
|
| 27 |
+
neutral : list[(start, end)] | empty
|
| 28 |
+
paraphrase_positives: list[(start, end)] | empty
|
| 29 |
+
For neutral-control: query/current/stale/distractor/paraphrase_positives all
|
| 30 |
+
empty; only `neutral` is populated.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
from dataclasses import dataclass, field
|
| 35 |
+
from typing import Dict, List, Optional, Tuple
|
| 36 |
+
import random
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
NAMES = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace",
|
| 40 |
+
"Hank", "Iris", "Jack", "Kate", "Leo", "Mia", "Nick",
|
| 41 |
+
"Olivia", "Paul", "Quinn", "Rosa", "Sam", "Tina",
|
| 42 |
+
"Uma", "Victor", "Wendy", "Xander", "Yara", "Zane",
|
| 43 |
+
"Adam", "Beth", "Clara", "Drew", "Ella", "Felix",
|
| 44 |
+
"Greta", "Henry", "Ivy", "Julian", "Kira", "Liam",
|
| 45 |
+
"Mona", "Noah"]
|
| 46 |
+
OBJECTS = ["car", "hat", "bag", "pen", "cup", "box", "ring", "lamp",
|
| 47 |
+
"book", "ball", "shoe", "coat", "desk", "bell", "fork",
|
| 48 |
+
"drum", "fish", "kite", "coin", "vase",
|
| 49 |
+
"rope", "horn", "tent", "boot", "scarf", "glove",
|
| 50 |
+
"torch", "mask", "wand", "shield", "spear", "axe",
|
| 51 |
+
"brush", "comb", "flute", "kettle", "knife", "ladle",
|
| 52 |
+
"mirror", "needle"]
|
| 53 |
+
CAPITALS_OLD = [
|
| 54 |
+
("Bonn", "Berlin", "1990"),
|
| 55 |
+
("Karachi", "Islamabad", "1960"),
|
| 56 |
+
("Lagos", "Abuja", "1991"),
|
| 57 |
+
("Rio de Janeiro", "Brasilia", "1960"),
|
| 58 |
+
("Almaty", "Astana", "1997"),
|
| 59 |
+
("Yangon", "Naypyidaw", "2006"),
|
| 60 |
+
("Saint Petersburg", "Moscow", "1918"),
|
| 61 |
+
("Philadelphia", "Washington", "1800"),
|
| 62 |
+
("Kyoto", "Tokyo", "1868"),
|
| 63 |
+
("Calcutta", "Delhi", "1911"),
|
| 64 |
+
("Auckland", "Wellington", "1865"),
|
| 65 |
+
("Melbourne", "Canberra", "1927"),
|
| 66 |
+
("Salvador", "Rio de Janeiro", "1763"),
|
| 67 |
+
("Toronto", "Ottawa", "1857"),
|
| 68 |
+
("Bogota", "Lima", "1542"),
|
| 69 |
+
("Quito", "Cuenca", "1830"),
|
| 70 |
+
("Caracas", "Maracay", "1917"),
|
| 71 |
+
("Asuncion", "Concepcion", "1811"),
|
| 72 |
+
("Sucre", "La Paz", "1899"),
|
| 73 |
+
("Cuzco", "Lima", "1535"),
|
| 74 |
+
("Tunis", "Carthage", "1881"),
|
| 75 |
+
("Algiers", "Oran", "1962"),
|
| 76 |
+
("Tripoli", "Benghazi", "1969"),
|
| 77 |
+
("Aden", "Sanaa", "1990"),
|
| 78 |
+
("Beirut", "Damascus", "1944"),
|
| 79 |
+
("Vienna", "Salzburg", "1918"),
|
| 80 |
+
("Krakow", "Warsaw", "1596"),
|
| 81 |
+
("Helsinki", "Turku", "1812"),
|
| 82 |
+
("Stockholm", "Uppsala", "1523"),
|
| 83 |
+
("Trondheim", "Oslo", "1814"),
|
| 84 |
+
]
|
| 85 |
+
def _build_gate_pairs():
|
| 86 |
+
letters = list("ABCDEFGHJKLMNPQRSTUVWXYZ")
|
| 87 |
+
numbers = list(range(1, 21))
|
| 88 |
+
rng = random.Random(20260526)
|
| 89 |
+
pairs = []
|
| 90 |
+
for _ in range(260):
|
| 91 |
+
L_old, L_new = rng.sample(letters, 2)
|
| 92 |
+
n_old, n_new = rng.sample(numbers, 2)
|
| 93 |
+
pairs.append((f"{L_old}{n_old}", f"{L_new}{n_new}"))
|
| 94 |
+
return pairs
|
| 95 |
+
GATES = _build_gate_pairs()
|
| 96 |
+
PARAPHRASE_OLD_CITIES = [
|
| 97 |
+
"Paris", "London", "Madrid", "Rome", "Vienna",
|
| 98 |
+
"Athens", "Lisbon", "Prague", "Warsaw", "Helsinki",
|
| 99 |
+
"Dublin", "Brussels", "Amsterdam", "Stockholm", "Copenhagen",
|
| 100 |
+
"Budapest", "Sofia", "Bucharest", "Zagreb", "Belgrade",
|
| 101 |
+
]
|
| 102 |
+
PARAPHRASE_NEW_CITIES = [
|
| 103 |
+
"Berlin", "Tokyo", "Seoul", "Cairo", "Oslo",
|
| 104 |
+
"Manila", "Bangkok", "Hanoi", "Jakarta", "Singapore",
|
| 105 |
+
"Mumbai", "Lagos", "Nairobi", "Dakar", "Accra",
|
| 106 |
+
"Lima", "Bogota", "Quito", "Santiago", "Montevideo",
|
| 107 |
+
]
|
| 108 |
+
HOWEVER_SUBJECTS = [
|
| 109 |
+
"capital", "headquarters", "embassy",
|
| 110 |
+
"consulate", "annex", "secretariat",
|
| 111 |
+
"garrison", "outpost", "depot",
|
| 112 |
+
"treasury", "archive", "registry",
|
| 113 |
+
"tribunal", "synod", "academy",
|
| 114 |
+
"observatory", "arena", "fortress",
|
| 115 |
+
"monastery", "citadel",
|
| 116 |
+
]
|
| 117 |
+
GATE_SUBJECTS = [
|
| 118 |
+
"gate", "code", "room", "key", "slot",
|
| 119 |
+
"platform", "channel", "pin", "lock", "terminal",
|
| 120 |
+
"passage", "station", "dock", "port",
|
| 121 |
+
"panel", "module", "bay", "console",
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
VOCAB_SPLIT_SEED = 20260528
|
| 126 |
+
_TRAIN_FRAC = 0.70
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _partition(pool, offset):
|
| 130 |
+
"""Split a pool of ATOMIC SURFACE STRINGS ~70/30. Dedups + sorts first so
|
| 131 |
+
the partition is over unique surfaces (review finding: tuple-level splitting
|
| 132 |
+
of CAPITALS_OLD/GATES leaked the old/new strings shared across tuples — the
|
| 133 |
+
surfaces, not the tuples, are the entities under the novel-entity claim)."""
|
| 134 |
+
uniq = sorted(set(pool))
|
| 135 |
+
idx = list(range(len(uniq)))
|
| 136 |
+
random.Random(VOCAB_SPLIT_SEED + offset).shuffle(idx)
|
| 137 |
+
n_tr = max(1, int(round(_TRAIN_FRAC * len(uniq))))
|
| 138 |
+
tr = set(idx[:n_tr])
|
| 139 |
+
train = [uniq[i] for i in range(len(uniq)) if i in tr]
|
| 140 |
+
test = [uniq[i] for i in range(len(uniq)) if i not in tr]
|
| 141 |
+
return train, test
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
_CITIES = ([c for (_o, _n, _y) in CAPITALS_OLD for c in (_o, _n)]
|
| 145 |
+
+ PARAPHRASE_OLD_CITIES + PARAPHRASE_NEW_CITIES)
|
| 146 |
+
_GATE_CODES = [c for (_o, _n) in GATES for c in (_o, _n)]
|
| 147 |
+
YEARS = sorted({_y for (_o, _n, _y) in CAPITALS_OLD})
|
| 148 |
+
|
| 149 |
+
_POOL_REGISTRY = {
|
| 150 |
+
"NAMES": NAMES, "OBJECTS": OBJECTS,
|
| 151 |
+
"CITIES": _CITIES, "GATE_CODES": _GATE_CODES,
|
| 152 |
+
"HOWEVER_SUBJECTS": HOWEVER_SUBJECTS, "GATE_SUBJECTS": GATE_SUBJECTS,
|
| 153 |
+
}
|
| 154 |
+
_SPLITS = {}
|
| 155 |
+
for _off, (_nm, _pool) in enumerate(_POOL_REGISTRY.items()):
|
| 156 |
+
_tr, _te = _partition(_pool, _off)
|
| 157 |
+
_SPLITS[_nm] = {"train": _tr, "test": _te, "all": sorted(set(_pool))}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _P(name, split):
|
| 161 |
+
"""Pool `name` restricted to vocabulary `split`. Raises on an unknown split
|
| 162 |
+
(review finding: a silent fallback to 'all' would leak train/test vocab)."""
|
| 163 |
+
if split not in ("train", "test", "all"):
|
| 164 |
+
raise ValueError(
|
| 165 |
+
f"unknown vocab split {split!r}; expected 'train'/'test'/'all'")
|
| 166 |
+
return _SPLITS[name][split]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def vocab_split_summary():
|
| 170 |
+
"""Sizes of each pool per split — for sanity logging."""
|
| 171 |
+
return {nm: {s: len(_SPLITS[nm][s]) for s in ("train", "test", "all")}
|
| 172 |
+
for nm in _SPLITS}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def vocab_split_overlap():
|
| 176 |
+
"""Global train∩test surface-string overlap across ALL pools. Should be
|
| 177 |
+
empty — validates the 'novel entity' claim end to end."""
|
| 178 |
+
tr, te = set(), set()
|
| 179 |
+
for nm in _SPLITS:
|
| 180 |
+
tr |= set(_SPLITS[nm]["train"])
|
| 181 |
+
te |= set(_SPLITS[nm]["test"])
|
| 182 |
+
return sorted(tr & te)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
SpanT = Tuple[int, int]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@dataclass
|
| 189 |
+
class V4cItem:
|
| 190 |
+
"""One synthetic contrastive pair with token-position spans."""
|
| 191 |
+
family: str
|
| 192 |
+
ids: List[int]
|
| 193 |
+
query: List[SpanT] = field(default_factory=list)
|
| 194 |
+
current: List[SpanT] = field(default_factory=list)
|
| 195 |
+
stale: List[SpanT] = field(default_factory=list)
|
| 196 |
+
distractor: List[SpanT] = field(default_factory=list)
|
| 197 |
+
neutral: List[SpanT] = field(default_factory=list)
|
| 198 |
+
paraphrase_positives: List[SpanT] = field(default_factory=list)
|
| 199 |
+
original_length: int = 0
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def _tok_span(tokenizer, full_ids: List[int], substring: str,
|
| 203 |
+
search_from: int = 0) -> Optional[SpanT]:
|
| 204 |
+
"""Locate `substring` in the decoded text and return its token-position
|
| 205 |
+
span. Returns None if not found. `search_from` lets the caller bias the
|
| 206 |
+
search past earlier occurrences (e.g., the question-side query token must
|
| 207 |
+
come AFTER both current and stale mentions)."""
|
| 208 |
+
encoded = tokenizer.encode(" " + substring, add_special_tokens=False)
|
| 209 |
+
if not encoded:
|
| 210 |
+
encoded = tokenizer.encode(substring, add_special_tokens=False)
|
| 211 |
+
if not encoded:
|
| 212 |
+
return None
|
| 213 |
+
target_len = len(encoded)
|
| 214 |
+
for start in range(search_from, len(full_ids) - target_len + 1):
|
| 215 |
+
if full_ids[start:start + target_len] == encoded:
|
| 216 |
+
return (start, start + target_len)
|
| 217 |
+
encoded_bare = tokenizer.encode(substring, add_special_tokens=False)
|
| 218 |
+
if encoded_bare:
|
| 219 |
+
L = len(encoded_bare)
|
| 220 |
+
for start in range(search_from, len(full_ids) - L + 1):
|
| 221 |
+
if full_ids[start:start + L] == encoded_bare:
|
| 222 |
+
return (start, start + L)
|
| 223 |
+
return None
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _last_tok_span(tokenizer, full_ids: List[int], substring: str) -> Optional[SpanT]:
|
| 227 |
+
"""Find the LAST occurrence of `substring` in token-position space."""
|
| 228 |
+
encoded = tokenizer.encode(" " + substring, add_special_tokens=False)
|
| 229 |
+
if not encoded:
|
| 230 |
+
return None
|
| 231 |
+
L = len(encoded)
|
| 232 |
+
found = None
|
| 233 |
+
for start in range(0, len(full_ids) - L + 1):
|
| 234 |
+
if full_ids[start:start + L] == encoded:
|
| 235 |
+
found = (start, start + L)
|
| 236 |
+
if found is None:
|
| 237 |
+
encoded_bare = tokenizer.encode(substring, add_special_tokens=False)
|
| 238 |
+
if encoded_bare:
|
| 239 |
+
L = len(encoded_bare)
|
| 240 |
+
for start in range(0, len(full_ids) - L + 1):
|
| 241 |
+
if full_ids[start:start + L] == encoded_bare:
|
| 242 |
+
found = (start, start + L)
|
| 243 |
+
return found
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def gen_but_update(tokenizer, rng: random.Random, split: str = "all",
|
| 247 |
+
force_stale_obj: str = None) -> V4cItem:
|
| 248 |
+
"""Alice owns hat. Bob owns pen. Carol owns cup. But Alice now owns car.
|
| 249 |
+
What does Alice own now? Answer:
|
| 250 |
+
|
| 251 |
+
`force_stale_obj` (hard-collision support): pin the stale object to a given
|
| 252 |
+
token (e.g. another item's CURRENT object) so the same token appears in
|
| 253 |
+
opposite roles across the batch — forcing role-by-position, not token id."""
|
| 254 |
+
n_distract = 2
|
| 255 |
+
names = rng.sample(_P("NAMES", split), n_distract + 1)
|
| 256 |
+
if force_stale_obj is not None:
|
| 257 |
+
others = rng.sample(
|
| 258 |
+
[o for o in _P("OBJECTS", split) if o != force_stale_obj],
|
| 259 |
+
n_distract + 1)
|
| 260 |
+
objs = [force_stale_obj] + others
|
| 261 |
+
else:
|
| 262 |
+
objs = rng.sample(_P("OBJECTS", split), n_distract + 2)
|
| 263 |
+
subject = names[0]
|
| 264 |
+
stale_obj = objs[0]
|
| 265 |
+
new_obj = objs[-1]
|
| 266 |
+
distractor_obj = objs[1] if n_distract > 0 else objs[0]
|
| 267 |
+
facts = [f"{names[i]} owns a {objs[i]}" for i in range(n_distract + 1)]
|
| 268 |
+
rng.shuffle(facts)
|
| 269 |
+
text = (". ".join(facts) + ". "
|
| 270 |
+
+ f"But {subject} now owns a {new_obj}. "
|
| 271 |
+
+ f"What does {subject} own now? Answer:")
|
| 272 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 273 |
+
|
| 274 |
+
current_span = _tok_span(tokenizer, ids, new_obj)
|
| 275 |
+
stale_span = _tok_span(tokenizer, ids, stale_obj)
|
| 276 |
+
distractor_span = _tok_span(tokenizer, ids, distractor_obj)
|
| 277 |
+
query_span = _last_tok_span(tokenizer, ids, subject)
|
| 278 |
+
return V4cItem(
|
| 279 |
+
family="but_update",
|
| 280 |
+
ids=ids,
|
| 281 |
+
query=[query_span] if query_span else [],
|
| 282 |
+
current=[current_span] if current_span else [],
|
| 283 |
+
stale=[stale_span] if stale_span else [],
|
| 284 |
+
distractor=[distractor_span] if distractor_span else [],
|
| 285 |
+
original_length=len(ids),
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def gen_however_revision(tokenizer, rng: random.Random,
|
| 290 |
+
split: str = "all",
|
| 291 |
+
force_subject: str = None) -> V4cItem:
|
| 292 |
+
"""The headquarters was Bonn until 1990. However the headquarters is now
|
| 293 |
+
Berlin. What is the headquarters? Answer:
|
| 294 |
+
|
| 295 |
+
Subject noun varies across items to avoid (query=`capital`, current=X)
|
| 296 |
+
collisions when the same `current` city is reused. With ~14 subjects ×
|
| 297 |
+
30 (old, new, year) triples, paired-collision rate stays under birthday-
|
| 298 |
+
paradox bounds for 100-150 items."""
|
| 299 |
+
old, new = rng.sample(_P("CITIES", split), 2)
|
| 300 |
+
year = rng.choice(YEARS)
|
| 301 |
+
subject = (force_subject if force_subject is not None
|
| 302 |
+
else rng.choice(_P("HOWEVER_SUBJECTS", split)))
|
| 303 |
+
text = (f"The {subject} was {old} until {year}. "
|
| 304 |
+
f"However the {subject} is now {new}. "
|
| 305 |
+
f"What is the {subject}? Answer:")
|
| 306 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 307 |
+
current_span = _tok_span(tokenizer, ids, new)
|
| 308 |
+
stale_span = _tok_span(tokenizer, ids, old)
|
| 309 |
+
query_span = _last_tok_span(tokenizer, ids, subject)
|
| 310 |
+
return V4cItem(
|
| 311 |
+
family="however_revision",
|
| 312 |
+
ids=ids,
|
| 313 |
+
query=[query_span] if query_span else [],
|
| 314 |
+
current=[current_span] if current_span else [],
|
| 315 |
+
stale=[stale_span] if stale_span else [],
|
| 316 |
+
original_length=len(ids),
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def gen_temporal_update(tokenizer, rng: random.Random,
|
| 321 |
+
split: str = "all",
|
| 322 |
+
force_subject: str = None) -> V4cItem:
|
| 323 |
+
"""Previously the platform was A12. Currently the platform is C7. Which
|
| 324 |
+
platform is current now? Answer:
|
| 325 |
+
|
| 326 |
+
Subject noun varies; combined with 260-pair GATES pool gives
|
| 327 |
+
~18 × 260 = 4680 unique (subject, current) combinations."""
|
| 328 |
+
old, new = rng.sample(_P("GATE_CODES", split), 2)
|
| 329 |
+
subject = (force_subject if force_subject is not None
|
| 330 |
+
else rng.choice(_P("GATE_SUBJECTS", split)))
|
| 331 |
+
text = (f"Previously the {subject} was {old}. "
|
| 332 |
+
f"Currently the {subject} is {new}. "
|
| 333 |
+
f"Which {subject} is current now? Answer:")
|
| 334 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 335 |
+
current_span = _tok_span(tokenizer, ids, new)
|
| 336 |
+
stale_span = _tok_span(tokenizer, ids, old)
|
| 337 |
+
query_span = _last_tok_span(tokenizer, ids, subject)
|
| 338 |
+
return V4cItem(
|
| 339 |
+
family="temporal_update",
|
| 340 |
+
ids=ids,
|
| 341 |
+
query=[query_span] if query_span else [],
|
| 342 |
+
current=[current_span] if current_span else [],
|
| 343 |
+
stale=[stale_span] if stale_span else [],
|
| 344 |
+
original_length=len(ids),
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def gen_paraphrased_equiv(tokenizer, rng: random.Random,
|
| 349 |
+
split: str = "all") -> V4cItem:
|
| 350 |
+
"""Same entity, two paraphrased questions → two query positives.
|
| 351 |
+
|
| 352 |
+
Carol used to live in Paris. She moved to Berlin. Where does Carol live now?
|
| 353 |
+
Carol resides in Berlin currently. What city is Carol's home? Answer:
|
| 354 |
+
"""
|
| 355 |
+
person = rng.choice(_P("NAMES", split))
|
| 356 |
+
old_city, new_city = rng.sample(_P("CITIES", split), 2)
|
| 357 |
+
text = (f"{person} used to live in {old_city}. "
|
| 358 |
+
f"{person} moved to {new_city}. "
|
| 359 |
+
f"Where does {person} live now? "
|
| 360 |
+
f"{person} resides in {new_city} currently. "
|
| 361 |
+
f"What city is {person}'s home? Answer:")
|
| 362 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 363 |
+
current_first = _tok_span(tokenizer, ids, new_city)
|
| 364 |
+
stale_span = _tok_span(tokenizer, ids, old_city)
|
| 365 |
+
current_second = None
|
| 366 |
+
if current_first is not None:
|
| 367 |
+
current_second = _tok_span(tokenizer, ids, new_city,
|
| 368 |
+
search_from=current_first[1])
|
| 369 |
+
query_span = _last_tok_span(tokenizer, ids, person)
|
| 370 |
+
item = V4cItem(
|
| 371 |
+
family="paraphrased_equiv",
|
| 372 |
+
ids=ids,
|
| 373 |
+
query=[query_span] if query_span else [],
|
| 374 |
+
current=[current_first] if current_first else [],
|
| 375 |
+
stale=[stale_span] if stale_span else [],
|
| 376 |
+
original_length=len(ids),
|
| 377 |
+
)
|
| 378 |
+
if current_second is not None:
|
| 379 |
+
item.paraphrase_positives.append(current_second)
|
| 380 |
+
return item
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def gen_neutral_control(tokenizer, rng: random.Random,
|
| 384 |
+
split: str = "all") -> V4cItem:
|
| 385 |
+
"""No update; supplies negatives only. The whole text serves as a single
|
| 386 |
+
`neutral` span pool — anchor + positive roles are skipped by the loss."""
|
| 387 |
+
n = 3
|
| 388 |
+
names = rng.sample(_P("NAMES", split), n)
|
| 389 |
+
objs = rng.sample(_P("OBJECTS", split), n)
|
| 390 |
+
facts = [f"{names[i]} owns a {objs[i]}" for i in range(n)]
|
| 391 |
+
text = ". ".join(facts) + ". "
|
| 392 |
+
text += f"What does {names[0]} own? Answer:"
|
| 393 |
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
| 394 |
+
return V4cItem(
|
| 395 |
+
family="neutral_control",
|
| 396 |
+
ids=ids,
|
| 397 |
+
neutral=[(0, len(ids))],
|
| 398 |
+
original_length=len(ids),
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
_FAMILIES = [
|
| 403 |
+
("but_update", gen_but_update, 0.30),
|
| 404 |
+
("however_revision", gen_however_revision, 0.20),
|
| 405 |
+
("temporal_update", gen_temporal_update, 0.20),
|
| 406 |
+
("paraphrased_equiv", gen_paraphrased_equiv, 0.10),
|
| 407 |
+
("neutral_control", gen_neutral_control, 0.20),
|
| 408 |
+
]
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
_FAMILY_FNS = {name: fn for name, fn, _ in _FAMILIES}
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def make_entity_swap(item: "V4cItem", tokenizer, rng: random.Random,
|
| 415 |
+
split: str = "train") -> "V4cItem":
|
| 416 |
+
"""Structurally-identical twin of `item`: SAME family AND template subtype,
|
| 417 |
+
only the entity FILLERS swapped, with its OWN recomputed spans (review
|
| 418 |
+
Blocker 3 — preserve role structure, NOT span offsets). For however/temporal
|
| 419 |
+
the template subtype is the query subject-noun (capital/gate/…), so it is
|
| 420 |
+
PRESERVED and only the cities/codes are freshly sampled (review finding 2:
|
| 421 |
+
a same-family resample that also changed the subject is broader than
|
| 422 |
+
'same template, fresh entities'). Used by the V4e swap-consistency loss:
|
| 423 |
+
g at the current/stale role spans must MATCH across this swap, forcing
|
| 424 |
+
v_vec PRODUCTION to key on STRUCTURE not entity id."""
|
| 425 |
+
fam = item.family
|
| 426 |
+
if fam in ("however_revision", "temporal_update") and item.query:
|
| 427 |
+
qs, qe = item.query[0]
|
| 428 |
+
subj = tokenizer.decode(item.ids[qs:qe]).strip()
|
| 429 |
+
fn = (gen_however_revision if fam == "however_revision"
|
| 430 |
+
else gen_temporal_update)
|
| 431 |
+
return fn(tokenizer, rng, split, force_subject=subj)
|
| 432 |
+
fn = _FAMILY_FNS.get(fam, gen_but_update)
|
| 433 |
+
return fn(tokenizer, rng, split)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def generate(tokenizer, n: int, seed: int = 42,
|
| 437 |
+
neutral_cap: float = 0.25, split: str = "all",
|
| 438 |
+
hard_collision_frac: float = 0.0,
|
| 439 |
+
family_weights: Optional[Dict[str, float]] = None
|
| 440 |
+
) -> List[V4cItem]:
|
| 441 |
+
"""Generate `n` V4c items by family with the weights above. neutral_cap
|
| 442 |
+
bounds the neutral_control fraction (since neutrals can't be anchors and
|
| 443 |
+
>25% neutrals starves SupCon supervision).
|
| 444 |
+
|
| 445 |
+
split: 'train'/'test'/'all' vocabulary partition (Correction 4 — novel
|
| 446 |
+
entity generalization is tested by training on 'train', probing 'test').
|
| 447 |
+
hard_collision_frac: fraction of but_update items whose STALE object is
|
| 448 |
+
pinned to a recently-seen CURRENT object (Correction 5 — same token in
|
| 449 |
+
opposite roles across the batch, forcing role-by-position not token id).
|
| 450 |
+
within-item current==stale is never created (audit hard-fails it)."""
|
| 451 |
+
rng = random.Random(seed)
|
| 452 |
+
_resolved = []
|
| 453 |
+
for name, fn, w in _FAMILIES:
|
| 454 |
+
if family_weights is not None and name in family_weights:
|
| 455 |
+
w = float(family_weights[name])
|
| 456 |
+
_resolved.append((name, fn, w))
|
| 457 |
+
families_cumweights = []
|
| 458 |
+
cum = 0.0
|
| 459 |
+
for name, fn, w in _resolved:
|
| 460 |
+
cum += w
|
| 461 |
+
families_cumweights.append((name, fn, cum))
|
| 462 |
+
items: List[V4cItem] = []
|
| 463 |
+
n_neutral = 0
|
| 464 |
+
n_neutral_cap = int(n * neutral_cap)
|
| 465 |
+
recent_currents: List[str] = []
|
| 466 |
+
while len(items) < n:
|
| 467 |
+
r = rng.random() * cum
|
| 468 |
+
for name, fn, cwt in families_cumweights:
|
| 469 |
+
if r <= cwt:
|
| 470 |
+
if name == "neutral_control" and n_neutral >= n_neutral_cap:
|
| 471 |
+
fn = gen_but_update
|
| 472 |
+
name = "but_update"
|
| 473 |
+
if (name == "but_update" and recent_currents
|
| 474 |
+
and rng.random() < hard_collision_frac):
|
| 475 |
+
forced = rng.choice(recent_currents)
|
| 476 |
+
item = gen_but_update(tokenizer, rng, split,
|
| 477 |
+
force_stale_obj=forced)
|
| 478 |
+
else:
|
| 479 |
+
item = fn(tokenizer, rng, split)
|
| 480 |
+
items.append(item)
|
| 481 |
+
if item.family == "neutral_control":
|
| 482 |
+
n_neutral += 1
|
| 483 |
+
if item.family == "but_update" and item.current:
|
| 484 |
+
cs, ce = item.current[0]
|
| 485 |
+
recent_currents.append(
|
| 486 |
+
tokenizer.decode(item.ids[cs:ce]).strip())
|
| 487 |
+
recent_currents = recent_currents[-32:]
|
| 488 |
+
break
|
| 489 |
+
return items
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def audit(items: List[V4cItem], tokenizer, *, verbose: bool = False
|
| 493 |
+
) -> Dict[str, object]:
|
| 494 |
+
"""Family-aware audit. Returns a report dict; `verbose` prints failures."""
|
| 495 |
+
report = {
|
| 496 |
+
"total": len(items),
|
| 497 |
+
"by_family": {},
|
| 498 |
+
"hard_fails": [],
|
| 499 |
+
"duplicate_collisions": 0,
|
| 500 |
+
"n_anchorable": 0,
|
| 501 |
+
"duplicate_collisions_by_family": {},
|
| 502 |
+
"top_repeated_pairs": [],
|
| 503 |
+
}
|
| 504 |
+
for it in items:
|
| 505 |
+
report["by_family"][it.family] = report["by_family"].get(it.family, 0) + 1
|
| 506 |
+
|
| 507 |
+
seen_keys = {}
|
| 508 |
+
pair_counter = {}
|
| 509 |
+
for i, it in enumerate(items):
|
| 510 |
+
ids_set = set(it.ids)
|
| 511 |
+
for span_list in [it.query, it.current, it.stale, it.distractor,
|
| 512 |
+
it.neutral, it.paraphrase_positives]:
|
| 513 |
+
for s, e in span_list:
|
| 514 |
+
if not (0 <= s < e <= len(it.ids)):
|
| 515 |
+
report["hard_fails"].append((i, f"span out of bounds: ({s},{e}) len={len(it.ids)}"))
|
| 516 |
+
|
| 517 |
+
if it.family == "neutral_control":
|
| 518 |
+
if not it.neutral:
|
| 519 |
+
report["hard_fails"].append((i, "neutral_control with empty neutral"))
|
| 520 |
+
for fname in ("query", "current", "stale", "distractor",
|
| 521 |
+
"paraphrase_positives"):
|
| 522 |
+
if getattr(it, fname):
|
| 523 |
+
report["hard_fails"].append((i, f"neutral_control has non-empty {fname}"))
|
| 524 |
+
else:
|
| 525 |
+
if not it.query:
|
| 526 |
+
report["hard_fails"].append((i, f"{it.family}: empty query"))
|
| 527 |
+
continue
|
| 528 |
+
if not it.current:
|
| 529 |
+
report["hard_fails"].append((i, f"{it.family}: empty current"))
|
| 530 |
+
continue
|
| 531 |
+
if not it.stale:
|
| 532 |
+
report["hard_fails"].append((i, f"{it.family}: empty stale"))
|
| 533 |
+
continue
|
| 534 |
+
current_text = tokenizer.decode(
|
| 535 |
+
[t for s, e in it.current for t in it.ids[s:e]]).strip()
|
| 536 |
+
stale_text = tokenizer.decode(
|
| 537 |
+
[t for s, e in it.stale for t in it.ids[s:e]]).strip()
|
| 538 |
+
if current_text == stale_text:
|
| 539 |
+
report["hard_fails"].append(
|
| 540 |
+
(i, f"{it.family}: current surface == stale surface "
|
| 541 |
+
f"({current_text!r})"))
|
| 542 |
+
for s, e in it.distractor:
|
| 543 |
+
d_text = tokenizer.decode(it.ids[s:e]).strip()
|
| 544 |
+
if d_text == current_text or d_text == stale_text:
|
| 545 |
+
report["hard_fails"].append(
|
| 546 |
+
(i, f"{it.family}: distractor surface matches "
|
| 547 |
+
f"current/stale ({d_text!r})"))
|
| 548 |
+
break
|
| 549 |
+
q_start = it.query[0][0]
|
| 550 |
+
c_max = max(e for s, e in it.current)
|
| 551 |
+
s_max = max(e for s, e in it.stale)
|
| 552 |
+
if not (q_start >= c_max and q_start >= s_max):
|
| 553 |
+
report["hard_fails"].append((
|
| 554 |
+
i, f"{it.family}: query_pos={q_start} not > "
|
| 555 |
+
f"current_end={c_max} / stale_end={s_max}"))
|
| 556 |
+
else:
|
| 557 |
+
report["n_anchorable"] += 1
|
| 558 |
+
|
| 559 |
+
q_tokens = tuple(it.ids[it.query[0][0]:it.query[0][1]])
|
| 560 |
+
c_tokens = tuple(it.ids[it.current[0][0]:it.current[0][1]])
|
| 561 |
+
key = (q_tokens, c_tokens)
|
| 562 |
+
if key in seen_keys:
|
| 563 |
+
report["duplicate_collisions"] += 1
|
| 564 |
+
report["duplicate_collisions_by_family"][it.family] = (
|
| 565 |
+
report["duplicate_collisions_by_family"].get(it.family, 0) + 1)
|
| 566 |
+
seen_keys[key] = True
|
| 567 |
+
q_decoded = tokenizer.decode(list(q_tokens)).strip()
|
| 568 |
+
c_decoded = tokenizer.decode(list(c_tokens)).strip()
|
| 569 |
+
pair_key = (it.family, q_decoded, c_decoded)
|
| 570 |
+
pair_counter[pair_key] = pair_counter.get(pair_key, 0) + 1
|
| 571 |
+
|
| 572 |
+
sorted_pairs = sorted(pair_counter.items(), key=lambda kv: -kv[1])
|
| 573 |
+
report["top_repeated_pairs"] = [
|
| 574 |
+
{"family": k[0], "query": k[1], "current": k[2], "count": v}
|
| 575 |
+
for k, v in sorted_pairs[:10] if v > 1
|
| 576 |
+
]
|
| 577 |
+
|
| 578 |
+
current_surfaces = set()
|
| 579 |
+
for it in items:
|
| 580 |
+
if it.current:
|
| 581 |
+
s, e = it.current[0]
|
| 582 |
+
current_surfaces.add(tokenizer.decode(it.ids[s:e]).strip())
|
| 583 |
+
n_anchor = 0
|
| 584 |
+
n_role_collision = 0
|
| 585 |
+
for it in items:
|
| 586 |
+
if it.family == "neutral_control" or not it.current or not it.stale:
|
| 587 |
+
continue
|
| 588 |
+
n_anchor += 1
|
| 589 |
+
neg_surfaces = []
|
| 590 |
+
s, e = it.stale[0]
|
| 591 |
+
neg_surfaces.append(tokenizer.decode(it.ids[s:e]).strip())
|
| 592 |
+
for s, e in it.distractor:
|
| 593 |
+
neg_surfaces.append(tokenizer.decode(it.ids[s:e]).strip())
|
| 594 |
+
if any(ns in current_surfaces for ns in neg_surfaces):
|
| 595 |
+
n_role_collision += 1
|
| 596 |
+
report["role_collision_rate"] = (n_role_collision / n_anchor
|
| 597 |
+
if n_anchor else 0.0)
|
| 598 |
+
report["role_collision_count"] = n_role_collision
|
| 599 |
+
|
| 600 |
+
if verbose and report["hard_fails"]:
|
| 601 |
+
for i, reason in report["hard_fails"][:20]:
|
| 602 |
+
print(f" HARD-FAIL item {i}: {reason}")
|
| 603 |
+
if len(report["hard_fails"]) > 20:
|
| 604 |
+
print(f" ... ({len(report['hard_fails']) - 20} more)")
|
| 605 |
+
return report
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def decode_spans(item: V4cItem, tokenizer) -> Dict[str, List[str]]:
|
| 609 |
+
"""Decode each span family back into surface text for visual audit."""
|
| 610 |
+
out = {}
|
| 611 |
+
for fname in ("query", "current", "stale", "distractor", "neutral",
|
| 612 |
+
"paraphrase_positives"):
|
| 613 |
+
out[fname] = [tokenizer.decode(item.ids[s:e]).strip()
|
| 614 |
+
for s, e in getattr(item, fname)]
|
| 615 |
+
return out
|
examples/load_checkpoint.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
if str(ROOT) not in sys.path:
|
| 10 |
+
sys.path.insert(0, str(ROOT))
|
| 11 |
+
|
| 12 |
+
from CEDL import build_model
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def constructor_kwargs(config_path):
|
| 16 |
+
with open(config_path) as f:
|
| 17 |
+
cfg = json.load(f)
|
| 18 |
+
mem = cfg.get("memory_readout", {})
|
| 19 |
+
source_name = str(mem.get("source", "contextual_memory_state"))
|
| 20 |
+
source_map = {
|
| 21 |
+
"contextual_memory_state": "q_mem",
|
| 22 |
+
"decoder_state": "h_d",
|
| 23 |
+
"expanded_state": "h_e",
|
| 24 |
+
"attractor_state": "q_attractor",
|
| 25 |
+
"q_mem": "q_mem",
|
| 26 |
+
}
|
| 27 |
+
return dict(
|
| 28 |
+
lambda_head=bool(mem.get("lambda_head", True)),
|
| 29 |
+
lambda_head_hidden=int(mem.get("lambda_head_hidden", 160)),
|
| 30 |
+
lambda_head_bias_init=float(mem.get("lambda_head_bias_init", -7.0)),
|
| 31 |
+
lambda_head_w_init_std=float(
|
| 32 |
+
mem.get("lambda_head_w_init_std", 0.05)),
|
| 33 |
+
bce_objective=(
|
| 34 |
+
mem.get("selection_objective") == "binary_answer_background"),
|
| 35 |
+
sel_weight=1.0,
|
| 36 |
+
bg_weight=1.0,
|
| 37 |
+
bg_target=float(mem.get("background_target", 0.01)),
|
| 38 |
+
wt_sparsity_weight=float(mem.get("sparsity_weight", 0.05)),
|
| 39 |
+
wt_sparsity_target=float(mem.get("sparsity_target", 0.05)),
|
| 40 |
+
memory_head_enabled=bool(mem.get("enabled", True)),
|
| 41 |
+
memory_ce_weight=float(mem.get("memory_ce_weight", 1.0)),
|
| 42 |
+
memory_pair_ce_weight=float(mem.get("pair_ce_weight", 5.0)),
|
| 43 |
+
memory_query_source=source_map.get(source_name, source_name),
|
| 44 |
+
memory_readout_mode="direct",
|
| 45 |
+
source_adapter=bool(mem.get("source_adapter", True)),
|
| 46 |
+
context_adapter=bool(mem.get("context_adapter", True)),
|
| 47 |
+
specialist_noinject=bool(mem.get("no_injection", True)),
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def unwrap_state_dict(obj):
|
| 52 |
+
if isinstance(obj, dict) and "model" in obj and isinstance(obj["model"], dict):
|
| 53 |
+
return obj["model"]
|
| 54 |
+
return obj
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
parser = argparse.ArgumentParser()
|
| 59 |
+
parser.add_argument("--checkpoint", default="pytorch_model.bin")
|
| 60 |
+
parser.add_argument("--config", default="cedl_config.json")
|
| 61 |
+
parser.add_argument("--device", default="cpu")
|
| 62 |
+
args = parser.parse_args()
|
| 63 |
+
|
| 64 |
+
ckpt_path = Path(args.checkpoint)
|
| 65 |
+
if not ckpt_path.exists():
|
| 66 |
+
raise FileNotFoundError(f"Checkpoint not found: {ckpt_path}")
|
| 67 |
+
cfg_path = Path(args.config)
|
| 68 |
+
if not cfg_path.exists():
|
| 69 |
+
raise FileNotFoundError(f"Config not found: {cfg_path}")
|
| 70 |
+
|
| 71 |
+
model = build_model(
|
| 72 |
+
"CEDL",
|
| 73 |
+
vocab=50257,
|
| 74 |
+
max_seq=1024,
|
| 75 |
+
**constructor_kwargs(cfg_path),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
state = unwrap_state_dict(torch.load(ckpt_path, map_location="cpu"))
|
| 79 |
+
result = model.load_state_dict(state, strict=True)
|
| 80 |
+
model.to(args.device)
|
| 81 |
+
model.eval()
|
| 82 |
+
|
| 83 |
+
n_params = sum(p.numel() for p in model.parameters())
|
| 84 |
+
print(f"Loaded {ckpt_path}")
|
| 85 |
+
print(f"Parameters: {n_params:,}")
|
| 86 |
+
print(f"Missing keys: {len(result.missing_keys)}")
|
| 87 |
+
print(f"Unexpected keys: {len(result.unexpected_keys)}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
main()
|
probes/probe_memory_causality.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Memory-readout causality probe.
|
| 2 |
+
|
| 3 |
+
Runs the same current-vs-stale items through the live CEDL memory path and a
|
| 4 |
+
memory-off ablation. The paired comparison estimates whether contextual memory
|
| 5 |
+
readout causally improves current-token selection.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python probes/probe_memory_causality.py \
|
| 9 |
+
--checkpoint pytorch_model.bin \
|
| 10 |
+
--sidecar cedl_config.json \
|
| 11 |
+
--n-items 50
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn as nn
|
| 22 |
+
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
sys.path.insert(0, "/content")
|
| 25 |
+
|
| 26 |
+
import CEDL
|
| 27 |
+
import data_v4c_pairs as v4c
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
MEMORY_GATE_ATTR = "v" + "6_lambda_head"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ZeroLambdaHead(nn.Module):
|
| 34 |
+
"""Forces the memory mixture gate near zero."""
|
| 35 |
+
def forward(self, h):
|
| 36 |
+
return torch.full(
|
| 37 |
+
h.shape[:-1] + (1,), -100.0,
|
| 38 |
+
device=h.device, dtype=h.dtype,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_sidecar_constructor_kwargs(sidecar_path):
|
| 43 |
+
"""Read public CEDL config and return constructor keyword arguments."""
|
| 44 |
+
with open(sidecar_path) as f:
|
| 45 |
+
sc = json.load(f)
|
| 46 |
+
mem = sc.get("memory_readout")
|
| 47 |
+
if not isinstance(mem, dict):
|
| 48 |
+
raise ValueError("Expected cedl_config.json with a memory_readout block.")
|
| 49 |
+
source_name = str(mem.get("source", "contextual_memory_state"))
|
| 50 |
+
source_map = {
|
| 51 |
+
"contextual_memory_state": "q_mem",
|
| 52 |
+
"decoder_state": "h_d",
|
| 53 |
+
"expanded_state": "h_e",
|
| 54 |
+
"attractor_state": "q_attractor",
|
| 55 |
+
"q_mem": "q_mem",
|
| 56 |
+
}
|
| 57 |
+
return dict(
|
| 58 |
+
lambda_head=bool(mem.get("lambda_head", True)),
|
| 59 |
+
lambda_head_hidden=int(mem.get("lambda_head_hidden", 160)),
|
| 60 |
+
lambda_head_bias_init=float(mem.get("lambda_head_bias_init", -7.0)),
|
| 61 |
+
lambda_head_w_init_std=float(
|
| 62 |
+
mem.get("lambda_head_w_init_std", 0.05)),
|
| 63 |
+
bce_objective=(
|
| 64 |
+
mem.get("selection_objective") == "binary_answer_background"),
|
| 65 |
+
sel_weight=1.0,
|
| 66 |
+
bg_weight=1.0,
|
| 67 |
+
bg_target=float(mem.get("background_target", 0.01)),
|
| 68 |
+
wt_sparsity_weight=float(mem.get("sparsity_weight", 0.05)),
|
| 69 |
+
wt_sparsity_target=float(mem.get("sparsity_target", 0.05)),
|
| 70 |
+
memory_head_enabled=bool(mem.get("enabled", True)),
|
| 71 |
+
memory_ce_weight=float(mem.get("memory_ce_weight", 1.0)),
|
| 72 |
+
memory_pair_ce_weight=float(mem.get("pair_ce_weight", 5.0)),
|
| 73 |
+
memory_query_source=source_map.get(source_name, source_name),
|
| 74 |
+
memory_readout_mode="direct",
|
| 75 |
+
source_adapter=bool(mem.get("source_adapter", True)),
|
| 76 |
+
context_adapter=bool(mem.get("context_adapter", True)),
|
| 77 |
+
specialist_noinject=bool(mem.get("no_injection", True)),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def model_forward_logits(m, ids_b):
|
| 82 |
+
"""Call m(ids_b) and return the logits tensor [B, T, V] regardless of
|
| 83 |
+
whether forward returns logits-only or (logits, aux_loss)."""
|
| 84 |
+
out = m(ids_b)
|
| 85 |
+
if isinstance(out, tuple):
|
| 86 |
+
logits = out[0]
|
| 87 |
+
else:
|
| 88 |
+
logits = out
|
| 89 |
+
return logits
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def main():
|
| 93 |
+
p = argparse.ArgumentParser()
|
| 94 |
+
p.add_argument("--checkpoint", type=str, default="pytorch_model.bin")
|
| 95 |
+
p.add_argument("--sidecar", type=str, default="cedl_config.json")
|
| 96 |
+
p.add_argument("--n-items", type=int, default=50)
|
| 97 |
+
p.add_argument("--seed", type=int, default=0)
|
| 98 |
+
args = p.parse_args()
|
| 99 |
+
|
| 100 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 101 |
+
print(f"[setup] device={device}")
|
| 102 |
+
print(f"[setup] checkpoint={args.checkpoint}")
|
| 103 |
+
print(f"[setup] sidecar={args.sidecar}")
|
| 104 |
+
print(f"[setup] n_items={args.n_items}")
|
| 105 |
+
|
| 106 |
+
model_kwargs = load_sidecar_constructor_kwargs(args.sidecar)
|
| 107 |
+
print(f"[setup] memory_head={model_kwargs['lambda_head']} "
|
| 108 |
+
f"hidden={model_kwargs['lambda_head_hidden']} "
|
| 109 |
+
f"bias_init={model_kwargs['lambda_head_bias_init']} "
|
| 110 |
+
f"sparsity_weight={model_kwargs['wt_sparsity_weight']} "
|
| 111 |
+
f"sparsity_target={model_kwargs['wt_sparsity_target']} "
|
| 112 |
+
f"memory_source={model_kwargs['memory_query_source']} "
|
| 113 |
+
f"readout_mode={model_kwargs['memory_readout_mode']}")
|
| 114 |
+
|
| 115 |
+
m = CEDL.build_model("CEDL", vocab=50257, max_seq=1024, **model_kwargs)
|
| 116 |
+
m = m.to(device).eval()
|
| 117 |
+
|
| 118 |
+
state = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
|
| 119 |
+
msd = state["model"] if isinstance(state, dict) and "model" in state else state
|
| 120 |
+
if any(k.startswith("_orig_mod.") for k in msd):
|
| 121 |
+
msd = {k.replace("_orig_mod.", ""): v for k, v in msd.items()}
|
| 122 |
+
res = m.load_state_dict(msd, strict=True)
|
| 123 |
+
print(f"[load] strict OK (missing={len(res.missing_keys)} unexpected={len(res.unexpected_keys)})")
|
| 124 |
+
|
| 125 |
+
m.feedback_alpha.fill_(1.0)
|
| 126 |
+
if hasattr(m, "sl_alpha"):
|
| 127 |
+
m.sl_alpha.fill_(1.0)
|
| 128 |
+
|
| 129 |
+
from transformers import GPT2TokenizerFast
|
| 130 |
+
tok = GPT2TokenizerFast.from_pretrained("gpt2")
|
| 131 |
+
print(f"\n[v4c] generating {args.n_items} items (seed={args.seed})...")
|
| 132 |
+
items = v4c.generate(tok, n=args.n_items, seed=args.seed)
|
| 133 |
+
print(f"[v4c] generated {len(items)} raw items")
|
| 134 |
+
|
| 135 |
+
saved_head = getattr(m, MEMORY_GATE_ATTR)
|
| 136 |
+
zero_head = ZeroLambdaHead().to(device)
|
| 137 |
+
|
| 138 |
+
margin_active = []
|
| 139 |
+
margin_off = []
|
| 140 |
+
pred_correct_active = []
|
| 141 |
+
pred_correct_off = []
|
| 142 |
+
lam_at_ans_active = []
|
| 143 |
+
n_skipped = 0
|
| 144 |
+
skip_reasons = {"neutral_control": 0, "no_cur_stale": 0,
|
| 145 |
+
"bad_length": 0, "same_token": 0}
|
| 146 |
+
|
| 147 |
+
for it_idx, it in enumerate(items):
|
| 148 |
+
if it.family == "neutral_control":
|
| 149 |
+
n_skipped += 1; skip_reasons["neutral_control"] += 1; continue
|
| 150 |
+
if not it.current or not it.stale:
|
| 151 |
+
n_skipped += 1; skip_reasons["no_cur_stale"] += 1; continue
|
| 152 |
+
ol = getattr(it, "original_length", 0)
|
| 153 |
+
if ol <= 1 or ol > 1024:
|
| 154 |
+
n_skipped += 1; skip_reasons["bad_length"] += 1; continue
|
| 155 |
+
cur_t = int(it.ids[it.current[0][0]])
|
| 156 |
+
stale_t = int(it.ids[it.stale[0][0]])
|
| 157 |
+
if cur_t == stale_t:
|
| 158 |
+
n_skipped += 1; skip_reasons["same_token"] += 1; continue
|
| 159 |
+
ans_p = ol - 1
|
| 160 |
+
|
| 161 |
+
ids_b = torch.tensor([it.ids[:ol]], device=device, dtype=torch.long)
|
| 162 |
+
|
| 163 |
+
setattr(m, MEMORY_GATE_ATTR, saved_head)
|
| 164 |
+
with torch.no_grad():
|
| 165 |
+
logits_a = model_forward_logits(m, ids_b)
|
| 166 |
+
cur_lp_a = float(logits_a[0, ans_p, cur_t].item())
|
| 167 |
+
stl_lp_a = float(logits_a[0, ans_p, stale_t].item())
|
| 168 |
+
pred_a = int(logits_a[0, ans_p].argmax().item())
|
| 169 |
+
margin_active.append(cur_lp_a - stl_lp_a)
|
| 170 |
+
pred_correct_active.append(pred_a == cur_t)
|
| 171 |
+
|
| 172 |
+
with torch.no_grad():
|
| 173 |
+
h_C = m.c_stage(ids_b, feedback=None)
|
| 174 |
+
h_E, h_E_sparse = m.e_stage(h_C, feedback=None)
|
| 175 |
+
v_vec, _ = m.salience(h_E) if m.use_salience else (None, None)
|
| 176 |
+
h_D, _ = m.d_stage(h_E, h_E_sparse, v_vec=v_vec)
|
| 177 |
+
lam_logit_a = saved_head(h_D[:, ans_p:ans_p + 1, :])
|
| 178 |
+
lam_a = torch.sigmoid(lam_logit_a).clamp(1e-4, 1.0 - 1e-4)
|
| 179 |
+
lam_at_ans_active.append(float(lam_a.item()))
|
| 180 |
+
|
| 181 |
+
setattr(m, MEMORY_GATE_ATTR, zero_head)
|
| 182 |
+
with torch.no_grad():
|
| 183 |
+
logits_o = model_forward_logits(m, ids_b)
|
| 184 |
+
cur_lp_o = float(logits_o[0, ans_p, cur_t].item())
|
| 185 |
+
stl_lp_o = float(logits_o[0, ans_p, stale_t].item())
|
| 186 |
+
pred_o = int(logits_o[0, ans_p].argmax().item())
|
| 187 |
+
margin_off.append(cur_lp_o - stl_lp_o)
|
| 188 |
+
pred_correct_off.append(pred_o == cur_t)
|
| 189 |
+
|
| 190 |
+
setattr(m, MEMORY_GATE_ATTR, saved_head)
|
| 191 |
+
|
| 192 |
+
n = len(margin_active)
|
| 193 |
+
print(f"\n[items] used={n} skipped={n_skipped} reasons={skip_reasons}")
|
| 194 |
+
if n == 0:
|
| 195 |
+
print("No valid items — aborting.")
|
| 196 |
+
return
|
| 197 |
+
|
| 198 |
+
ma = np.array(margin_active)
|
| 199 |
+
mo = np.array(margin_off)
|
| 200 |
+
delta = ma - mo
|
| 201 |
+
|
| 202 |
+
print(f"\n[λ at answer row, ACTIVE path] "
|
| 203 |
+
f"mean={np.mean(lam_at_ans_active):.4f} "
|
| 204 |
+
f"std={np.std(lam_at_ans_active):.4f} "
|
| 205 |
+
f"min={min(lam_at_ans_active):.4f} "
|
| 206 |
+
f"max={max(lam_at_ans_active):.4f}")
|
| 207 |
+
|
| 208 |
+
print(f"\n[current − stale logit margin at answer position]")
|
| 209 |
+
print(f" ACTIVE: mean={ma.mean():+.3f} std={ma.std():.3f} "
|
| 210 |
+
f"median={np.median(ma):+.3f}")
|
| 211 |
+
print(f" BANK-OFF: mean={mo.mean():+.3f} std={mo.std():.3f} "
|
| 212 |
+
f"median={np.median(mo):+.3f}")
|
| 213 |
+
print(f" Δ (act−off): mean={delta.mean():+.3f} std={delta.std():.3f} "
|
| 214 |
+
f"median={np.median(delta):+.3f}")
|
| 215 |
+
|
| 216 |
+
from scipy import stats as sst
|
| 217 |
+
t_stat, p_val = sst.ttest_rel(ma, mo)
|
| 218 |
+
print(f" paired t = {t_stat:+.3f} p = {p_val:.4f} N={n}")
|
| 219 |
+
d_paired = float(delta.mean() / max(delta.std(), 1e-9))
|
| 220 |
+
print(f" Cohen's d (paired) = {d_paired:+.3f}")
|
| 221 |
+
|
| 222 |
+
acc_a = float(np.mean(pred_correct_active))
|
| 223 |
+
acc_o = float(np.mean(pred_correct_off))
|
| 224 |
+
print(f"\n[top-1 accuracy: argmax(logits[ans_p]) == cur_t]")
|
| 225 |
+
print(f" ACTIVE: {acc_a*100:5.1f}% ({sum(pred_correct_active)}/{n})")
|
| 226 |
+
print(f" BANK-OFF: {acc_o*100:5.1f}% ({sum(pred_correct_off)}/{n})")
|
| 227 |
+
print(f" Δ: {(acc_a-acc_o)*100:+5.1f} pp")
|
| 228 |
+
|
| 229 |
+
print(f"\n[first 10 items — paired breakdown]")
|
| 230 |
+
print(f" {'#':>3} {'m_A':>7} {'m_O':>7} {'Δ':>7} "
|
| 231 |
+
f"{'pred_A':>6} {'pred_O':>6}")
|
| 232 |
+
for i in range(min(10, n)):
|
| 233 |
+
print(f" {i:>3} {ma[i]:>+7.3f} {mo[i]:>+7.3f} {delta[i]:>+7.3f} "
|
| 234 |
+
f"{'OK' if pred_correct_active[i] else 'XX':>6} "
|
| 235 |
+
f"{'OK' if pred_correct_off[i] else 'XX':>6}")
|
| 236 |
+
|
| 237 |
+
print(f"\n{'='*64}")
|
| 238 |
+
print(f"Memory-readout causality — verdict")
|
| 239 |
+
print(f"{'='*64}")
|
| 240 |
+
if t_stat > 2.0 and (acc_a - acc_o) > 0.10:
|
| 241 |
+
verdict = (
|
| 242 |
+
f"ACTIVE > BANK-OFF (t={t_stat:+.2f}, Δacc=+{(acc_a-acc_o)*100:.1f}pp, "
|
| 243 |
+
f"d={d_paired:+.2f}).\n"
|
| 244 |
+
f" → Bank is causally useful for V4c current-vs-stale.\n"
|
| 245 |
+
f" → PROCEED to full probe suite + manuscript update."
|
| 246 |
+
)
|
| 247 |
+
elif abs(t_stat) < 1.0 and abs(acc_a - acc_o) < 0.05:
|
| 248 |
+
verdict = (
|
| 249 |
+
f"ACTIVE ≈ BANK-OFF (t={t_stat:+.2f}, Δacc={(acc_a-acc_o)*100:+.1f}pp, "
|
| 250 |
+
f"d={d_paired:+.2f}).\n"
|
| 251 |
+
f" → λ head routes correctly but BANK READOUT IS NOT CAUSALLY USEFUL.\n"
|
| 252 |
+
f" → If mem_head_bank is already untied and alternate sources still\n"
|
| 253 |
+
f" fail, test direct-source readout before more bank replay."
|
| 254 |
+
)
|
| 255 |
+
elif t_stat < -2.0:
|
| 256 |
+
verdict = (
|
| 257 |
+
f"BANK-OFF > ACTIVE (t={t_stat:+.2f}, Δacc={(acc_a-acc_o)*100:+.1f}pp, "
|
| 258 |
+
f"d={d_paired:+.2f}).\n"
|
| 259 |
+
f" → BANK ACTIVELY HURTS V4c prediction. Diagnose the bank readout\n"
|
| 260 |
+
f" bottleneck before any manuscript work."
|
| 261 |
+
)
|
| 262 |
+
else:
|
| 263 |
+
verdict = (
|
| 264 |
+
f"INTERMEDIATE (t={t_stat:+.2f}, Δacc={(acc_a-acc_o)*100:+.1f}pp, "
|
| 265 |
+
f"d={d_paired:+.2f}).\n"
|
| 266 |
+
f" → Mixed signal. Run full probe suite to see broader pattern; if\n"
|
| 267 |
+
f" contrastive / stale-vs-current probes show clear lift, the bank\n"
|
| 268 |
+
f" is helpful on average even if the V4c-margin metric is noisy."
|
| 269 |
+
)
|
| 270 |
+
print(f" {verdict}")
|
| 271 |
+
print(f"{'='*64}")
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
main()
|
probes/probe_memory_diagnosis.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Memory-readout distributional diagnosis at current-vs-stale answer rows.
|
| 2 |
+
|
| 3 |
+
Runs trunk-only, memory-only, and active paths on the same current-vs-stale
|
| 4 |
+
items. The report shows whether the contextual memory readout decodes the
|
| 5 |
+
current token and how it differs from the trunk distribution.
|
| 6 |
+
|
| 7 |
+
Reports:
|
| 8 |
+
1. Structural check: is `mem_head_bank` a separate parameter from `mem_head`?
|
| 9 |
+
If shared/tied, that's THE diagnostic.
|
| 10 |
+
2. Per-path top-1 distribution: how often does each path's argmax match
|
| 11 |
+
current_t / stale_t / something else?
|
| 12 |
+
3. Mean rank of current_t and stale_t in each path's logit distribution.
|
| 13 |
+
4. Softmax entropy: bank vs trunk (overconfident? uniform? matched?).
|
| 14 |
+
5. First-10-item qualitative dump: bank's top-3 vs trunk's top-3 vs
|
| 15 |
+
current/stale tokens (decoded).
|
| 16 |
+
|
| 17 |
+
Usage:
|
| 18 |
+
python probes/probe_memory_diagnosis.py \
|
| 19 |
+
--checkpoint pytorch_model.bin \
|
| 20 |
+
--sidecar cedl_config.json \
|
| 21 |
+
--n-items 50
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn as nn
|
| 32 |
+
import torch.nn.functional as F
|
| 33 |
+
|
| 34 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 35 |
+
sys.path.insert(0, "/content")
|
| 36 |
+
|
| 37 |
+
import CEDL
|
| 38 |
+
import data_v4c_pairs as v4c
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
MEMORY_GATE_ATTR = "v" + "6_lambda_head"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ConstantLambdaHead(nn.Module):
|
| 45 |
+
def __init__(self, logit_value):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.logit_value = float(logit_value)
|
| 48 |
+
def forward(self, h):
|
| 49 |
+
return torch.full(
|
| 50 |
+
h.shape[:-1] + (1,), self.logit_value,
|
| 51 |
+
device=h.device, dtype=h.dtype,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def load_sidecar_constructor_kwargs(sidecar_path):
|
| 56 |
+
with open(sidecar_path) as f:
|
| 57 |
+
sc = json.load(f)
|
| 58 |
+
mem = sc.get("memory_readout")
|
| 59 |
+
if not isinstance(mem, dict):
|
| 60 |
+
raise ValueError("Expected cedl_config.json with a memory_readout block.")
|
| 61 |
+
source_name = str(mem.get("source", "contextual_memory_state"))
|
| 62 |
+
source_map = {
|
| 63 |
+
"contextual_memory_state": "q_mem",
|
| 64 |
+
"decoder_state": "h_d",
|
| 65 |
+
"expanded_state": "h_e",
|
| 66 |
+
"attractor_state": "q_attractor",
|
| 67 |
+
"q_mem": "q_mem",
|
| 68 |
+
}
|
| 69 |
+
return dict(
|
| 70 |
+
lambda_head=bool(mem.get("lambda_head", True)),
|
| 71 |
+
lambda_head_hidden=int(mem.get("lambda_head_hidden", 160)),
|
| 72 |
+
lambda_head_bias_init=float(mem.get("lambda_head_bias_init", -7.0)),
|
| 73 |
+
lambda_head_w_init_std=float(
|
| 74 |
+
mem.get("lambda_head_w_init_std", 0.05)),
|
| 75 |
+
bce_objective=(
|
| 76 |
+
mem.get("selection_objective") == "binary_answer_background"),
|
| 77 |
+
sel_weight=1.0,
|
| 78 |
+
bg_weight=1.0,
|
| 79 |
+
bg_target=float(mem.get("background_target", 0.01)),
|
| 80 |
+
wt_sparsity_weight=float(mem.get("sparsity_weight", 0.05)),
|
| 81 |
+
wt_sparsity_target=float(mem.get("sparsity_target", 0.05)),
|
| 82 |
+
memory_head_enabled=bool(mem.get("enabled", True)),
|
| 83 |
+
memory_ce_weight=float(mem.get("memory_ce_weight", 1.0)),
|
| 84 |
+
memory_pair_ce_weight=float(mem.get("pair_ce_weight", 5.0)),
|
| 85 |
+
memory_query_source=source_map.get(source_name, source_name),
|
| 86 |
+
memory_readout_mode="direct",
|
| 87 |
+
source_adapter=bool(mem.get("source_adapter", True)),
|
| 88 |
+
context_adapter=bool(mem.get("context_adapter", True)),
|
| 89 |
+
specialist_noinject=bool(mem.get("no_injection", True)),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def model_forward_logits(m, ids_b):
|
| 94 |
+
out = m(ids_b)
|
| 95 |
+
return out[0] if isinstance(out, tuple) else out
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def categorize_top1(top1_t, cur_t, stale_t):
|
| 99 |
+
if top1_t == cur_t: return "current"
|
| 100 |
+
if top1_t == stale_t: return "stale"
|
| 101 |
+
return "other"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def token_decode(tok, token_id):
|
| 105 |
+
s = tok.decode([int(token_id)])
|
| 106 |
+
return repr(s) if len(s) < 16 else repr(s[:13]) + "..."
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def main():
|
| 110 |
+
p = argparse.ArgumentParser()
|
| 111 |
+
p.add_argument("--checkpoint", type=str, default="pytorch_model.bin")
|
| 112 |
+
p.add_argument("--sidecar", type=str, default="cedl_config.json")
|
| 113 |
+
p.add_argument("--n-items", type=int, default=50)
|
| 114 |
+
p.add_argument("--seed", type=int, default=0)
|
| 115 |
+
args = p.parse_args()
|
| 116 |
+
|
| 117 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 118 |
+
print(f"[setup] device={device} ckpt={args.checkpoint}")
|
| 119 |
+
|
| 120 |
+
model_kwargs = load_sidecar_constructor_kwargs(args.sidecar)
|
| 121 |
+
print(f"[setup] memory_source={model_kwargs['memory_query_source']} "
|
| 122 |
+
f"readout_mode={model_kwargs['memory_readout_mode']}")
|
| 123 |
+
m = CEDL.build_model("CEDL", vocab=50257, max_seq=1024, **model_kwargs)
|
| 124 |
+
m = m.to(device).eval()
|
| 125 |
+
|
| 126 |
+
state = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
|
| 127 |
+
msd = state["model"] if isinstance(state, dict) and "model" in state else state
|
| 128 |
+
if any(k.startswith("_orig_mod.") for k in msd):
|
| 129 |
+
msd = {k.replace("_orig_mod.", ""): v for k, v in msd.items()}
|
| 130 |
+
res = m.load_state_dict(msd, strict=True)
|
| 131 |
+
print(f"[load] strict OK (missing={len(res.missing_keys)} unexpected={len(res.unexpected_keys)})")
|
| 132 |
+
|
| 133 |
+
m.feedback_alpha.fill_(1.0)
|
| 134 |
+
if hasattr(m, "sl_alpha"):
|
| 135 |
+
m.sl_alpha.fill_(1.0)
|
| 136 |
+
|
| 137 |
+
print(f"\n{'='*64}")
|
| 138 |
+
print("STRUCTURAL CHECK — bank output projection")
|
| 139 |
+
print(f"{'='*64}")
|
| 140 |
+
has_mh = hasattr(m, "mem_head")
|
| 141 |
+
has_mhb = hasattr(m, "mem_head_bank")
|
| 142 |
+
print(f" m.mem_head present: {has_mh}")
|
| 143 |
+
print(f" m.mem_head_bank present: {has_mhb}")
|
| 144 |
+
if has_mh and has_mhb:
|
| 145 |
+
mh = m.mem_head
|
| 146 |
+
mhb = m.mem_head_bank
|
| 147 |
+
same_obj = mh is mhb
|
| 148 |
+
print(f" m.mem_head is m.mem_head_bank: {same_obj}")
|
| 149 |
+
if not same_obj:
|
| 150 |
+
mh_params = {k: v for k, v in mh.named_parameters()}
|
| 151 |
+
mhb_params = {k: v for k, v in mhb.named_parameters()}
|
| 152 |
+
print(f" mem_head params: {list(mh_params.keys())}")
|
| 153 |
+
print(f" mem_head_bank params: {list(mhb_params.keys())}")
|
| 154 |
+
for k in mh_params:
|
| 155 |
+
if k in mhb_params:
|
| 156 |
+
same_param = mh_params[k] is mhb_params[k]
|
| 157 |
+
equal_val = torch.equal(mh_params[k], mhb_params[k])
|
| 158 |
+
cos = F.cosine_similarity(
|
| 159 |
+
mh_params[k].flatten().unsqueeze(0),
|
| 160 |
+
mhb_params[k].flatten().unsqueeze(0),
|
| 161 |
+
).item()
|
| 162 |
+
print(f" {k}: same_tensor={same_param} equal={equal_val} cos={cos:+.4f}")
|
| 163 |
+
elif has_mh and not has_mhb:
|
| 164 |
+
print(f" → bank uses mem_head directly (NO separate bank output proj).")
|
| 165 |
+
print(f" → bank logits = mem_head(attended_value) → shaped by WikiText LM prior.")
|
| 166 |
+
else:
|
| 167 |
+
bank_keys = [k for k in msd if "bank" in k.lower()]
|
| 168 |
+
print(f" bank-related state_dict keys: {bank_keys}")
|
| 169 |
+
|
| 170 |
+
from transformers import GPT2TokenizerFast
|
| 171 |
+
tok = GPT2TokenizerFast.from_pretrained("gpt2")
|
| 172 |
+
items = v4c.generate(tok, n=args.n_items, seed=args.seed)
|
| 173 |
+
print(f"\n[v4c] generated {len(items)} items")
|
| 174 |
+
|
| 175 |
+
saved_head = getattr(m, MEMORY_GATE_ATTR)
|
| 176 |
+
zero_head = ConstantLambdaHead(-100.0).to(device)
|
| 177 |
+
one_head = ConstantLambdaHead(+100.0).to(device)
|
| 178 |
+
|
| 179 |
+
records = []
|
| 180 |
+
|
| 181 |
+
for it_idx, it in enumerate(items):
|
| 182 |
+
if it.family == "neutral_control": continue
|
| 183 |
+
if not it.current or not it.stale: continue
|
| 184 |
+
ol = getattr(it, "original_length", 0)
|
| 185 |
+
if ol <= 1 or ol > 1024: continue
|
| 186 |
+
cur_t = int(it.ids[it.current[0][0]])
|
| 187 |
+
stale_t = int(it.ids[it.stale[0][0]])
|
| 188 |
+
if cur_t == stale_t: continue
|
| 189 |
+
ans_p = ol - 1
|
| 190 |
+
|
| 191 |
+
ids_b = torch.tensor([it.ids[:ol]], device=device, dtype=torch.long)
|
| 192 |
+
|
| 193 |
+
results = {"item": it_idx, "cur_t": cur_t, "stale_t": stale_t}
|
| 194 |
+
for path, head in [("trunk", zero_head), ("bank", one_head), ("active", saved_head)]:
|
| 195 |
+
setattr(m, MEMORY_GATE_ATTR, head)
|
| 196 |
+
with torch.no_grad():
|
| 197 |
+
logits = model_forward_logits(m, ids_b)
|
| 198 |
+
row = logits[0, ans_p]
|
| 199 |
+
log_probs = F.log_softmax(row, dim=-1)
|
| 200 |
+
top5_p, top5_t = torch.topk(log_probs, k=5)
|
| 201 |
+
ent = float(-(log_probs.exp() * log_probs).sum().item())
|
| 202 |
+
cur_rank = int((log_probs > log_probs[cur_t]).sum().item()) + 1
|
| 203 |
+
stl_rank = int((log_probs > log_probs[stale_t]).sum().item()) + 1
|
| 204 |
+
results[path] = dict(
|
| 205 |
+
top5_t=top5_t.tolist(),
|
| 206 |
+
top5_lp=top5_p.tolist(),
|
| 207 |
+
top1_t=int(top5_t[0].item()),
|
| 208 |
+
ent=ent,
|
| 209 |
+
cur_rank=cur_rank,
|
| 210 |
+
stl_rank=stl_rank,
|
| 211 |
+
cur_lp=float(log_probs[cur_t].item()),
|
| 212 |
+
stl_lp=float(log_probs[stale_t].item()),
|
| 213 |
+
)
|
| 214 |
+
records.append(results)
|
| 215 |
+
|
| 216 |
+
setattr(m, MEMORY_GATE_ATTR, saved_head)
|
| 217 |
+
n = len(records)
|
| 218 |
+
print(f"[used] {n} items\n")
|
| 219 |
+
|
| 220 |
+
if n == 0:
|
| 221 |
+
print("No valid items.")
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
for path in ["trunk", "bank", "active"]:
|
| 225 |
+
top1_cur = sum(1 for r in records if r[path]["top1_t"] == r["cur_t"])
|
| 226 |
+
top1_stl = sum(1 for r in records if r[path]["top1_t"] == r["stale_t"])
|
| 227 |
+
top1_other = n - top1_cur - top1_stl
|
| 228 |
+
ents = np.array([r[path]["ent"] for r in records])
|
| 229 |
+
cur_ranks = np.array([r[path]["cur_rank"] for r in records])
|
| 230 |
+
stl_ranks = np.array([r[path]["stl_rank"] for r in records])
|
| 231 |
+
cur_lps = np.array([r[path]["cur_lp"] for r in records])
|
| 232 |
+
stl_lps = np.array([r[path]["stl_lp"] for r in records])
|
| 233 |
+
print(f"[{path:>6}] top1: cur={top1_cur:>2}/{n} ({top1_cur/n*100:5.1f}%) "
|
| 234 |
+
f"stl={top1_stl:>2}/{n} ({top1_stl/n*100:5.1f}%) "
|
| 235 |
+
f"other={top1_other:>2}/{n} ({top1_other/n*100:5.1f}%)")
|
| 236 |
+
print(f" entropy: mean={ents.mean():.3f} std={ents.std():.3f} "
|
| 237 |
+
f"(uniform~10.83 for V=50257)")
|
| 238 |
+
print(f" cur_rank: mean={cur_ranks.mean():.0f} median={np.median(cur_ranks):.0f} "
|
| 239 |
+
f"min={cur_ranks.min()} max={cur_ranks.max()}")
|
| 240 |
+
print(f" stl_rank: mean={stl_ranks.mean():.0f} median={np.median(stl_ranks):.0f} "
|
| 241 |
+
f"min={stl_ranks.min()} max={stl_ranks.max()}")
|
| 242 |
+
print(f" logp(cur): mean={cur_lps.mean():.3f} logp(stl): mean={stl_lps.mean():.3f}\n")
|
| 243 |
+
|
| 244 |
+
bank_eq_trunk = sum(1 for r in records if r["bank"]["top1_t"] == r["trunk"]["top1_t"])
|
| 245 |
+
bank_in_trunk_top5 = sum(1 for r in records
|
| 246 |
+
if r["bank"]["top1_t"] in r["trunk"]["top5_t"])
|
| 247 |
+
trunk_in_bank_top5 = sum(1 for r in records
|
| 248 |
+
if r["trunk"]["top1_t"] in r["bank"]["top5_t"])
|
| 249 |
+
print(f"[cross-path]")
|
| 250 |
+
print(f" bank top1 == trunk top1: {bank_eq_trunk:>2}/{n} ({bank_eq_trunk/n*100:5.1f}%)")
|
| 251 |
+
print(f" bank top1 ∈ trunk top5: {bank_in_trunk_top5:>2}/{n} ({bank_in_trunk_top5/n*100:5.1f}%)")
|
| 252 |
+
print(f" trunk top1 ∈ bank top5: {trunk_in_bank_top5:>2}/{n} ({trunk_in_bank_top5/n*100:5.1f}%)")
|
| 253 |
+
|
| 254 |
+
print(f"\n[first 10 — qualitative bank vs trunk top-3 at answer position]")
|
| 255 |
+
for i, r in enumerate(records[:10]):
|
| 256 |
+
cur_s = token_decode(tok, r["cur_t"])
|
| 257 |
+
stl_s = token_decode(tok, r["stale_t"])
|
| 258 |
+
print(f"\n --- item {r['item']} cur={cur_s} stale={stl_s} ---")
|
| 259 |
+
for path in ["trunk", "bank", "active"]:
|
| 260 |
+
top3_t = r[path]["top5_t"][:3]
|
| 261 |
+
top3_lp = r[path]["top5_lp"][:3]
|
| 262 |
+
top3_decoded = [(token_decode(tok, t), f"{lp:+.2f}") for t, lp in zip(top3_t, top3_lp)]
|
| 263 |
+
print(f" {path:>6}: top3={top3_decoded} cur_rank={r[path]['cur_rank']} stl_rank={r[path]['stl_rank']}")
|
| 264 |
+
|
| 265 |
+
print(f"\n{'='*64}")
|
| 266 |
+
print("B2 diagnostic verdict")
|
| 267 |
+
print(f"{'='*64}")
|
| 268 |
+
|
| 269 |
+
bank_top1_cur = sum(1 for r in records if r["bank"]["top1_t"] == r["cur_t"]) / n
|
| 270 |
+
trunk_top1_cur = sum(1 for r in records if r["trunk"]["top1_t"] == r["cur_t"]) / n
|
| 271 |
+
bank_mean_ent = float(np.mean([r["bank"]["ent"] for r in records]))
|
| 272 |
+
trunk_mean_ent = float(np.mean([r["trunk"]["ent"] for r in records]))
|
| 273 |
+
bank_cur_rank_mean = float(np.mean([r["bank"]["cur_rank"] for r in records]))
|
| 274 |
+
trunk_cur_rank_mean = float(np.mean([r["trunk"]["cur_rank"] for r in records]))
|
| 275 |
+
readout_mode = str(model_kwargs.get("memory_readout_mode", "direct"))
|
| 276 |
+
|
| 277 |
+
if bank_top1_cur >= trunk_top1_cur and bank_top1_cur >= 0.25:
|
| 278 |
+
print(f" Bank/readout predicts cur token at top-1 {bank_top1_cur*100:.1f}% "
|
| 279 |
+
f"(vs trunk {trunk_top1_cur*100:.1f}%).")
|
| 280 |
+
print(f" Bank/readout ranks cur_t at mean position {bank_cur_rank_mean:.0f} "
|
| 281 |
+
f"(vs trunk {trunk_cur_rank_mean:.0f}).")
|
| 282 |
+
if bank_mean_ent + 1.0 < trunk_mean_ent:
|
| 283 |
+
print(f" Entropy is lower than trunk ({bank_mean_ent:.2f} vs "
|
| 284 |
+
f"{trunk_mean_ent:.2f}); this is overconfident but useful "
|
| 285 |
+
f"when top-1/rank improve.")
|
| 286 |
+
if readout_mode == "direct":
|
| 287 |
+
print(f" → DIRECT READOUT PASSES B2. q_mem is decodable; the old "
|
| 288 |
+
f"256-slot bank path was the bottleneck.")
|
| 289 |
+
else:
|
| 290 |
+
print(f" → BANK READOUT PASSES B2. Proceed to B1 causal attribution.")
|
| 291 |
+
elif bank_top1_cur < 0.5 * trunk_top1_cur and bank_cur_rank_mean > 2 * trunk_cur_rank_mean:
|
| 292 |
+
print(f" Bank predicts cur token at top-1 only {bank_top1_cur*100:.1f}% (vs trunk {trunk_top1_cur*100:.1f}%)")
|
| 293 |
+
print(f" Bank ranks cur_t at mean position {bank_cur_rank_mean:.0f} (vs trunk {trunk_cur_rank_mean:.0f})")
|
| 294 |
+
print(f" → Bank output is task-misaligned. STAGE 2B JUSTIFIED.")
|
| 295 |
+
print(f" → If 'mem_head_bank is mem_head' check above shows TIED weights:")
|
| 296 |
+
print(f" primary fix is to UNTIE mem_head_bank (give bank its own output proj).")
|
| 297 |
+
print(f" → If weights ARE already separate and alternate sources still fail:")
|
| 298 |
+
print(f" test direct-source readout before more bank replay.")
|
| 299 |
+
elif abs(bank_mean_ent - trunk_mean_ent) > 1.0:
|
| 300 |
+
print(f" Bank entropy ({bank_mean_ent:.2f}) differs from trunk ({trunk_mean_ent:.2f}) by {abs(bank_mean_ent-trunk_mean_ent):.2f}.")
|
| 301 |
+
print(f" → Bank distribution is mis-calibrated relative to trunk.")
|
| 302 |
+
else:
|
| 303 |
+
print(f" Bank top-1 cur rate ({bank_top1_cur*100:.1f}%) "
|
| 304 |
+
f"vs trunk ({trunk_top1_cur*100:.1f}%) — diagnostic inconclusive.")
|
| 305 |
+
print(f" Examine qualitative dump above for specific failure mode.")
|
| 306 |
+
print(f"{'='*64}")
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
if __name__ == "__main__":
|
| 310 |
+
main()
|
probes/probe_memory_source_readout.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Probe whether a memory source vector is directly decodable.
|
| 3 |
+
|
| 4 |
+
This diagnostic freezes the trained CEDL checkpoint and trains a temporary
|
| 5 |
+
linear vocab readout from one selected source vector:
|
| 6 |
+
|
| 7 |
+
h_d, h_e, q_attractor, or q_mem
|
| 8 |
+
|
| 9 |
+
It bypasses the external bank bottleneck:
|
| 10 |
+
|
| 11 |
+
source -> direct_head -> vocab
|
| 12 |
+
|
| 13 |
+
instead of:
|
| 14 |
+
|
| 15 |
+
source -> bank_q_proj -> 256-slot mem_keys/mem_vals -> mem_head_bank -> vocab
|
| 16 |
+
|
| 17 |
+
If direct_head generalizes on B2 while the bank path fails, the bottleneck is
|
| 18 |
+
the learned 256-slot bank readout. If direct_head also fails, the selected
|
| 19 |
+
source does not robustly encode the current-token target under this synthetic
|
| 20 |
+
held-out probe.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import sys
|
| 27 |
+
from typing import Dict, List, Tuple
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn as nn
|
| 32 |
+
import torch.nn.functional as F
|
| 33 |
+
|
| 34 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 35 |
+
sys.path.insert(0, "/content")
|
| 36 |
+
|
| 37 |
+
import CEDL
|
| 38 |
+
import data_v4c_pairs as v4c
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
SOURCES = ("h_d", "h_e", "q_attractor", "q_mem")
|
| 42 |
+
MEMORY_GATE_ATTR = "v" + "6_lambda_head"
|
| 43 |
+
STORE_SOURCES_ATTR = "store_v" + "6_bank_sources"
|
| 44 |
+
NEEDS_SOURCES_METHOD = "_v" + "6_needs_dstage_bank_sources"
|
| 45 |
+
SOURCE_INPUT_METHOD = "_v" + "6_bank_query_input"
|
| 46 |
+
|
| 47 |
+
SOURCE_ALIASES = {
|
| 48 |
+
"contextual_memory_state": "q_mem",
|
| 49 |
+
"decoder_state": "h_d",
|
| 50 |
+
"expanded_state": "h_e",
|
| 51 |
+
"attractor_state": "q_attractor",
|
| 52 |
+
"q_mem": "q_mem",
|
| 53 |
+
"h_d": "h_d",
|
| 54 |
+
"h_e": "h_e",
|
| 55 |
+
"q_attractor": "q_attractor",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def load_sidecar_constructor_kwargs(
|
| 60 |
+
sidecar_path: str,
|
| 61 |
+
source: str | None,
|
| 62 |
+
) -> Dict[str, object]:
|
| 63 |
+
with open(sidecar_path) as f:
|
| 64 |
+
sc = json.load(f)
|
| 65 |
+
mem = sc.get("memory_readout")
|
| 66 |
+
if not isinstance(mem, dict):
|
| 67 |
+
raise ValueError("Expected cedl_config.json with a memory_readout block.")
|
| 68 |
+
source_name = str(source or mem.get("source", "contextual_memory_state"))
|
| 69 |
+
return dict(
|
| 70 |
+
lambda_head=bool(mem.get("lambda_head", True)),
|
| 71 |
+
lambda_head_hidden=int(mem.get("lambda_head_hidden", 160)),
|
| 72 |
+
lambda_head_bias_init=float(mem.get("lambda_head_bias_init", -7.0)),
|
| 73 |
+
lambda_head_w_init_std=float(
|
| 74 |
+
mem.get("lambda_head_w_init_std", 0.05)),
|
| 75 |
+
bce_objective=(
|
| 76 |
+
mem.get("selection_objective") == "binary_answer_background"),
|
| 77 |
+
sel_weight=1.0,
|
| 78 |
+
bg_weight=1.0,
|
| 79 |
+
bg_target=float(mem.get("background_target", 0.01)),
|
| 80 |
+
wt_sparsity_weight=float(mem.get("sparsity_weight", 0.05)),
|
| 81 |
+
wt_sparsity_target=float(mem.get("sparsity_target", 0.05)),
|
| 82 |
+
memory_head_enabled=bool(mem.get("enabled", True)),
|
| 83 |
+
memory_ce_weight=float(mem.get("memory_ce_weight", 1.0)),
|
| 84 |
+
memory_pair_ce_weight=float(mem.get("pair_ce_weight", 5.0)),
|
| 85 |
+
memory_query_source=SOURCE_ALIASES.get(source_name, source_name),
|
| 86 |
+
memory_readout_mode="direct",
|
| 87 |
+
source_adapter=bool(mem.get("source_adapter", True)),
|
| 88 |
+
context_adapter=bool(mem.get("context_adapter", True)),
|
| 89 |
+
specialist_noinject=bool(mem.get("no_injection", True)),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def load_model(args, device: torch.device):
|
| 94 |
+
source = None if args.source == "sidecar" else args.source
|
| 95 |
+
model_kwargs = load_sidecar_constructor_kwargs(args.sidecar, source)
|
| 96 |
+
model = CEDL.build_model("CEDL", vocab=50257, max_seq=1024, **model_kwargs)
|
| 97 |
+
model = model.to(device).eval()
|
| 98 |
+
|
| 99 |
+
state = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
|
| 100 |
+
msd = state["model"] if isinstance(state, dict) and "model" in state else state
|
| 101 |
+
if any(k.startswith("_orig_mod.") for k in msd):
|
| 102 |
+
msd = {k.replace("_orig_mod.", ""): v for k, v in msd.items()}
|
| 103 |
+
res = model.load_state_dict(msd, strict=True)
|
| 104 |
+
print(f"[load] strict OK missing={len(res.missing_keys)} "
|
| 105 |
+
f"unexpected={len(res.unexpected_keys)}")
|
| 106 |
+
|
| 107 |
+
model.feedback_alpha.fill_(1.0)
|
| 108 |
+
if hasattr(model, "sl_alpha"):
|
| 109 |
+
model.sl_alpha.fill_(1.0)
|
| 110 |
+
for p in model.parameters():
|
| 111 |
+
p.requires_grad_(False)
|
| 112 |
+
return model
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def make_batch(tokenizer, batch_size: int, max_seq: int, seed: int,
|
| 116 |
+
split: str, hard_collision_frac: float):
|
| 117 |
+
items = v4c.generate(
|
| 118 |
+
tokenizer,
|
| 119 |
+
n=batch_size,
|
| 120 |
+
seed=seed,
|
| 121 |
+
split=split,
|
| 122 |
+
hard_collision_frac=hard_collision_frac,
|
| 123 |
+
family_weights={
|
| 124 |
+
"but_update": 0.50,
|
| 125 |
+
"however_revision": 0.20,
|
| 126 |
+
"temporal_update": 0.15,
|
| 127 |
+
"paraphrased_equiv": 0.05,
|
| 128 |
+
"neutral_control": 0.10,
|
| 129 |
+
},
|
| 130 |
+
)
|
| 131 |
+
return CEDL._pad_v4c_items(items, max_seq), items
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@torch.no_grad()
|
| 135 |
+
def answer_source(model, ids: torch.Tensor, items) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 136 |
+
h_C = model.c_stage(ids, feedback=None)
|
| 137 |
+
h_E, h_E_sparse = model.e_stage(h_C, feedback=None)
|
| 138 |
+
v_vec, _v_scalar = model.salience(h_E)
|
| 139 |
+
|
| 140 |
+
prev = getattr(model.d_stage, STORE_SOURCES_ATTR, False)
|
| 141 |
+
setattr(model.d_stage, STORE_SOURCES_ATTR,
|
| 142 |
+
getattr(model, NEEDS_SOURCES_METHOD)())
|
| 143 |
+
try:
|
| 144 |
+
h_D, _ = model.d_stage(h_E, h_E_sparse, v_vec=v_vec)
|
| 145 |
+
finally:
|
| 146 |
+
setattr(model.d_stage, STORE_SOURCES_ATTR, prev)
|
| 147 |
+
|
| 148 |
+
b_idx, p_idx, cur_tok, stale_tok = CEDL._v4c_collect_answer_quads(ids, items)
|
| 149 |
+
if b_idx.numel() == 0:
|
| 150 |
+
empty = torch.empty(0, model.d_model, device=ids.device)
|
| 151 |
+
return empty, cur_tok, stale_tok
|
| 152 |
+
src = getattr(model, SOURCE_INPUT_METHOD)(h_D, h_E, b_idx, p_idx)
|
| 153 |
+
return src.detach(), cur_tok, stale_tok
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def init_direct_head(model, init_from: str) -> nn.Linear:
|
| 157 |
+
head = nn.Linear(model.d_model, 50257, bias=True).to(next(model.parameters()).device)
|
| 158 |
+
with torch.no_grad():
|
| 159 |
+
if init_from == "mem_head_bank" and hasattr(model, "mem_head_bank"):
|
| 160 |
+
head.weight.copy_(model.mem_head_bank.weight)
|
| 161 |
+
head.bias.copy_(model.mem_head_bank.bias)
|
| 162 |
+
elif init_from == "tok_emb":
|
| 163 |
+
head.weight.copy_(model.c_stage.tok_emb.weight)
|
| 164 |
+
if getattr(model.l_stage.mem_head, "bias", None) is not None:
|
| 165 |
+
head.bias.copy_(model.l_stage.mem_head.bias)
|
| 166 |
+
else:
|
| 167 |
+
head.bias.zero_()
|
| 168 |
+
elif init_from == "random":
|
| 169 |
+
nn.init.normal_(head.weight, std=0.02)
|
| 170 |
+
head.bias.zero_()
|
| 171 |
+
else:
|
| 172 |
+
raise ValueError(f"unknown --init-from {init_from!r}")
|
| 173 |
+
return head
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def row_metrics(logits: torch.Tensor, cur_tok: torch.Tensor, stale_tok: torch.Tensor):
|
| 177 |
+
logp = F.log_softmax(logits, dim=-1)
|
| 178 |
+
top1 = logp.argmax(dim=-1)
|
| 179 |
+
entropy = -(logp.exp() * logp).sum(dim=-1)
|
| 180 |
+
cur_rank = (logp > logp.gather(1, cur_tok[:, None])).sum(dim=1) + 1
|
| 181 |
+
stale_rank = (logp > logp.gather(1, stale_tok[:, None])).sum(dim=1) + 1
|
| 182 |
+
margin = (
|
| 183 |
+
logp.gather(1, cur_tok[:, None]).squeeze(1)
|
| 184 |
+
- logp.gather(1, stale_tok[:, None]).squeeze(1)
|
| 185 |
+
)
|
| 186 |
+
return logp, top1, entropy, cur_rank, stale_rank, margin
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@torch.no_grad()
|
| 190 |
+
def evaluate_replay(model, head, tokenizer, *, n_items: int, batch_size: int,
|
| 191 |
+
max_seq: int, seed: int, split: str) -> Dict[str, float]:
|
| 192 |
+
rows: List[Dict[str, float]] = []
|
| 193 |
+
produced = 0
|
| 194 |
+
batch_seed = seed
|
| 195 |
+
while produced < n_items:
|
| 196 |
+
need = min(batch_size, n_items - produced)
|
| 197 |
+
ids, items = make_batch(
|
| 198 |
+
tokenizer, need, max_seq, batch_seed, split,
|
| 199 |
+
hard_collision_frac=0.0,
|
| 200 |
+
)
|
| 201 |
+
ids = ids.to(next(model.parameters()).device)
|
| 202 |
+
src, cur_tok, stale_tok = answer_source(model, ids, items)
|
| 203 |
+
batch_seed += 1009
|
| 204 |
+
if src.numel() == 0:
|
| 205 |
+
continue
|
| 206 |
+
logits = head(src)
|
| 207 |
+
_logp, top1, entropy, cur_rank, stale_rank, margin = row_metrics(
|
| 208 |
+
logits, cur_tok, stale_tok)
|
| 209 |
+
for i in range(cur_tok.numel()):
|
| 210 |
+
rows.append({
|
| 211 |
+
"top1_cur": float(top1[i].item() == cur_tok[i].item()),
|
| 212 |
+
"top1_stale": float(top1[i].item() == stale_tok[i].item()),
|
| 213 |
+
"entropy": float(entropy[i].item()),
|
| 214 |
+
"cur_rank": float(cur_rank[i].item()),
|
| 215 |
+
"stale_rank": float(stale_rank[i].item()),
|
| 216 |
+
"margin": float(margin[i].item()),
|
| 217 |
+
})
|
| 218 |
+
produced += need
|
| 219 |
+
if not rows:
|
| 220 |
+
raise RuntimeError("evaluation generated no valid V4c answer rows")
|
| 221 |
+
return {
|
| 222 |
+
"n": float(len(rows)),
|
| 223 |
+
"top1_cur": float(np.mean([r["top1_cur"] for r in rows])),
|
| 224 |
+
"top1_stale": float(np.mean([r["top1_stale"] for r in rows])),
|
| 225 |
+
"entropy": float(np.mean([r["entropy"] for r in rows])),
|
| 226 |
+
"cur_rank_mean": float(np.mean([r["cur_rank"] for r in rows])),
|
| 227 |
+
"cur_rank_median": float(np.median([r["cur_rank"] for r in rows])),
|
| 228 |
+
"stale_rank_mean": float(np.mean([r["stale_rank"] for r in rows])),
|
| 229 |
+
"stale_rank_median": float(np.median([r["stale_rank"] for r in rows])),
|
| 230 |
+
"margin": float(np.mean([r["margin"] for r in rows])),
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def print_metrics(label: str, metrics: Dict[str, float]):
|
| 235 |
+
print(f"[{label}] n={int(metrics['n'])} "
|
| 236 |
+
f"top1_cur={metrics['top1_cur'] * 100:5.1f}% "
|
| 237 |
+
f"top1_stale={metrics['top1_stale'] * 100:5.1f}% "
|
| 238 |
+
f"entropy={metrics['entropy']:.3f} "
|
| 239 |
+
f"cur_rank_mean={metrics['cur_rank_mean']:.0f} "
|
| 240 |
+
f"cur_rank_med={metrics['cur_rank_median']:.0f} "
|
| 241 |
+
f"stale_rank_med={metrics['stale_rank_median']:.0f} "
|
| 242 |
+
f"margin={metrics['margin']:+.3f}")
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def token_decode(tok, token_id):
|
| 246 |
+
s = tok.decode([int(token_id)])
|
| 247 |
+
return repr(s) if len(s) < 16 else repr(s[:13]) + "..."
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
class ConstantLambdaHead(nn.Module):
|
| 251 |
+
def __init__(self, logit_value):
|
| 252 |
+
super().__init__()
|
| 253 |
+
self.logit_value = float(logit_value)
|
| 254 |
+
|
| 255 |
+
def forward(self, h):
|
| 256 |
+
return torch.full(
|
| 257 |
+
h.shape[:-1] + (1,), self.logit_value,
|
| 258 |
+
device=h.device, dtype=h.dtype,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def model_forward_logits(m, ids_b):
|
| 263 |
+
out = m(ids_b)
|
| 264 |
+
return out[0] if isinstance(out, tuple) else out
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
@torch.no_grad()
|
| 268 |
+
def evaluate_b2_direct(model, head, tokenizer, *, n_items: int, seed: int):
|
| 269 |
+
device = next(model.parameters()).device
|
| 270 |
+
items = v4c.generate(tokenizer, n=n_items, seed=seed)
|
| 271 |
+
records = []
|
| 272 |
+
saved_head = getattr(model, MEMORY_GATE_ATTR)
|
| 273 |
+
setattr(model, MEMORY_GATE_ATTR, ConstantLambdaHead(-100.0).to(device))
|
| 274 |
+
try:
|
| 275 |
+
for it_idx, it in enumerate(items):
|
| 276 |
+
if it.family == "neutral_control":
|
| 277 |
+
continue
|
| 278 |
+
if not it.current or not it.stale:
|
| 279 |
+
continue
|
| 280 |
+
ol = getattr(it, "original_length", 0)
|
| 281 |
+
if ol <= 1 or ol > 1024:
|
| 282 |
+
continue
|
| 283 |
+
cur_t = int(it.ids[it.current[0][0]])
|
| 284 |
+
stale_t = int(it.ids[it.stale[0][0]])
|
| 285 |
+
if cur_t == stale_t:
|
| 286 |
+
continue
|
| 287 |
+
ans_p = ol - 1
|
| 288 |
+
ids_b = torch.tensor([it.ids[:ol]], device=device, dtype=torch.long)
|
| 289 |
+
|
| 290 |
+
trunk_logits = model_forward_logits(model, ids_b)[0, ans_p]
|
| 291 |
+
src, cur_tok, stale_tok = answer_source(model, ids_b, [it])
|
| 292 |
+
if src.numel() == 0:
|
| 293 |
+
continue
|
| 294 |
+
direct_logits = head(src)[0]
|
| 295 |
+
|
| 296 |
+
results = {"item": it_idx, "cur_t": cur_t, "stale_t": stale_t}
|
| 297 |
+
for path, row in (("trunk", trunk_logits), ("direct", direct_logits)):
|
| 298 |
+
logp = F.log_softmax(row, dim=-1)
|
| 299 |
+
top5_p, top5_t = torch.topk(logp, k=5)
|
| 300 |
+
results[path] = {
|
| 301 |
+
"top5_t": top5_t.tolist(),
|
| 302 |
+
"top5_lp": top5_p.tolist(),
|
| 303 |
+
"top1_t": int(top5_t[0].item()),
|
| 304 |
+
"ent": float(-(logp.exp() * logp).sum().item()),
|
| 305 |
+
"cur_rank": int((logp > logp[cur_t]).sum().item()) + 1,
|
| 306 |
+
"stl_rank": int((logp > logp[stale_t]).sum().item()) + 1,
|
| 307 |
+
"cur_lp": float(logp[cur_t].item()),
|
| 308 |
+
"stl_lp": float(logp[stale_t].item()),
|
| 309 |
+
}
|
| 310 |
+
records.append(results)
|
| 311 |
+
finally:
|
| 312 |
+
setattr(model, MEMORY_GATE_ATTR, saved_head)
|
| 313 |
+
|
| 314 |
+
n = len(records)
|
| 315 |
+
print(f"\n[b2-direct] generated={len(items)} used={n}")
|
| 316 |
+
if n == 0:
|
| 317 |
+
return {}
|
| 318 |
+
|
| 319 |
+
for path in ("trunk", "direct"):
|
| 320 |
+
top1_cur = sum(1 for r in records if r[path]["top1_t"] == r["cur_t"])
|
| 321 |
+
top1_stl = sum(1 for r in records if r[path]["top1_t"] == r["stale_t"])
|
| 322 |
+
top1_other = n - top1_cur - top1_stl
|
| 323 |
+
ents = np.array([r[path]["ent"] for r in records])
|
| 324 |
+
cur_ranks = np.array([r[path]["cur_rank"] for r in records])
|
| 325 |
+
stl_ranks = np.array([r[path]["stl_rank"] for r in records])
|
| 326 |
+
cur_lps = np.array([r[path]["cur_lp"] for r in records])
|
| 327 |
+
stl_lps = np.array([r[path]["stl_lp"] for r in records])
|
| 328 |
+
print(f"[{path:>6}] top1: cur={top1_cur:>2}/{n} ({top1_cur/n*100:5.1f}%) "
|
| 329 |
+
f"stl={top1_stl:>2}/{n} ({top1_stl/n*100:5.1f}%) "
|
| 330 |
+
f"other={top1_other:>2}/{n} ({top1_other/n*100:5.1f}%)")
|
| 331 |
+
print(f" entropy: mean={ents.mean():.3f} std={ents.std():.3f}")
|
| 332 |
+
print(f" cur_rank: mean={cur_ranks.mean():.0f} median={np.median(cur_ranks):.0f} "
|
| 333 |
+
f"min={cur_ranks.min()} max={cur_ranks.max()}")
|
| 334 |
+
print(f" stl_rank: mean={stl_ranks.mean():.0f} median={np.median(stl_ranks):.0f} "
|
| 335 |
+
f"min={stl_ranks.min()} max={stl_ranks.max()}")
|
| 336 |
+
print(f" logp(cur): mean={cur_lps.mean():.3f} logp(stl): mean={stl_lps.mean():.3f}\n")
|
| 337 |
+
|
| 338 |
+
direct_eq_trunk = sum(
|
| 339 |
+
1 for r in records if r["direct"]["top1_t"] == r["trunk"]["top1_t"])
|
| 340 |
+
direct_in_trunk_top5 = sum(
|
| 341 |
+
1 for r in records if r["direct"]["top1_t"] in r["trunk"]["top5_t"])
|
| 342 |
+
trunk_in_direct_top5 = sum(
|
| 343 |
+
1 for r in records if r["trunk"]["top1_t"] in r["direct"]["top5_t"])
|
| 344 |
+
print("[cross-path]")
|
| 345 |
+
print(f" direct top1 == trunk top1: {direct_eq_trunk:>2}/{n} ({direct_eq_trunk/n*100:5.1f}%)")
|
| 346 |
+
print(f" direct top1 in trunk top5: {direct_in_trunk_top5:>2}/{n} ({direct_in_trunk_top5/n*100:5.1f}%)")
|
| 347 |
+
print(f" trunk top1 in direct top5: {trunk_in_direct_top5:>2}/{n} ({trunk_in_direct_top5/n*100:5.1f}%)")
|
| 348 |
+
|
| 349 |
+
print("\n[first 10 - qualitative direct vs trunk top-3 at answer position]")
|
| 350 |
+
for r in records[:10]:
|
| 351 |
+
cur_s = token_decode(tokenizer, r["cur_t"])
|
| 352 |
+
stl_s = token_decode(tokenizer, r["stale_t"])
|
| 353 |
+
print(f"\n --- item {r['item']} cur={cur_s} stale={stl_s} ---")
|
| 354 |
+
for path in ("trunk", "direct"):
|
| 355 |
+
top3_t = r[path]["top5_t"][:3]
|
| 356 |
+
top3_lp = r[path]["top5_lp"][:3]
|
| 357 |
+
top3_decoded = [
|
| 358 |
+
(token_decode(tokenizer, t), f"{lp:+.2f}")
|
| 359 |
+
for t, lp in zip(top3_t, top3_lp)
|
| 360 |
+
]
|
| 361 |
+
print(f" {path:>6}: top3={top3_decoded} "
|
| 362 |
+
f"cur_rank={r[path]['cur_rank']} stl_rank={r[path]['stl_rank']}")
|
| 363 |
+
|
| 364 |
+
direct_top1_cur = sum(
|
| 365 |
+
1 for r in records if r["direct"]["top1_t"] == r["cur_t"]) / n
|
| 366 |
+
trunk_top1_cur = sum(
|
| 367 |
+
1 for r in records if r["trunk"]["top1_t"] == r["cur_t"]) / n
|
| 368 |
+
print(f"\n{'='*64}")
|
| 369 |
+
print("Direct-source readout verdict")
|
| 370 |
+
print(f"{'='*64}")
|
| 371 |
+
if direct_top1_cur >= trunk_top1_cur:
|
| 372 |
+
print(f" DIRECT >= TRUNK ({direct_top1_cur*100:.1f}% vs {trunk_top1_cur*100:.1f}%).")
|
| 373 |
+
print(" -> Source is decodable; 256-slot bank readout is the bottleneck.")
|
| 374 |
+
elif direct_top1_cur >= 0.5 * trunk_top1_cur:
|
| 375 |
+
print(f" DIRECT PARTIAL ({direct_top1_cur*100:.1f}% vs trunk {trunk_top1_cur*100:.1f}%).")
|
| 376 |
+
print(" -> Source has signal, but direct readout is still not robust enough.")
|
| 377 |
+
else:
|
| 378 |
+
print(f" DIRECT FAIL ({direct_top1_cur*100:.1f}% vs trunk {trunk_top1_cur*100:.1f}%).")
|
| 379 |
+
print(" -> Selected source is not robustly decodable under B2.")
|
| 380 |
+
print(f"{'='*64}")
|
| 381 |
+
|
| 382 |
+
return {
|
| 383 |
+
"n": n,
|
| 384 |
+
"direct_top1_cur": direct_top1_cur,
|
| 385 |
+
"trunk_top1_cur": trunk_top1_cur,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def save_outputs(head, args, before, after, b2):
|
| 390 |
+
os.makedirs(args.out_dir, exist_ok=True)
|
| 391 |
+
stem = f"CEDL_direct_source_{args.source}"
|
| 392 |
+
head_path = os.path.join(args.out_dir, f"{stem}_head.pt")
|
| 393 |
+
metrics_path = os.path.join(args.out_dir, f"{stem}_metrics.json")
|
| 394 |
+
torch.save({
|
| 395 |
+
"direct_head": head.state_dict(),
|
| 396 |
+
"checkpoint": args.checkpoint,
|
| 397 |
+
"sidecar": args.sidecar,
|
| 398 |
+
"source": args.source,
|
| 399 |
+
"init_from": args.init_from,
|
| 400 |
+
"steps": args.steps,
|
| 401 |
+
"lr": args.lr,
|
| 402 |
+
"batch_size": args.batch_size,
|
| 403 |
+
"seed": args.seed,
|
| 404 |
+
}, head_path)
|
| 405 |
+
with open(metrics_path, "w") as f:
|
| 406 |
+
json.dump({"before": before, "after": after, "b2": b2}, f, indent=2)
|
| 407 |
+
print(f"[save] direct_head={head_path}")
|
| 408 |
+
print(f"[save] metrics={metrics_path}")
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def main():
|
| 412 |
+
p = argparse.ArgumentParser()
|
| 413 |
+
p.add_argument("--checkpoint", required=True)
|
| 414 |
+
p.add_argument("--sidecar", required=True)
|
| 415 |
+
p.add_argument("--out-dir", default="outputs/memory_source_readout")
|
| 416 |
+
p.add_argument("--source", choices=("sidecar",) + SOURCES, default="sidecar")
|
| 417 |
+
p.add_argument("--init-from", choices=("mem_head_bank", "tok_emb", "random"),
|
| 418 |
+
default="mem_head_bank")
|
| 419 |
+
p.add_argument("--steps", type=int, default=2000)
|
| 420 |
+
p.add_argument("--batch-size", type=int, default=64)
|
| 421 |
+
p.add_argument("--max-seq", type=int, default=128)
|
| 422 |
+
p.add_argument("--lr", type=float, default=5e-4)
|
| 423 |
+
p.add_argument("--weight-decay", type=float, default=0.0)
|
| 424 |
+
p.add_argument("--seed", type=int, default=7234)
|
| 425 |
+
p.add_argument("--split", choices=("all", "train", "test"), default="all")
|
| 426 |
+
p.add_argument("--hard-collision-frac", type=float, default=0.2)
|
| 427 |
+
p.add_argument("--eval-items", type=int, default=256)
|
| 428 |
+
p.add_argument("--b2-items", type=int, default=50)
|
| 429 |
+
p.add_argument("--b2-seed", type=int, default=0)
|
| 430 |
+
p.add_argument("--log-every", type=int, default=100)
|
| 431 |
+
args = p.parse_args()
|
| 432 |
+
|
| 433 |
+
torch.manual_seed(args.seed)
|
| 434 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 435 |
+
print(f"[setup] device={device}")
|
| 436 |
+
print(f"[setup] checkpoint={args.checkpoint}")
|
| 437 |
+
print(f"[setup] sidecar={args.sidecar}")
|
| 438 |
+
print(f"[setup] out_dir={args.out_dir}")
|
| 439 |
+
print(f"[setup] source={args.source}")
|
| 440 |
+
print(f"[setup] init_from={args.init_from}")
|
| 441 |
+
|
| 442 |
+
from transformers import GPT2TokenizerFast
|
| 443 |
+
tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")
|
| 444 |
+
|
| 445 |
+
model = load_model(args, device)
|
| 446 |
+
print(f"[setup] resolved_source={model_kwargs['memory_query_source']}")
|
| 447 |
+
head = init_direct_head(model, args.init_from)
|
| 448 |
+
trainable = sum(p.numel() for p in head.parameters())
|
| 449 |
+
print(f"[freeze] trainable params={trainable:,} (direct_head only)")
|
| 450 |
+
|
| 451 |
+
before = evaluate_replay(
|
| 452 |
+
model, head, tokenizer, n_items=args.eval_items,
|
| 453 |
+
batch_size=args.batch_size, max_seq=args.max_seq,
|
| 454 |
+
seed=args.seed + 17, split=args.split,
|
| 455 |
+
)
|
| 456 |
+
print_metrics("before", before)
|
| 457 |
+
|
| 458 |
+
opt = torch.optim.AdamW(
|
| 459 |
+
head.parameters(), lr=args.lr, weight_decay=args.weight_decay,
|
| 460 |
+
betas=(0.9, 0.95),
|
| 461 |
+
)
|
| 462 |
+
seen_rows = 0
|
| 463 |
+
ema_loss = None
|
| 464 |
+
for step in range(1, args.steps + 1):
|
| 465 |
+
ids, items = make_batch(
|
| 466 |
+
tokenizer, args.batch_size, args.max_seq,
|
| 467 |
+
args.seed + step * 7919, args.split, args.hard_collision_frac,
|
| 468 |
+
)
|
| 469 |
+
ids = ids.to(device)
|
| 470 |
+
src, cur_tok, _stale_tok = answer_source(model, ids, items)
|
| 471 |
+
if src.numel() == 0:
|
| 472 |
+
continue
|
| 473 |
+
logits = head(src)
|
| 474 |
+
loss = F.cross_entropy(logits, cur_tok)
|
| 475 |
+
opt.zero_grad(set_to_none=True)
|
| 476 |
+
loss.backward()
|
| 477 |
+
torch.nn.utils.clip_grad_norm_(head.parameters(), max_norm=1.0)
|
| 478 |
+
opt.step()
|
| 479 |
+
|
| 480 |
+
seen_rows += int(cur_tok.numel())
|
| 481 |
+
loss_val = float(loss.detach().item())
|
| 482 |
+
ema_loss = loss_val if ema_loss is None else 0.98 * ema_loss + 0.02 * loss_val
|
| 483 |
+
if step == 1 or step % args.log_every == 0:
|
| 484 |
+
with torch.no_grad():
|
| 485 |
+
logp = F.log_softmax(logits, dim=-1)
|
| 486 |
+
top1_cur = (logp.argmax(dim=-1) == cur_tok).float().mean().item()
|
| 487 |
+
entropy = (-(logp.exp() * logp).sum(dim=-1)).mean().item()
|
| 488 |
+
print(f"[train] step={step}/{args.steps} rows={seen_rows} "
|
| 489 |
+
f"loss={loss_val:.4f} ema={ema_loss:.4f} "
|
| 490 |
+
f"batch_top1_cur={top1_cur * 100:5.1f}% "
|
| 491 |
+
f"entropy={entropy:.3f}", flush=True)
|
| 492 |
+
|
| 493 |
+
after = evaluate_replay(
|
| 494 |
+
model, head, tokenizer, n_items=args.eval_items,
|
| 495 |
+
batch_size=args.batch_size, max_seq=args.max_seq,
|
| 496 |
+
seed=args.seed + 17, split=args.split,
|
| 497 |
+
)
|
| 498 |
+
print_metrics("after", after)
|
| 499 |
+
b2 = evaluate_b2_direct(
|
| 500 |
+
model, head, tokenizer, n_items=args.b2_items, seed=args.b2_seed)
|
| 501 |
+
save_outputs(head, args, before, after, b2)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
if __name__ == "__main__":
|
| 505 |
+
main()
|
pytorch_model.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e15a3d6dd38a1f85e2a9c4de409ac3cbd7dece4cf202fca05872ebea8180bfbf
|
| 3 |
+
size 545141751
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
transformers
|
| 3 |
+
datasets
|
| 4 |
+
tqdm
|
| 5 |
+
numpy
|
| 6 |
+
scipy
|
| 7 |
+
|