File size: 10,722 Bytes
20a3a74
 
5a8cdaf
20a3a74
 
9b78a90
20a3a74
5a8cdaf
20a3a74
 
43b947d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b78a90
 
43b947d
 
 
 
 
 
20a3a74
 
43b947d
 
 
 
20a3a74
5a8cdaf
43b947d
20a3a74
 
 
 
 
 
 
43b947d
20a3a74
 
 
 
 
 
 
43b947d
20a3a74
9b78a90
20a3a74
43b947d
20a3a74
 
 
 
 
 
 
 
43b947d
20a3a74
9b78a90
20a3a74
 
43b947d
 
20a3a74
 
 
 
 
43b947d
20a3a74
 
43b947d
20a3a74
 
 
43b947d
20a3a74
 
43b947d
20a3a74
 
 
43b947d
 
20a3a74
 
43b947d
20a3a74
43b947d
 
20a3a74
 
43b947d
20a3a74
5a8cdaf
43b947d
20a3a74
 
43b947d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20a3a74
43b947d
 
 
20a3a74
 
43b947d
20a3a74
 
43b947d
 
9b78a90
43b947d
 
9b78a90
43b947d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b78a90
 
43b947d
 
 
 
 
 
 
 
 
20a3a74
 
43b947d
9b78a90
43b947d
 
 
 
 
 
9b78a90
43b947d
9b78a90
 
43b947d
 
9b78a90
43b947d
 
 
 
 
 
 
 
 
 
9b78a90
 
43b947d
 
 
9b78a90
43b947d
9b78a90
43b947d
 
 
 
 
 
 
 
 
 
9b78a90
43b947d
 
 
 
 
 
9b78a90
 
 
43b947d
 
9b78a90
43b947d
 
 
 
 
 
 
 
20a3a74
 
43b947d
 
 
20a3a74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import gradio as gr
import torch
import sys, os, time, hashlib

sys.path.insert(0, "src")
from mulattice.protocol import SynthCfg, build_model, force_fast_scan, logits_at, synth_batch

DEVICE = "cpu"
CFG = SynthCfg()

print("Loading models...", flush=True)
MODELS = {}
for name in ["capacity8k", "forgetting", "realtext"]:
    path = f"checkpoints/{name}.pt"
    if os.path.exists(path):
        ck = torch.load(path, map_location="cpu", weights_only=False)
        c = SynthCfg()
        if ck.get("proto_cfg", {}).get("addr_scale"):
            c.addr_scale = float(ck["proto_cfg"]["addr_scale"])
        m = build_model(c)
        m.load_state_dict(ck["model"])
        m = m.to(DEVICE).eval()
        force_fast_scan(m)
        MODELS[name] = m
        print(f"  {name}.pt loaded", flush=True)
print("All models ready", flush=True)

INK = "#1c1e26"; MUT = "#6b7280"; HAIR = "#e5e7eb"; BG = "#fcfcfa"
ACC = "#2547d0"; OK = "#0e8a5f"; BAD = "#c2413b"; SOFT = "#f4f4f0"; UPD = "#b07807"

with open("demo.css") as f:
    DEMO_CSS = f.read()


def run_custom(text, n1, v1, nv1, n2, v2, nv2):
    m = MODELS.get("forgetting", MODELS.get("capacity8k"))
    seed = int(time.time()) % 100000
    pairs = []
    for n, v, nv in [(n1, v1, nv1), (n2, v2, nv2)]:
        if n.strip() and v.strip():
            pairs.append({"name": n.strip(), "value": v.strip(),
                          "new_value": nv.strip() if nv.strip() else None})
    if not pairs:
        return "Enter at least one fact (a name and a value).", ""

    used = set()
    def h(s, span, off):
        x = int(hashlib.md5(f"{seed}:{s}".encode()).hexdigest(), 16) % span
        while (off + x) in used:
            x = (x + 1) % span
        used.add(off + x)
        return off + x

    kmap, vmap = {}, {}
    for p in pairs:
        kmap[p["name"]] = h("k" + p["name"], CFG.NK, 0)
    for p in pairs:
        vmap[p["value"]] = h("v" + p["value"], CFG.NV, CFG.NK)
        if p["new_value"]:
            vmap[p["new_value"]] = h("v2" + p["new_value"], CFG.NV, CFG.NK)

    words = [w for w in text.split() if w.strip()][:500] or ["..."]
    fid = {w: h("f" + w, CFG.NK, 0) for w in set(words)}
    fill = [fid[w] for w in words]

    store_rows, upd_rows = [], []
    for p in pairs:
        vid = vmap[p["value"]]
        row = dict(p); row["kid"] = kmap[p["name"]]; row["vid"] = vid
        if p["new_value"]:
            row["nid"] = vmap[p["new_value"]]
            upd_rows.append(row)
        store_rows.append(row)

    F = 6; block = F + 2
    n_upd = len(upd_rows); Q = len(store_rows)
    L = (len(store_rows) + n_upd) * block + 2 * Q
    toks = torch.zeros(1, L, dtype=torch.long)

    def take_f(n):
        out = []
        while len(out) < n:
            i = int(torch.randint(0, len(fill), (1,)))
            out.extend(fill[i:i+n-len(out)])
        return out[:n]

    for i, r in enumerate(store_rows):
        s = i * block
        toks[0, s:s+F] = torch.tensor(take_f(F))
        toks[0, s+F], toks[0, s+F+1] = r["kid"], r["vid"]
    for j, r in enumerate(upd_rows):
        s = (len(store_rows) + j) * block
        toks[0, s:s+F] = torch.tensor(take_f(F))
        toks[0, s+F], toks[0, s+F+1] = r["kid"], r["nid"]
    S2 = (len(store_rows) + n_upd) * block
    ans_l = []
    for q, r in enumerate(store_rows):
        toks[0, S2+2*q] = r["kid"]
        toks[0, S2+2*q+1] = CFG.SEP
        ans_l.append(S2 + 2*q + 1)

    t0 = time.time()
    with torch.no_grad():
        pred = logits_at(m, toks, torch.tensor([ans_l])).argmax(-1).cpu()
    dt = time.time() - t0
    cells = m.blocks[0].mixer.scan.last_n_cells

    inv = {v: k for k, v in vmap.items()}
    correct = 0
    rows_html = '<table><tr><th>Asked</th><th>Answered</th><th>Expected</th><th></th></tr>'
    for q, r in enumerate(store_rows):
        got = inv.get(int(pred[0, q]), "?")
        exp = r["new_value"] or r["value"]
        ok = got == exp
        correct += ok
        old = f' <span class="pill">was {r["value"]}</span>' if r["new_value"] else ""
        cls = "y" if ok else "n"
        icon = "✅" if ok else "❌"
        rows_html += f'<tr><td><b>{r["name"]}</b></td><td class="{cls}">{got}</td><td>{exp}{old}</td><td>{icon}</td></tr>'
    rows_html += '</table>'

    stats_html = ('<div class="stats" style="display:grid">'
        f'<div class="stat"><b>{correct}/{len(store_rows)}</b><span>correct</span></div>'
        f'<div class="stat"><b>{L:,}</b><span>words read</span></div>'
        f'<div class="stat"><b>{dt:.1f}s</b><span>time</span></div>'
        f'<div class="stat"><b>{cells:,}</b><span>memory slots</span></div>'
        '</div>')
    return rows_html, stats_html


def run_scale(n_pairs, gap, pct_changed):
    m = MODELS.get("forgetting", MODELS.get("capacity8k"))
    g = torch.Generator().manual_seed(42)
    n = int(n_pairs)
    b = synth_batch(1, n, min(32, n), CFG,
                    filler=int(gap), overwrite_frac=float(pct_changed)/100, generator=g)
    t0 = time.time()
    with torch.no_grad():
        pred = logits_at(m, b["tokens"], b["ans_pos"]).argmax(-1).cpu()
    dt = time.time() - t0
    ok = (pred == b["gold"]).squeeze(0)
    up = b["was_updated"].squeeze(0)
    cells = m.blocks[0].mixer.scan.last_n_cells
    acc = ok.float().mean().item()
    acc_up = ok[up].float().mean().item() if bool(up.any()) else 1.0
    acc_keep = ok[~up].float().mean().item() if bool((~up).any()) else 1.0

    result_html = f'''<div style="text-align:center;padding:28px 0">
        <div style="font-size:48px;font-weight:600;color:{OK};margin-bottom:8px">{acc:.1%}</div>
        <div style="font-size:15px;color:{MUT};margin:6px 0 16px">{int(ok.sum())} of {ok.numel()} recalled correctly</div>
        <div style="font-size:14px;margin:4px 0">Changed facts: <b style="color:{UPD}">{acc_up:.1%}</b> &middot; Unchanged: <b style="color:{OK}">{acc_keep:.1%}</b></div>
    </div>
    <div class="stats" style="display:grid">
        <div class="stat"><b>{b["L"]:,}</b><span>words read</span></div>
        <div class="stat"><b>{cells:,}</b><span>memory slots</span></div>
        <div class="stat"><b>{dt:.1f}s</b><span>time</span></div>
    </div>'''
    return result_html


with gr.Blocks(title="μLattice — AI Memory That Grows Without the Cost Growing",
               css=DEMO_CSS + f"""
.gradio-container {{ max-width: 980px !important; margin: auto !important;
    background: {BG} !important; padding: 0 24px 60px !important;
    font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif !important; }}
footer {{ display: none !important; }}
""") as demo:

    gr.HTML(f"""
    <header>
     <div class="kicker">Tensorbend Research &middot; Eric E. &middot; Live proof &middot; Real weights</div>
     <h1 class="serif">&mu;Lattice &mdash; the growing memory, running</h1>
     <p class="dek serif">A model that reads text and keeps every fact in it &mdash; stores
     associations, replaces them on sight, and answers from memory. Every word costs the
     same, whether the model holds ten facts or eight thousand.</p>
     <div class="meta">3 trained models loaded: capacity8k &middot; forgetting &middot; realtext &middot; 4.2M parameters</div>
    </header>
    <main>
    """)

    # ── Section 01 ──
    gr.HTML("""
    <section>
     <div class="sn"><span class="n">01</span><h2 class="serif">Try it with your own facts</h2></div>
     <p class="lead">Paste any text. Define facts as <em>name &rarr; value</em> pairs.
     Mark any to be <strong>changed mid-stream</strong>. The model reads your words,
     stores the facts, and must return the <em>latest</em> value.</p>
     <div class="fig">
    """)

    with gr.Row():
        with gr.Column(scale=1):
            doc = gr.Textbox(label="Your text (the model reads this)", lines=5,
                value="The harbour was quiet that morning. Gulls circled the masts while the tide went out. A clerk stamped papers without looking up. Somewhere a bell rang twice, then stopped. The road east had been washed out again. Dust settled over the market stalls.")
        with gr.Column(scale=1):
            n1 = gr.Textbox(label="Name", value="the vault password")
            v1 = gr.Textbox(label="Value", value="swordfish")
            nv1 = gr.Textbox(label="Changed to (optional)", value="bluedragon")
            n2 = gr.Textbox(label="Name", value="the meeting time")
            v2 = gr.Textbox(label="Value", value="3pm")
            nv2 = gr.Textbox(label="Changed to (optional)", value="")

    btn1 = gr.Button("Store, overwrite, and answer", variant="primary")
    out1 = gr.HTML()
    stats1 = gr.HTML()

    gr.HTML(f"""
     <div class="figcap">The model reads your words as the stream, stores your facts as it
     passes them, replaces marked ones, then answers from memory. Uses the
     <b>forgetting</b> checkpoint (best at overwrite tasks).</div>
    </div>
    </section>
    """)

    # ── Section 02 ──
    gr.HTML(f"""
    <section>
     <div class="sn"><span class="n">02</span><h2 class="serif">The store at scale</h2></div>
     <p class="lead">The same mechanism under load. Store facts, change some mid-stream,
     then ask for them all back. The cost per word stays flat no matter how many are stored.</p>
     <div class="fig">
    """)

    with gr.Row():
        np_s = gr.Slider(64, 512, value=128, step=64, label="How many facts",
            info="Capped at 512 for CPU (GPU handles 8,192+)")
        gap_s = gr.Slider(0, 14, value=8, step=1, label="Words between each fact")
        ow_s = gr.Slider(0, 100, value=50, step=10, label="% of facts changed mid-stream")

    btn2 = gr.Button("Run the stress test", variant="primary")
    out2 = gr.HTML()

    gr.HTML(f"""
     <div class="figcap">Uses the <b>forgetting</b> checkpoint. On GPU this scales to
     8,192 facts in one 133K-token pass. On free-tier CPU, capped at 512.</div>
    </div>
    </section>
    </main>
    <footer style="max-width:980px;margin:60px auto 0;padding:24px 0;color:{MUT};font-size:13px;border-top:1px solid {HAIR};text-align:center">
     <p>Every answer is produced by a <b>live forward pass</b> of the trained model.</p>
     <p style="margin-top:12px"><a href="https://github.com/eelbaz/mLattice/blob/main/PAPER.md" style="color:{ACC};text-decoration:none">Full paper</a> &middot;
        <a href="https://github.com/eelbaz/mLattice" style="color:{ACC};text-decoration:none">Code &amp; tests</a></p>
     <p style="margin-top:12px;font-size:12px">Tensorbend Research &middot; Eric E. &middot; October 2026</p>
    </footer>
    """)

    btn1.click(run_custom, inputs=[doc, n1, v1, nv1, n2, v2, nv2], outputs=[out1, stats1])
    btn2.click(run_scale, inputs=[np_s, gap_s, ow_s], outputs=[out2])

demo.launch()