Spaces:
Running on Zero
Running on Zero
Sillage: the demo, its memory of paper 1, and the recorded runs
Browse files- .gitattributes +1 -0
- README.md +79 -7
- app.py +454 -0
- build_page.py +291 -0
- build_state.py +37 -0
- capture.py +69 -0
- data.json +119 -0
- demo.gif +3 -0
- index.html +260 -0
- requirements.txt +5 -0
- sample.py +137 -0
- state/cold.pkl +3 -0
- state/index.pkl +3 -0
- state/log.json +12 -0
- state/state.npz +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
demo.gif filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,15 +1,87 @@
|
|
| 1 |
---
|
| 2 |
-
title: Sillage
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
short_description: A frozen GPT-2 given a 4 MB memory. No gradients, no index.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Sillage — a frozen LM that remembers what it reads
|
| 3 |
+
emoji: 🌊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.13.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
short_description: A frozen GPT-2 given a 4 MB memory. No gradients, no index.
|
| 12 |
+
models:
|
| 13 |
+
- openai-community/gpt2
|
| 14 |
+
preload_from_hub:
|
| 15 |
+
- openai-community/gpt2
|
| 16 |
+
tags:
|
| 17 |
+
- associative-memory
|
| 18 |
+
- test-time-learning
|
| 19 |
+
- continual-learning
|
| 20 |
+
- hebbian
|
| 21 |
+
- gradient-free
|
| 22 |
+
- knn-lm
|
| 23 |
+
- hyperdimensional-computing
|
| 24 |
+
- cpu
|
| 25 |
---
|
| 26 |
|
| 27 |
+
# Sillage — the demo
|
| 28 |
+
|
| 29 |
+
A frozen language model reads a document once and gets better at it, with no
|
| 30 |
+
gradients, no fine-tuning and no vector database. Three screens:
|
| 31 |
+
|
| 32 |
+
1. **It has already read a paper.** A memory of paper 1 (8 969 tokens) ships
|
| 33 |
+
with this Space, so the first thing you see is a frozen GPT-2 and a
|
| 34 |
+
remembering GPT-2 completing the same sentence side by side. No wait.
|
| 35 |
+
2. **Give it your own text.** Up to 1 600 tokens, read once. The first half
|
| 36 |
+
builds the memory, the second half is *measured* with it — the papers' own
|
| 37 |
+
dev/test split — and you get the list of predictions the memory corrected.
|
| 38 |
+
3. **Where it does not work.** The regime boundary, in the authors' words.
|
| 39 |
+
|
| 40 |
+
Code, four preprints with DOIs and the reproduction pipeline:
|
| 41 |
+
<https://github.com/riscoss63/sillage> · `pip install sillage` (this Space
|
| 42 |
+
runs the published 1.0.1)
|
| 43 |
+
|
| 44 |
+
## Hardware: ZeroGPU, and why the loop stays on the CPU
|
| 45 |
+
|
| 46 |
+
Hosting a Gradio Space on free `cpu-basic` now requires PRO, but a personal
|
| 47 |
+
account in good standing (verified email, older than 30 days) may host **two
|
| 48 |
+
ZeroGPU Spaces for free**. Pick ZeroGPU in the Space settings — the creation
|
| 49 |
+
form on the website lets you choose it; `hf repos create` does not, which is
|
| 50 |
+
why that command fails with a 402.
|
| 51 |
+
|
| 52 |
+
ZeroGPU quota is charged to the **visitor** (2 minutes a day for an anonymous
|
| 53 |
+
one, 5 for a signed-in free account) for the time spent inside a
|
| 54 |
+
`@spaces.GPU` function. This demo is mostly *not* GPU work: the Hebbian
|
| 55 |
+
mechanism is numpy on the CPU. So the app is split accordingly:
|
| 56 |
+
|
| 57 |
+
* `forward_all` — every frozen forward pass of the document, in one decorated
|
| 58 |
+
call, a couple of seconds of GPU;
|
| 59 |
+
* the read loop — the memory, the adapter, the abstention, all outside the
|
| 60 |
+
decorator, on the CPU, costing the visitor nothing;
|
| 61 |
+
* `compare` / `compare_user` — short generations, decorated, a second or two.
|
| 62 |
+
|
| 63 |
+
Reading a 1 600-token document therefore costs a visitor a few seconds of
|
| 64 |
+
quota rather than half a minute.
|
| 65 |
+
|
| 66 |
+
## What is in this repository
|
| 67 |
+
|
| 68 |
+
| file | role |
|
| 69 |
+
|---|---|
|
| 70 |
+
| `app.py` | the three screens |
|
| 71 |
+
| `sample.py` | two documents to click: an invented operations manual, and paper 1 |
|
| 72 |
+
| `build_state.py` | rebuilds `state/`, the memory the demo starts with |
|
| 73 |
+
| `state/` | 7.1 MB: a GPT-2 memory that has already read paper 1 |
|
| 74 |
+
| `index.html`, `data.json`, `capture.py`, `build_page.py` | a static fallback page built from recorded runs — used only if the Space is switched to `sdk: static`, which is free for everyone with no eligibility rules |
|
| 75 |
+
| `demo.gif` | the two-session terminal recording |
|
| 76 |
+
|
| 77 |
+
## Running it locally
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
pip install sillage gradio
|
| 81 |
+
python build_state.py path/to/a/document # the memory the demo starts with
|
| 82 |
+
python app.py # http://127.0.0.1:7860
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
There is no GPU-specific code path to test: `@spaces.GPU` is a no-op when the
|
| 86 |
+
`spaces` package is absent, and the model goes to `cuda` only when there is
|
| 87 |
+
one.
|
app.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sillage — Hugging Face Space.
|
| 2 |
+
|
| 3 |
+
Three screens, in the order that convinces:
|
| 4 |
+
|
| 5 |
+
1. "It has already read this paper." A memory built at deploy time is
|
| 6 |
+
already loaded, so the very first thing a visitor sees is a frozen model
|
| 7 |
+
and a remembering model completing the same sentence side by side. No
|
| 8 |
+
waiting, no setup, no account.
|
| 9 |
+
2. "Give it your own text." Half a minute, then the same comparison on
|
| 10 |
+
THEIR document, plus the list of predictions the memory actually
|
| 11 |
+
corrected -- the part that turns a number into a fact.
|
| 12 |
+
3. "Where it does not work." The regime boundary, stated by the authors
|
| 13 |
+
before anyone else has to point it out.
|
| 14 |
+
|
| 15 |
+
Everything here runs on `pip install sillage`; the read loop below is the
|
| 16 |
+
library's own, instrumented to report which tokens changed.
|
| 17 |
+
|
| 18 |
+
On ZeroGPU the visitor pays quota only for time spent inside a @spaces.GPU
|
| 19 |
+
function, so the split matters: the frozen forward passes are batched into
|
| 20 |
+
one decorated call, the Hebbian mechanism stays in numpy on the CPU, and the
|
| 21 |
+
suggested prompts of screen 1 are served from runs recorded at build time.
|
| 22 |
+
Looking at the page costs nothing; a click costs a second or two.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import collections
|
| 26 |
+
import io
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
import re
|
| 30 |
+
import shutil
|
| 31 |
+
import tempfile
|
| 32 |
+
|
| 33 |
+
import gradio as gr
|
| 34 |
+
import numpy as np
|
| 35 |
+
import torch
|
| 36 |
+
|
| 37 |
+
from sillage import Sillage
|
| 38 |
+
from sillage.core import CAP
|
| 39 |
+
|
| 40 |
+
try: # ZeroGPU: a real GPU is attached only
|
| 41 |
+
import spaces # inside a decorated function
|
| 42 |
+
GPU = spaces.GPU
|
| 43 |
+
except ImportError: # anywhere else the decorator is a no-op
|
| 44 |
+
def GPU(*args, **kwargs):
|
| 45 |
+
if args and callable(args[0]):
|
| 46 |
+
return args[0]
|
| 47 |
+
return lambda fn: fn
|
| 48 |
+
|
| 49 |
+
import sample
|
| 50 |
+
|
| 51 |
+
STATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "state")
|
| 52 |
+
MAX_TOKENS = 1600 # 160 MB of logits, ~30 s end to end
|
| 53 |
+
MIN_TOKENS = 900 # below this, the split has nothing to say
|
| 54 |
+
WINDOW, STRIDE = 1024, 512
|
| 55 |
+
PAPER = "Sillage (paper 1), 8 969 tokens"
|
| 56 |
+
|
| 57 |
+
# Chosen by running them: each one makes the two columns visibly diverge,
|
| 58 |
+
# and the right-hand column recites the paper rather than inventing.
|
| 59 |
+
EXAMPLE_PROMPTS = [
|
| 60 |
+
"On a 36k-token stream of novel technical text, the memory",
|
| 61 |
+
"At 500k tokens the fixed matrix",
|
| 62 |
+
"surprise gating quadruples the gain of",
|
| 63 |
+
"Every write is gated by the model's own",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# --------------------------------------------------------------- backend ---
|
| 68 |
+
|
| 69 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 70 |
+
memory = Sillage(model="gpt2", state=STATE, device=DEVICE, quiet=True)
|
| 71 |
+
frozen = Sillage(model="gpt2", state=tempfile.mkdtemp(), device=DEVICE,
|
| 72 |
+
quiet=True)
|
| 73 |
+
|
| 74 |
+
# Each visitor who reads a document leaves a 7.4 MB state behind. Keep the
|
| 75 |
+
# most recent few so the follow-up completion still works, and delete the
|
| 76 |
+
# rest: a public Space should not fill its disk with strangers' memories.
|
| 77 |
+
SESSIONS = collections.deque(maxlen=8)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def new_session_dir():
|
| 81 |
+
if len(SESSIONS) == SESSIONS.maxlen:
|
| 82 |
+
shutil.rmtree(SESSIONS[0], ignore_errors=True)
|
| 83 |
+
d = tempfile.mkdtemp(prefix="sillage-session-")
|
| 84 |
+
SESSIONS.append(d)
|
| 85 |
+
return d
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def models():
|
| 89 |
+
"""Load GPT-2 once and let both assistants share the frozen weights."""
|
| 90 |
+
tok, model = memory.load_model()
|
| 91 |
+
frozen._tok, frozen._model = tok, model
|
| 92 |
+
frozen.device = memory.device
|
| 93 |
+
return tok, model
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ZeroGPU asks for the weights to be placed at startup rather than inside a
|
| 97 |
+
# decorated call, where a cold load would eat into the visitor's quota and
|
| 98 |
+
# could outlast the function's duration budget.
|
| 99 |
+
models()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# Screen 1's suggested prompts were run once, at build time, into data.json.
|
| 103 |
+
# Greedy decoding over a fixed memory is deterministic, so a cached answer is
|
| 104 |
+
# the same answer -- and serving it costs the visitor no GPU quota at all.
|
| 105 |
+
# Anything a visitor types instead is computed live.
|
| 106 |
+
CACHED = {}
|
| 107 |
+
try:
|
| 108 |
+
for _c in json.load(io.open(os.path.join(
|
| 109 |
+
os.path.dirname(os.path.abspath(__file__)), "data.json"),
|
| 110 |
+
encoding="utf-8"))["completions"]:
|
| 111 |
+
CACHED[_c["prompt"].strip()] = (
|
| 112 |
+
_c["prompt"] + _c["frozen"], _c["prompt"] + _c["memory"],
|
| 113 |
+
_c["same"])
|
| 114 |
+
except Exception: # no capture yet: everything runs live
|
| 115 |
+
CACHED = {}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def compare(prompt, n_tokens=24):
|
| 119 |
+
"""Screen 1. Recorded prompts are served from cache, the rest is live."""
|
| 120 |
+
hit = CACHED.get((prompt or "").strip())
|
| 121 |
+
if hit:
|
| 122 |
+
return hit[0], hit[1], _verdict(hit[2])
|
| 123 |
+
return compare_live(prompt, n_tokens)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@GPU(duration=30)
|
| 127 |
+
def compare_live(prompt, n_tokens=24):
|
| 128 |
+
"""The decorated path: generation is the only part that needs the model.
|
| 129 |
+
|
| 130 |
+
Twenty-four tokens twice is a second or two of GPU, which is what a
|
| 131 |
+
visitor's daily ZeroGPU quota can afford many times over.
|
| 132 |
+
"""
|
| 133 |
+
return _compare(prompt, n_tokens)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _verdict(same):
|
| 137 |
+
return ("identical here — the memory abstains when it is not confident, "
|
| 138 |
+
"which is the point of the abstention threshold" if same else
|
| 139 |
+
"the two columns diverge: everything after the prompt on the "
|
| 140 |
+
"right comes from what was read, not from GPT-2's weights")
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@GPU(duration=60)
|
| 144 |
+
@torch.no_grad()
|
| 145 |
+
def forward_all(ids):
|
| 146 |
+
"""Every frozen forward pass for a document, in one GPU visit.
|
| 147 |
+
|
| 148 |
+
The Hebbian loop that follows is numpy on the CPU: keeping it outside
|
| 149 |
+
this function is what stops a 30-second read from costing 30 seconds of
|
| 150 |
+
the visitor's GPU quota. Returned in fp16 -- 1600 positions of GPT-2
|
| 151 |
+
logits are 160 MB, and the loop only ever reads them.
|
| 152 |
+
"""
|
| 153 |
+
tok, model = models()
|
| 154 |
+
x = torch.tensor(ids, device=memory.device)
|
| 155 |
+
n = len(ids) - 1
|
| 156 |
+
logits = np.empty((n + 1, model.config.vocab_size), dtype=np.float16)
|
| 157 |
+
hidden = np.empty((n + 1, model.config.n_embd), dtype=np.float16)
|
| 158 |
+
a = 0
|
| 159 |
+
while a < n:
|
| 160 |
+
w = min(WINDOW, len(ids) - a)
|
| 161 |
+
out = model(x[a:a + w].unsqueeze(0), output_hidden_states=True)
|
| 162 |
+
lo = 0 if a == 0 else WINDOW - STRIDE
|
| 163 |
+
logits[a + lo:a + w] = (
|
| 164 |
+
out.logits[0, lo:w].to(torch.float16).cpu().numpy())
|
| 165 |
+
hidden[a + lo:a + w] = (
|
| 166 |
+
out.hidden_states[-1][0, lo:w].to(torch.float16).cpu().numpy())
|
| 167 |
+
if a + w >= len(ids):
|
| 168 |
+
break
|
| 169 |
+
a += STRIDE
|
| 170 |
+
return logits, hidden
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _compare(prompt, n_tokens=24):
|
| 174 |
+
"""The same prompt, decoded greedily, with and without the memory."""
|
| 175 |
+
if not prompt.strip():
|
| 176 |
+
return "", "", ""
|
| 177 |
+
models()
|
| 178 |
+
a = frozen.complete(prompt, n=n_tokens)
|
| 179 |
+
b = memory.complete(prompt, n=n_tokens)
|
| 180 |
+
return prompt + a, prompt + b, _verdict(a.strip() == b.strip())
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def suggest_prompt(text, tail):
|
| 184 |
+
"""Half of a sentence whose opening recurs — so the memory knows the rest.
|
| 185 |
+
|
| 186 |
+
A prompt taken at random usually lands where the memory has nothing to
|
| 187 |
+
say, and the demo falls flat through no fault of the mechanism. Picking a
|
| 188 |
+
sentence whose first words already appeared earlier is the honest way to
|
| 189 |
+
show the memory doing what it is for.
|
| 190 |
+
"""
|
| 191 |
+
best = ""
|
| 192 |
+
for sentence in re.split(r"(?<=[.:])\s+|\n+", tail):
|
| 193 |
+
words = sentence.split()
|
| 194 |
+
if len(words) < 9:
|
| 195 |
+
continue
|
| 196 |
+
if text.count(" ".join(words[:5])) >= 2:
|
| 197 |
+
cut = " ".join(words[:max(5, len(words) // 2)])
|
| 198 |
+
if len(cut) > len(best):
|
| 199 |
+
best = cut
|
| 200 |
+
return best or " ".join(tail.split()[:10])
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def read_and_report(text, progress=gr.Progress()):
|
| 204 |
+
"""Stream a document through a fresh memory, reporting what it changed.
|
| 205 |
+
|
| 206 |
+
This is `sillage.runtime.Sillage.read_text`, unrolled for two reasons:
|
| 207 |
+
the demo collects the positions where the memory turned a wrong
|
| 208 |
+
prediction into a right one, and the frozen forward passes are done in
|
| 209 |
+
one batch beforehand (on the GPU when there is one) so that this loop --
|
| 210 |
+
plain numpy, no model -- costs the visitor nothing. Strictly prequential
|
| 211 |
+
either way: every token is scored before it is written.
|
| 212 |
+
"""
|
| 213 |
+
text = (text or "").strip()
|
| 214 |
+
tok, lm = models()
|
| 215 |
+
state_dir = new_session_dir()
|
| 216 |
+
fresh = Sillage(model="gpt2", state=state_dir, device=memory.device,
|
| 217 |
+
quiet=True)
|
| 218 |
+
fresh._tok, fresh._model = tok, lm
|
| 219 |
+
mem = fresh.mem
|
| 220 |
+
|
| 221 |
+
ids = np.array(tok.encode(text), dtype=np.int64)[:MAX_TOKENS]
|
| 222 |
+
n = len(ids) - 1
|
| 223 |
+
if n < MIN_TOKENS:
|
| 224 |
+
return (None, f"That is {max(n, 0)} tokens. Below about "
|
| 225 |
+
f"{MIN_TOKENS} there is nothing to measure honestly: the "
|
| 226 |
+
f"first half of a document is what builds the memory, and "
|
| 227 |
+
f"the abstention threshold needs a few hundred observations "
|
| 228 |
+
f"before it will let the memory speak at all. Paste a longer "
|
| 229 |
+
f"one — or click an example below.", None, "", "")
|
| 230 |
+
dev_end = n // 2 # first half: the memory reads and calibrates
|
| 231 |
+
mem.new_stream()
|
| 232 |
+
thrG = thrS = np.inf # silent until the dev half is behind us
|
| 233 |
+
nll_b = nll_f = nll_m = 0.0
|
| 234 |
+
cnt = active = 0
|
| 235 |
+
fixes = []
|
| 236 |
+
logits, hidden = forward_all(ids) # the only GPU visit
|
| 237 |
+
for j in range(n):
|
| 238 |
+
if j == dev_end: # the papers' protocol: dev decides, test tells
|
| 239 |
+
thrG, thrS = mem.thresholds()
|
| 240 |
+
truth = int(ids[j + 1])
|
| 241 |
+
lb = logits[j].astype(np.float32)
|
| 242 |
+
mx = lb.max()
|
| 243 |
+
lpb = lb - (mx + np.log(np.exp(lb - mx).sum()))
|
| 244 |
+
lp = float(lpb[truth])
|
| 245 |
+
la, phi = mem.adapt(lb, hidden[j].astype(np.float32))
|
| 246 |
+
m2 = la.max()
|
| 247 |
+
p_ad = np.exp(la - m2)
|
| 248 |
+
p_ad /= p_ad.sum()
|
| 249 |
+
lp_f = float(np.log(max(p_ad[truth], 1e-30)))
|
| 250 |
+
qG = mem.step_key(int(ids[j]))
|
| 251 |
+
uG, sG = mem.scores(mem.M, qG)
|
| 252 |
+
mem.res_G.append(float(sG.max()))
|
| 253 |
+
pc = mem.cold_lookup(truth)
|
| 254 |
+
p_true = mem.mix_true(np.exp(lp_f), sG, truth, None, pc, thrG, thrS)
|
| 255 |
+
speaks = float(sG.max()) >= thrG
|
| 256 |
+
if j >= dev_end:
|
| 257 |
+
active += speaks
|
| 258 |
+
nll_b += -lp
|
| 259 |
+
nll_f += -lp_f
|
| 260 |
+
nll_m += -np.log(max(p_true, 1e-30))
|
| 261 |
+
cnt += 1
|
| 262 |
+
if speaks and len(fixes) < 12:
|
| 263 |
+
# mix_true takes the probability of one token, mix_full takes the
|
| 264 |
+
# whole distribution -- so the cold tier is asked twice, once for
|
| 265 |
+
# each shape
|
| 266 |
+
full = mem.mix_full(p_ad.copy(), sG, None, mem.cold_lookup(),
|
| 267 |
+
thrG, thrS)
|
| 268 |
+
said, now = int(np.argmax(p_ad)), int(np.argmax(full))
|
| 269 |
+
if now == truth != said:
|
| 270 |
+
ctx = tok.decode(ids[max(0, j - 9):j + 1])
|
| 271 |
+
fixes.append([" ".join(ctx.split())[-58:],
|
| 272 |
+
repr(tok.decode([truth])),
|
| 273 |
+
repr(tok.decode([said]))])
|
| 274 |
+
g = min(CAP, max(0.0, -lp))
|
| 275 |
+
mem.write_all(qG, uG, None, None, truth, g, phi, p_ad)
|
| 276 |
+
if j % 128 == 0:
|
| 277 |
+
progress(j / n, desc=f"reading {j}/{n} tokens")
|
| 278 |
+
fresh.index.add(text, "your document")
|
| 279 |
+
fresh.save()
|
| 280 |
+
|
| 281 |
+
ppl = [float(np.exp(v / cnt)) for v in (nll_b, nll_f, nll_m)]
|
| 282 |
+
plot = {"frozen GPT-2": ppl[0], "+ fast weights": ppl[1],
|
| 283 |
+
"+ memory": ppl[2]}
|
| 284 |
+
knn = n * 1536 * 4 / 1e6 # one 1536-float key+value per token
|
| 285 |
+
summary = (
|
| 286 |
+
f"**Read once, left to right: {dev_end} tokens built the memory, the "
|
| 287 |
+
f"next {cnt} were measured with it.** That split is the papers' own "
|
| 288 |
+
f"protocol — the numbers below are on text the memory had not seen "
|
| 289 |
+
f"when it scored them, and every token was scored *before* being "
|
| 290 |
+
f"written.\n\n"
|
| 291 |
+
f"Perplexity on that second half: {ppl[0]:.2f} frozen → "
|
| 292 |
+
f"{ppl[1]:.2f} with the rank-16 adapter → **{ppl[2]:.2f}** with the "
|
| 293 |
+
f"memory on top — **{100 * (1 - ppl[2] / ppl[0]):.0f}% lower**. The "
|
| 294 |
+
f"memory spoke on {100 * active / cnt:.0f}% of those positions and "
|
| 295 |
+
f"kept quiet on the rest; abstaining when it has nothing to say is "
|
| 296 |
+
f"what keeps it from doing harm.\n\n"
|
| 297 |
+
f"State on disk: **7.4 MB**, and it would still be 7.4 MB after a "
|
| 298 |
+
f"million tokens. A kNN-LM datastore over the same text would "
|
| 299 |
+
f"already hold about {knn:.1f} MB, and would keep growing.")
|
| 300 |
+
return (plot, summary, fixes or None, state_dir,
|
| 301 |
+
suggest_prompt(text, tok.decode(ids[dev_end:])))
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def read_then_compare(text, progress=gr.Progress()):
|
| 305 |
+
"""Read, report, and hand back a prompt worth trying plus the new state."""
|
| 306 |
+
plot, summary, fixes, state_dir, suggestion = read_and_report(text,
|
| 307 |
+
progress)
|
| 308 |
+
return (plot, summary, fixes, state_dir,
|
| 309 |
+
gr.update(value=suggestion, visible=bool(state_dir)),
|
| 310 |
+
gr.update(visible=bool(state_dir)))
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@GPU(duration=30)
|
| 314 |
+
def compare_user(prompt, state_dir, n_tokens=12):
|
| 315 |
+
"""The same comparison, against the memory the visitor just built."""
|
| 316 |
+
if not state_dir or not os.path.exists(os.path.join(state_dir,
|
| 317 |
+
"state.npz")):
|
| 318 |
+
return "", "", ("That session's memory has been cleared — read a "
|
| 319 |
+
"document again.")
|
| 320 |
+
if not (prompt or "").strip():
|
| 321 |
+
return "", "", ""
|
| 322 |
+
tok, model = models()
|
| 323 |
+
theirs = Sillage(model="gpt2", state=state_dir, device=memory.device,
|
| 324 |
+
quiet=True)
|
| 325 |
+
theirs._tok, theirs._model = tok, model
|
| 326 |
+
a = frozen.complete(prompt, n=n_tokens)
|
| 327 |
+
b = theirs.complete(prompt, n=n_tokens)
|
| 328 |
+
note = ("identical here — try a phrase that recurs in your document, "
|
| 329 |
+
"the memory only speaks where it is confident"
|
| 330 |
+
if a.strip() == b.strip() else
|
| 331 |
+
"the right-hand column comes from your document, not from "
|
| 332 |
+
"GPT-2's weights")
|
| 333 |
+
return prompt + a, prompt + b, note
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# ------------------------------------------------------------------ ui -----
|
| 337 |
+
|
| 338 |
+
CSS = """
|
| 339 |
+
.hero {text-align:center}
|
| 340 |
+
.small {font-size:0.9em; opacity:0.75}
|
| 341 |
+
footer {visibility:hidden}
|
| 342 |
+
"""
|
| 343 |
+
|
| 344 |
+
with gr.Blocks(title="Sillage: a frozen LM that remembers "
|
| 345 |
+
"what it reads") as demo:
|
| 346 |
+
gr.Markdown(
|
| 347 |
+
"# Sillage\n"
|
| 348 |
+
"### A frozen language model that remembers what it reads — "
|
| 349 |
+
"4.2 MB, no gradients, no fine-tuning, no vector database.\n"
|
| 350 |
+
"[]"
|
| 351 |
+
"(https://pypi.org/project/sillage/) "
|
| 352 |
+
"[]"
|
| 353 |
+
"(https://github.com/riscoss63/sillage) "
|
| 354 |
+
"[]"
|
| 355 |
+
"(https://doi.org/10.5281/zenodo.22079016)",
|
| 356 |
+
elem_classes="hero")
|
| 357 |
+
gr.Markdown(
|
| 358 |
+
"> This Space runs **GPT-2 124M on a free CPU**. Its prose is weak in "
|
| 359 |
+
"absolute terms — that is not what is on display. What is on display "
|
| 360 |
+
"is the **difference** between the two columns, and it comes from a "
|
| 361 |
+
"4.2 MB matrix written while reading, with no gradient anywhere.",
|
| 362 |
+
elem_classes="small")
|
| 363 |
+
|
| 364 |
+
with gr.Tab("1 · It has already read a paper"):
|
| 365 |
+
gr.Markdown(
|
| 366 |
+
f"The memory loaded here has read **{PAPER}** — the paper that "
|
| 367 |
+
"describes this very mechanism. GPT-2 has never seen that text. "
|
| 368 |
+
"Complete a sentence from it and watch the right-hand column "
|
| 369 |
+
"recall what the left one cannot know.")
|
| 370 |
+
prompt = gr.Textbox(label="Beginning of a sentence",
|
| 371 |
+
value=EXAMPLE_PROMPTS[0], lines=2)
|
| 372 |
+
gr.Examples(examples=[[p] for p in EXAMPLE_PROMPTS], inputs=prompt,
|
| 373 |
+
label="Try one of these")
|
| 374 |
+
go = gr.Button("Complete both", variant="primary")
|
| 375 |
+
with gr.Row():
|
| 376 |
+
out_frozen = gr.Textbox(label="GPT-2, frozen", lines=6)
|
| 377 |
+
out_memory = gr.Textbox(label="GPT-2 + Sillage memory", lines=6)
|
| 378 |
+
verdict = gr.Markdown(elem_classes="small")
|
| 379 |
+
go.click(compare, prompt, [out_frozen, out_memory, verdict])
|
| 380 |
+
demo.load(compare, prompt, [out_frozen, out_memory, verdict])
|
| 381 |
+
|
| 382 |
+
with gr.Tab("2 · Give it your own text"):
|
| 383 |
+
gr.Markdown(
|
| 384 |
+
f"Paste something GPT-2 has never seen: your notes, an internal "
|
| 385 |
+
f"document, a README, a specification. It is read **once**, left "
|
| 386 |
+
f"to right, and every token is scored *before* it is memorised — "
|
| 387 |
+
f"so the numbers below are honest online measurements, not a "
|
| 388 |
+
f"replay. Capped at {MAX_TOKENS} tokens (about 90 seconds on this "
|
| 389 |
+
f"free CPU).")
|
| 390 |
+
user_text = gr.Textbox(label="Your document", lines=12,
|
| 391 |
+
value=sample.MANUAL)
|
| 392 |
+
gr.Examples(
|
| 393 |
+
examples=[[sample.MANUAL], [sample.paper_excerpt()]],
|
| 394 |
+
inputs=user_text, example_labels=[
|
| 395 |
+
"An invented operations manual (GPT-2 cannot know it)",
|
| 396 |
+
"The opening of paper 1 (novel technical prose)"],
|
| 397 |
+
label="Or start from one of these")
|
| 398 |
+
read_btn = gr.Button("Read it", variant="primary")
|
| 399 |
+
chart = gr.Label(label="Perplexity, lower is better")
|
| 400 |
+
report = gr.Markdown()
|
| 401 |
+
fixed = gr.Dataframe(
|
| 402 |
+
headers=["context", "what came next", "what frozen GPT-2 said"],
|
| 403 |
+
label="Predictions the memory corrected — the frozen model had "
|
| 404 |
+
"no way of knowing these", wrap=True)
|
| 405 |
+
|
| 406 |
+
gr.Markdown("### Now ask it to continue a sentence from *your* text")
|
| 407 |
+
session = gr.State("")
|
| 408 |
+
your_prompt = gr.Textbox(label="Beginning of a sentence", lines=2,
|
| 409 |
+
visible=False)
|
| 410 |
+
your_btn = gr.Button("Complete both", variant="primary",
|
| 411 |
+
visible=False)
|
| 412 |
+
with gr.Row():
|
| 413 |
+
your_frozen = gr.Textbox(label="GPT-2, frozen", lines=5)
|
| 414 |
+
your_memory = gr.Textbox(label="GPT-2 + the memory you just "
|
| 415 |
+
"built", lines=5)
|
| 416 |
+
your_note = gr.Markdown(elem_classes="small")
|
| 417 |
+
read_btn.click(read_then_compare, user_text,
|
| 418 |
+
[chart, report, fixed, session, your_prompt, your_btn])
|
| 419 |
+
your_btn.click(compare_user, [your_prompt, session],
|
| 420 |
+
[your_frozen, your_memory, your_note])
|
| 421 |
+
|
| 422 |
+
with gr.Tab("3 · Where it does not work"):
|
| 423 |
+
gr.Markdown(
|
| 424 |
+
"**The regime matters, and it is measured.**\n\n"
|
| 425 |
+
"| system | perplexity | memory used |\n|---|---|---|\n"
|
| 426 |
+
"| frozen GPT-2 | 31.2 | 0 |\n"
|
| 427 |
+
"| + RAG-style retrieve & rescore | 29.9 | corpus + index |\n"
|
| 428 |
+
"| + kNN-LM, unbounded store | 23.6 | 55 MB, grows forever |\n"
|
| 429 |
+
"| + **this memory** (fixed) | **19.2** | **4.2 MB, constant** |\n"
|
| 430 |
+
"| + memory and fast weights | **16.6** | 7.4 MB, constant |\n\n"
|
| 431 |
+
"That is on 36k tokens of novel technical text. **On long, "
|
| 432 |
+
"low-repetition narrative, an unbounded kNN-LM still wins** "
|
| 433 |
+
"(+0.048 vs +0.007 nats): this memory captures verbatim "
|
| 434 |
+
"recurrence, not paraphrase. The boundary is mapped in the "
|
| 435 |
+
"papers rather than hidden.\n\n"
|
| 436 |
+
"Three results that did *not* work are published too: hidden "
|
| 437 |
+
"states make poor Hebbian keys; surprise gating helps the memory "
|
| 438 |
+
"and *hurts* the fast-weight adapter; and calibrating the readout "
|
| 439 |
+
"on your own stream loses to a proper tuning, because the "
|
| 440 |
+
"calibration window is read by a colder memory than the one it "
|
| 441 |
+
"will govern.\n\n"
|
| 442 |
+
"---\n\n"
|
| 443 |
+
"**Run it yourself, on any causal LM:**\n\n"
|
| 444 |
+
"```bash\npip install sillage\nsillage read notes.md\n"
|
| 445 |
+
"sillage ask \"what did the report say?\"\n"
|
| 446 |
+
"sillage complete \"The report said\"\n```\n\n"
|
| 447 |
+
"Four preprints with DOIs, the full reproduction pipeline and "
|
| 448 |
+
"every number as committed JSON: "
|
| 449 |
+
"[github.com/riscoss63/sillage](https://github.com/riscoss63/sillage)")
|
| 450 |
+
|
| 451 |
+
if __name__ == "__main__":
|
| 452 |
+
# one reading at a time: the whole point is a single CPU
|
| 453 |
+
demo.queue(default_concurrency_limit=1).launch(
|
| 454 |
+
theme=gr.themes.Soft(), css=CSS)
|
build_page.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build index.html for the static Space from the captured runs.
|
| 2 |
+
|
| 3 |
+
A static Space has no backend, so the page carries its evidence with it: the
|
| 4 |
+
outputs in data.json are inlined, and the page says where they came from.
|
| 5 |
+
Nothing here is illustrative -- every string was produced by the tool.
|
| 6 |
+
|
| 7 |
+
python capture.py && python build_page.py
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import io
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
|
| 16 |
+
PAGE = """<!doctype html>
|
| 17 |
+
<html lang="en">
|
| 18 |
+
<head>
|
| 19 |
+
<meta charset="utf-8">
|
| 20 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 21 |
+
<title>Sillage — a frozen LM that remembers what it reads</title>
|
| 22 |
+
<style>
|
| 23 |
+
:root {
|
| 24 |
+
--bg:#0d1117; --panel:#161b22; --line:#26303d; --ink:#c9d1d9;
|
| 25 |
+
--dim:#8b949e; --hl:#79c0ff; --warm:#ffc457; --ok:#3fb950;
|
| 26 |
+
}
|
| 27 |
+
* { box-sizing:border-box }
|
| 28 |
+
body {
|
| 29 |
+
margin:0; background:var(--bg); color:var(--ink); line-height:1.6;
|
| 30 |
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,
|
| 31 |
+
Helvetica,Arial,sans-serif;
|
| 32 |
+
}
|
| 33 |
+
.wrap { max-width:980px; margin:0 auto; padding:0 20px 80px }
|
| 34 |
+
header { text-align:center; padding:56px 0 24px }
|
| 35 |
+
h1 { font-size:2.6rem; margin:0 0 10px; letter-spacing:-.02em }
|
| 36 |
+
.tag { font-size:1.15rem; color:var(--ink); max-width:640px; margin:0 auto }
|
| 37 |
+
.etym { color:var(--dim); font-style:italic; font-size:.95rem;
|
| 38 |
+
max-width:560px; margin:18px auto 0 }
|
| 39 |
+
.badges { margin:22px 0 8px; display:flex; gap:8px; justify-content:center;
|
| 40 |
+
flex-wrap:wrap }
|
| 41 |
+
.badges img { height:20px }
|
| 42 |
+
h2 { font-size:1.5rem; margin:52px 0 6px; letter-spacing:-.01em }
|
| 43 |
+
h2 .num { color:var(--dim); font-weight:400; margin-right:8px }
|
| 44 |
+
.lede { color:var(--dim); margin:0 0 20px }
|
| 45 |
+
.gif { width:100%; border:1px solid var(--line); border-radius:10px;
|
| 46 |
+
display:block; margin:26px 0 }
|
| 47 |
+
.chips { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px }
|
| 48 |
+
.chip {
|
| 49 |
+
background:var(--panel); border:1px solid var(--line); color:var(--ink);
|
| 50 |
+
border-radius:999px; padding:7px 14px; cursor:pointer; font-size:.9rem;
|
| 51 |
+
font-family:inherit; transition:border-color .15s, color .15s;
|
| 52 |
+
}
|
| 53 |
+
.chip:hover { border-color:var(--hl); color:var(--hl) }
|
| 54 |
+
.chip[aria-pressed="true"] { border-color:var(--hl); color:var(--hl);
|
| 55 |
+
background:#16273b }
|
| 56 |
+
.cols { display:grid; grid-template-columns:1fr 1fr; gap:14px }
|
| 57 |
+
@media (max-width:760px) { .cols { grid-template-columns:1fr } }
|
| 58 |
+
.col { background:var(--panel); border:1px solid var(--line);
|
| 59 |
+
border-radius:10px; padding:14px 16px; min-height:170px }
|
| 60 |
+
.col h3 { margin:0 0 10px; font-size:.85rem; text-transform:uppercase;
|
| 61 |
+
letter-spacing:.08em; color:var(--dim); font-weight:600 }
|
| 62 |
+
.col.win h3 { color:var(--ok) }
|
| 63 |
+
.out { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 64 |
+
font-size:.9rem; white-space:pre-wrap; word-break:break-word; margin:0 }
|
| 65 |
+
.out .p { color:var(--dim) }
|
| 66 |
+
.out .new { color:var(--warm) }
|
| 67 |
+
.col.win .out .new { color:var(--hl) }
|
| 68 |
+
.note { color:var(--dim); font-size:.9rem; margin:12px 0 0 }
|
| 69 |
+
table { width:100%; border-collapse:collapse; margin:18px 0; font-size:.92rem }
|
| 70 |
+
th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line) }
|
| 71 |
+
th { color:var(--dim); font-weight:600; font-size:.82rem;
|
| 72 |
+
text-transform:uppercase; letter-spacing:.06em }
|
| 73 |
+
td.mono,th.mono { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 74 |
+
font-size:.86rem }
|
| 75 |
+
tr.best td { color:var(--hl); font-weight:600 }
|
| 76 |
+
.panel { background:var(--panel); border:1px solid var(--line);
|
| 77 |
+
border-radius:10px; padding:18px 20px; margin:18px 0 }
|
| 78 |
+
.panel p:first-child { margin-top:0 }
|
| 79 |
+
.panel p:last-child { margin-bottom:0 }
|
| 80 |
+
pre.sh { background:var(--panel); border:1px solid var(--line);
|
| 81 |
+
border-radius:10px; padding:16px 18px; overflow-x:auto;
|
| 82 |
+
font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 83 |
+
font-size:.9rem; color:var(--ink) }
|
| 84 |
+
pre.sh .c { color:var(--dim) }
|
| 85 |
+
a { color:var(--hl) }
|
| 86 |
+
footer { color:var(--dim); font-size:.9rem; text-align:center;
|
| 87 |
+
margin-top:64px; border-top:1px solid var(--line); padding-top:24px }
|
| 88 |
+
.kbd { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 89 |
+
background:#1c2432; border:1px solid var(--line); border-radius:5px;
|
| 90 |
+
padding:1px 6px; font-size:.85em }
|
| 91 |
+
</style>
|
| 92 |
+
</head>
|
| 93 |
+
<body>
|
| 94 |
+
<div class="wrap">
|
| 95 |
+
|
| 96 |
+
<header>
|
| 97 |
+
<h1>Sillage</h1>
|
| 98 |
+
<p class="tag">A frozen language model that <b>remembers what it reads</b> —
|
| 99 |
+
4.2 MB, no gradients, no fine-tuning, no vector database.</p>
|
| 100 |
+
<p class="etym">sillage (n., French) — the trace left behind by something
|
| 101 |
+
that has passed: a ship's wake, a scent in a room. What a model keeps of
|
| 102 |
+
what it read.</p>
|
| 103 |
+
<div class="badges">
|
| 104 |
+
<a href="https://pypi.org/project/sillage/"><img alt="PyPI"
|
| 105 |
+
src="https://img.shields.io/pypi/v/sillage.svg"></a>
|
| 106 |
+
<a href="https://github.com/riscoss63/sillage"><img alt="GitHub"
|
| 107 |
+
src="https://img.shields.io/badge/code-GitHub-181717?logo=github"></a>
|
| 108 |
+
<a href="https://doi.org/10.5281/zenodo.22079016"><img alt="DOI"
|
| 109 |
+
src="https://zenodo.org/badge/DOI/10.5281/zenodo.22079016.svg"></a>
|
| 110 |
+
<a href="https://github.com/riscoss63/sillage/blob/main/LICENSE"><img
|
| 111 |
+
alt="MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
|
| 112 |
+
</div>
|
| 113 |
+
</header>
|
| 114 |
+
|
| 115 |
+
<img class="gif" src="demo.gif" alt="Two sessions a day apart: the model
|
| 116 |
+
reads a draft on Monday and halves the perplexity of the next draft on
|
| 117 |
+
Tuesday, then completes a sentence it can only know from what it read.">
|
| 118 |
+
|
| 119 |
+
<h2><span class="num">1</span>It has already read a paper</h2>
|
| 120 |
+
<p class="lede">The memory below has read
|
| 121 |
+
<b id="read-what"></b> — the paper describing this very mechanism, which
|
| 122 |
+
GPT-2 has never seen. Same prompt, same greedy decoding, one column with the
|
| 123 |
+
memory and one without. Pick a beginning:</p>
|
| 124 |
+
|
| 125 |
+
<div class="chips" id="chips"></div>
|
| 126 |
+
<div class="cols">
|
| 127 |
+
<div class="col"><h3>GPT-2, frozen</h3><p class="out" id="out-frozen"></p></div>
|
| 128 |
+
<div class="col win"><h3>GPT-2 + Sillage memory</h3>
|
| 129 |
+
<p class="out" id="out-memory"></p></div>
|
| 130 |
+
</div>
|
| 131 |
+
<p class="note" id="note"></p>
|
| 132 |
+
|
| 133 |
+
<h2><span class="num">2</span>What it does to a document it has never seen</h2>
|
| 134 |
+
<p class="lede">An operations manual invented for this demo, so GPT-2 cannot
|
| 135 |
+
have seen it. Read <b>once</b>, left to right: the first half builds the
|
| 136 |
+
memory, the second half is measured with it — the papers' own dev/test split —
|
| 137 |
+
and every token is scored <i>before</i> being written.</p>
|
| 138 |
+
<div class="panel" id="manual-summary"></div>
|
| 139 |
+
|
| 140 |
+
<h3 style="margin:26px 0 4px">Predictions the memory corrected</h3>
|
| 141 |
+
<p class="lede">The frozen model had no way of knowing these. They are facts
|
| 142 |
+
that exist only in that document.</p>
|
| 143 |
+
<table id="fixes"><thead><tr>
|
| 144 |
+
<th class="mono">context</th><th class="mono">what came next</th>
|
| 145 |
+
<th class="mono">what frozen GPT-2 said</th>
|
| 146 |
+
</tr></thead><tbody></tbody></table>
|
| 147 |
+
|
| 148 |
+
<h2><span class="num">3</span>How much better, exactly</h2>
|
| 149 |
+
<p class="lede">36k tokens of technical text the model had never seen, frozen
|
| 150 |
+
GPT-2 124M, every system tuned identically on a held-out prefix,
|
| 151 |
+
95 % bootstrap confidence intervals.</p>
|
| 152 |
+
<table>
|
| 153 |
+
<thead><tr><th>system</th><th>perplexity</th><th>change</th>
|
| 154 |
+
<th>memory used</th></tr></thead>
|
| 155 |
+
<tbody>
|
| 156 |
+
<tr><td>frozen GPT-2</td><td>31.2</td><td>—</td><td>0</td></tr>
|
| 157 |
+
<tr><td>+ RAG-style retrieve & rescore</td><td>29.9</td><td>−4 %</td>
|
| 158 |
+
<td>corpus + index</td></tr>
|
| 159 |
+
<tr><td>+ kNN-LM, <b>unbounded</b> store</td><td>23.6</td><td>−24 %</td>
|
| 160 |
+
<td>55 MB, grows forever</td></tr>
|
| 161 |
+
<tr class="best"><td>+ this memory (fixed)</td><td>19.2</td><td>−38 %</td>
|
| 162 |
+
<td>4.2 MB, constant</td></tr>
|
| 163 |
+
<tr class="best"><td>+ memory and fast weights</td><td>16.6</td><td>−47 %</td>
|
| 164 |
+
<td>7.4 MB, constant</td></tr>
|
| 165 |
+
</tbody></table>
|
| 166 |
+
<p class="note">Paired block bootstrap <b>P = 1.000</b> against the unbounded
|
| 167 |
+
datastore, replicated over 5 random seeds and on a second model
|
| 168 |
+
(Qwen3-0.6B).</p>
|
| 169 |
+
|
| 170 |
+
<h2><span class="num">4</span>Where it does not work</h2>
|
| 171 |
+
<div class="panel">
|
| 172 |
+
<p><b>On long, low-repetition narrative, an unbounded kNN-LM still wins</b>
|
| 173 |
+
(+0.048 vs +0.007 nats). This memory captures verbatim recurrence, not
|
| 174 |
+
paraphrase. The boundary is measured and published rather than hidden.</p>
|
| 175 |
+
<p>Three results that did <i>not</i> work are published too. Hidden states
|
| 176 |
+
make poor Hebbian keys — their geometry is too entangled. Surprise gating
|
| 177 |
+
helps the memory and <i>hurts</i> the fast-weight adapter, because the delta
|
| 178 |
+
rule already carries its own error term. And calibrating the readout on your
|
| 179 |
+
own stream loses to a proper tuning (+0.109 against +0.120 nats), because the
|
| 180 |
+
calibration window is read by a colder memory than the one it will govern.</p>
|
| 181 |
+
<p>A fixed matrix also saturates at long horizons, near 0.5 writes per
|
| 182 |
+
parameter. Forgetting recovers ×2.3 of the gain and a 4× larger matrix
|
| 183 |
+
recovers ×3.4 — both are one flag away.</p>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<h2><span class="num">5</span>Run it yourself</h2>
|
| 187 |
+
<p class="lede">Every output on this page came out of the command line below,
|
| 188 |
+
on a laptop CPU. It works with any causal language model — a Hugging Face id
|
| 189 |
+
or a local folder — not just the two the papers measured.</p>
|
| 190 |
+
<pre class="sh">pip install sillage
|
| 191 |
+
|
| 192 |
+
sillage index notes.md <span class="c"># instant: no model needed</span>
|
| 193 |
+
sillage ask "what did the report say?"
|
| 194 |
+
|
| 195 |
+
sillage read notes.md <span class="c"># memorise it</span>
|
| 196 |
+
sillage complete "The report said" <span class="c"># generate WITH the memory</span>
|
| 197 |
+
sillage status <span class="c"># what it knows, tier by tier</span>
|
| 198 |
+
|
| 199 |
+
sillage read notes.md --model HuggingFaceTB/SmolLM2-135M <span class="c"># any LM</span></pre>
|
| 200 |
+
|
| 201 |
+
<footer>
|
| 202 |
+
<p>Four preprints with permanent DOIs, the full reproduction pipeline, every
|
| 203 |
+
number as committed JSON and 24 tests:
|
| 204 |
+
<a href="https://github.com/riscoss63/sillage">github.com/riscoss63/sillage</a>
|
| 205 |
+
· <a href="https://pypi.org/project/sillage/">pypi.org/project/sillage</a>
|
| 206 |
+
· <a href="https://doi.org/10.5281/zenodo.22079016">doi:10.5281/zenodo.22079016</a></p>
|
| 207 |
+
<p id="provenance"></p>
|
| 208 |
+
</footer>
|
| 209 |
+
|
| 210 |
+
</div>
|
| 211 |
+
<script>
|
| 212 |
+
const DATA = __DATA__;
|
| 213 |
+
|
| 214 |
+
function split(prompt, text) {
|
| 215 |
+
// the prompt is echoed back by the tool; colour only what was generated
|
| 216 |
+
const rest = text.startsWith(prompt) ? text.slice(prompt.length) : text;
|
| 217 |
+
const head = text.startsWith(prompt) ? prompt : "";
|
| 218 |
+
return [head, rest];
|
| 219 |
+
}
|
| 220 |
+
function render(i) {
|
| 221 |
+
const c = DATA.completions[i];
|
| 222 |
+
for (const [id, key] of [["out-frozen", "frozen"], ["out-memory", "memory"]]) {
|
| 223 |
+
const [head, rest] = split(c.prompt, c.prompt + c[key]);
|
| 224 |
+
document.getElementById(id).innerHTML =
|
| 225 |
+
'<span class="p">' + esc(head) + '</span><span class="new">'
|
| 226 |
+
+ esc(rest) + '</span>';
|
| 227 |
+
}
|
| 228 |
+
document.getElementById("note").textContent = c.same
|
| 229 |
+
? "Identical here — the memory abstains when it is not confident, which "
|
| 230 |
+
+ "is what keeps it from doing harm."
|
| 231 |
+
: "Everything after the prompt on the right comes from the paper the "
|
| 232 |
+
+ "memory read, not from GPT-2's weights.";
|
| 233 |
+
document.querySelectorAll(".chip").forEach((b, k) =>
|
| 234 |
+
b.setAttribute("aria-pressed", k === i ? "true" : "false"));
|
| 235 |
+
}
|
| 236 |
+
function esc(s) {
|
| 237 |
+
return s.replace(/[&<>]/g, m => ({"&":"&","<":"<",">":">"}[m]));
|
| 238 |
+
}
|
| 239 |
+
const chips = document.getElementById("chips");
|
| 240 |
+
DATA.completions.forEach((c, i) => {
|
| 241 |
+
const b = document.createElement("button");
|
| 242 |
+
b.className = "chip";
|
| 243 |
+
b.type = "button";
|
| 244 |
+
b.textContent = c.prompt.length > 52 ? c.prompt.slice(0, 52) + "…" : c.prompt;
|
| 245 |
+
b.title = c.prompt;
|
| 246 |
+
b.onclick = () => render(i);
|
| 247 |
+
chips.appendChild(b);
|
| 248 |
+
});
|
| 249 |
+
document.getElementById("read-what").textContent = DATA.read;
|
| 250 |
+
document.getElementById("manual-summary").innerHTML = md(DATA.manual.summary);
|
| 251 |
+
const body = document.querySelector("#fixes tbody");
|
| 252 |
+
DATA.manual.fixes.forEach(row => {
|
| 253 |
+
const tr = document.createElement("tr");
|
| 254 |
+
row.forEach(cell => {
|
| 255 |
+
const td = document.createElement("td");
|
| 256 |
+
td.className = "mono";
|
| 257 |
+
td.textContent = cell;
|
| 258 |
+
tr.appendChild(td);
|
| 259 |
+
});
|
| 260 |
+
body.appendChild(tr);
|
| 261 |
+
});
|
| 262 |
+
document.getElementById("provenance").textContent =
|
| 263 |
+
"The completions and corrections on this page are recorded runs of "
|
| 264 |
+
+ DATA.model + " on a laptop CPU, state " + DATA.state_mb
|
| 265 |
+
+ " MB — reproduce them with the commands above.";
|
| 266 |
+
function md(s) {
|
| 267 |
+
return "<p>" + esc(s)
|
| 268 |
+
.replace(/\\*\\*(.+?)\\*\\*/g, "<b>$1</b>")
|
| 269 |
+
.replace(/\\*(.+?)\\*/g, "<i>$1</i>")
|
| 270 |
+
.replace(/\\n\\n/g, "</p><p>") + "</p>";
|
| 271 |
+
}
|
| 272 |
+
render(0);
|
| 273 |
+
</script>
|
| 274 |
+
</body>
|
| 275 |
+
</html>
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def main():
|
| 280 |
+
data = json.load(io.open(os.path.join(HERE, "data.json"),
|
| 281 |
+
encoding="utf-8"))
|
| 282 |
+
page = PAGE.replace("__DATA__", json.dumps(data, ensure_ascii=False))
|
| 283 |
+
out = os.path.join(HERE, "index.html")
|
| 284 |
+
io.open(out, "w", encoding="utf-8", newline="\n").write(page)
|
| 285 |
+
print(f"wrote {out}: {len(page) / 1024:.0f} kB, "
|
| 286 |
+
f"{len(data['completions'])} completions, "
|
| 287 |
+
f"{len(data['manual']['fixes'])} corrections")
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
main()
|
build_state.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the memory the Space ships with, so the first screen is instant.
|
| 2 |
+
|
| 3 |
+
Reads paper 1 into a GPT-2 memory and leaves it in ./state (7.4 MB). Run it
|
| 4 |
+
from a clone of the sillage repository, or point it at any document you want
|
| 5 |
+
the Space to have already read.
|
| 6 |
+
|
| 7 |
+
python build_state.py [path/to/document.tex]
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import shutil
|
| 12 |
+
import sys
|
| 13 |
+
|
| 14 |
+
from sillage import Sillage
|
| 15 |
+
|
| 16 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 17 |
+
STATE = os.path.join(HERE, "state")
|
| 18 |
+
DEFAULT = os.path.join(os.path.dirname(HERE), "llm_memory", "papers",
|
| 19 |
+
"sillage", "sillage.tex")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
doc = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
|
| 24 |
+
if not os.path.exists(doc):
|
| 25 |
+
raise SystemExit(f"no such document: {doc}")
|
| 26 |
+
shutil.rmtree(STATE, ignore_errors=True)
|
| 27 |
+
s = Sillage(model="gpt2", state=STATE)
|
| 28 |
+
rec = s.read(doc)[0]
|
| 29 |
+
size = sum(os.path.getsize(os.path.join(STATE, f))
|
| 30 |
+
for f in os.listdir(STATE)) / 1e6
|
| 31 |
+
print(f"{rec['file']}: {rec['tokens']} tokens, PPL {rec['ppl_frozen']} "
|
| 32 |
+
f"-> {rec['ppl_with_memory']}, state {size:.1f} MB in {STATE}")
|
| 33 |
+
print("The Space will load this memory at startup.")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
main()
|
capture.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Capture real outputs so the static page can show them without a server.
|
| 2 |
+
|
| 3 |
+
A static Space has no backend: nothing can run GPT-2 for the visitor. What it
|
| 4 |
+
can do is show what GPT-2 really produced, side by side, and say plainly that
|
| 5 |
+
these are recorded runs anyone can reproduce with `pip install sillage`.
|
| 6 |
+
|
| 7 |
+
Everything written to data.json here comes out of the tool, unedited.
|
| 8 |
+
|
| 9 |
+
python capture.py -> data.json
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import tempfile
|
| 15 |
+
|
| 16 |
+
import sample
|
| 17 |
+
from sillage import Sillage
|
| 18 |
+
|
| 19 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
+
STATE = os.path.join(HERE, "state")
|
| 21 |
+
N = 20
|
| 22 |
+
|
| 23 |
+
PROMPTS = [
|
| 24 |
+
"On a 36k-token stream of novel technical text, the memory",
|
| 25 |
+
"the memory improves GPT-2's test negative log-likelihood by",
|
| 26 |
+
"At 500k tokens the fixed matrix",
|
| 27 |
+
"surprise gating quadruples the gain of",
|
| 28 |
+
"Every write is gated by the model's own",
|
| 29 |
+
"a three-factor plasticity rule whose modulator is",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
memory = Sillage(model="gpt2", state=STATE, quiet=False)
|
| 35 |
+
frozen = Sillage(model="gpt2", state=tempfile.mkdtemp(), quiet=True)
|
| 36 |
+
tok, model = memory.load_model()
|
| 37 |
+
frozen._tok, frozen._model = tok, model
|
| 38 |
+
|
| 39 |
+
out = {"model": "openai-community/gpt2",
|
| 40 |
+
"read": "papers/sillage/sillage.tex (8969 tokens)",
|
| 41 |
+
"state_mb": round(sum(
|
| 42 |
+
os.path.getsize(os.path.join(STATE, f))
|
| 43 |
+
for f in os.listdir(STATE)) / 1e6, 1),
|
| 44 |
+
"completions": []}
|
| 45 |
+
for p in PROMPTS:
|
| 46 |
+
a, b = frozen.complete(p, n=N), memory.complete(p, n=N)
|
| 47 |
+
out["completions"].append({"prompt": p, "frozen": a, "memory": b,
|
| 48 |
+
"same": a.strip() == b.strip()})
|
| 49 |
+
print(f" {p[:44]:46s} {'=' if a == b else 'differs'}")
|
| 50 |
+
|
| 51 |
+
import app # reuse the demo's instrumented read loop
|
| 52 |
+
plot, summary, fixes, _, suggestion = app.read_and_report(sample.MANUAL)
|
| 53 |
+
out["manual"] = {"summary": summary, "fixes": fixes or [],
|
| 54 |
+
"perplexity": plot, "suggestion": suggestion}
|
| 55 |
+
a, b = frozen.complete(suggestion, n=12), None
|
| 56 |
+
fresh = Sillage(model="gpt2", state=app.SESSIONS[-1], quiet=True)
|
| 57 |
+
fresh._tok, fresh._model = tok, model
|
| 58 |
+
b = fresh.complete(suggestion, n=12)
|
| 59 |
+
out["manual"]["completion"] = {"prompt": suggestion, "frozen": a,
|
| 60 |
+
"memory": b}
|
| 61 |
+
|
| 62 |
+
with open(os.path.join(HERE, "data.json"), "w", encoding="utf-8") as f:
|
| 63 |
+
json.dump(out, f, indent=1, ensure_ascii=False)
|
| 64 |
+
print(f"wrote data.json: {len(out['completions'])} completions, "
|
| 65 |
+
f"{len(out['manual']['fixes'])} corrections")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
data.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "openai-community/gpt2",
|
| 3 |
+
"read": "papers/sillage/sillage.tex (8969 tokens)",
|
| 4 |
+
"state_mb": 7.4,
|
| 5 |
+
"completions": [
|
| 6 |
+
{
|
| 7 |
+
"prompt": "On a 36k-token stream of novel technical text, the memory",
|
| 8 |
+
"frozen": " of the original text is not lost.\n\nThe original text is not lost.\n\nThe",
|
| 9 |
+
"memory": " improves GPT-2's test negative log-likelihood by +0.486 +/- 0.",
|
| 10 |
+
"same": false
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"prompt": "the memory improves GPT-2's test negative log-likelihood by",
|
| 14 |
+
"frozen": " a factor of 1.5.\n\nThe results of the study are summarized in Table 1.",
|
| 15 |
+
"memory": " +0.486 +/- 0.005 nats (perplexity 31.2 -> 19.",
|
| 16 |
+
"same": false
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"prompt": "At 500k tokens the fixed matrix",
|
| 20 |
+
"frozen": " is a bit more complex.\n\nThe first thing to note is that the fixed matrix is not",
|
| 21 |
+
"memory": " saturates (0.5 writes per parameter); leaky decay recovers 2.3k writes per",
|
| 22 |
+
"same": false
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"prompt": "surprise gating quadruples the gain of",
|
| 26 |
+
"frozen": " the previous year.\n\nThe new study, published in the journal Nature Communications, found that the",
|
| 27 |
+
"memory": " uniform writes at equal plasticity budget (+0.203 vs +0.050 for the count variant",
|
| 28 |
+
"same": false
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"prompt": "Every write is gated by the model's own",
|
| 32 |
+
"frozen": " rules.\n\nThe model's rules are:\n\nThe model must be able to write a",
|
| 33 |
+
"memory": " surprise\n\n\nThe model's surprise is the surprise of the augmented reality -- the surprise of the augmented",
|
| 34 |
+
"same": false
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"prompt": "a three-factor plasticity rule whose modulator is",
|
| 38 |
+
"frozen": " a single-cell polyethylene (CPM) polyethylene (PPM) polyethyl",
|
| 39 |
+
"memory": " free at inference. We call the system . On the other hand, the system is free at inference",
|
| 40 |
+
"same": false
|
| 41 |
+
}
|
| 42 |
+
],
|
| 43 |
+
"manual": {
|
| 44 |
+
"summary": "**Read once, left to right: 652 tokens built the memory, the next 653 were measured with it.** That split is the papers' own protocol — the numbers below are on text the memory had not seen when it scored them, and every token was scored *before* being written.\n\nPerplexity on that second half: 11.16 frozen → 9.84 with the rank-16 adapter → **9.80** with the memory on top — **12% lower**. The memory spoke on 36% of those positions and kept quiet on the rest; abstaining when it has nothing to say is what keeps it from doing harm.\n\nState on disk: **7.4 MB**, and it would still be 7.4 MB after a million tokens. A kNN-LM datastore over the same text would already hold about 8.0 MB, and would keep growing.",
|
| 45 |
+
"fixes": [
|
| 46 |
+
[
|
| 47 |
+
"wick valves or the ninth cabinet is reported immediately",
|
| 48 |
+
"','",
|
| 49 |
+
"' to'"
|
| 50 |
+
],
|
| 51 |
+
[
|
| 52 |
+
"incoming duty officer. The morning routine begins at",
|
| 53 |
+
"' 05'",
|
| 54 |
+
"' 19'"
|
| 55 |
+
],
|
| 56 |
+
[
|
| 57 |
+
" holds the amber cipher. The rotation changes every eleven",
|
| 58 |
+
"' days'",
|
| 59 |
+
"'\\n'"
|
| 60 |
+
],
|
| 61 |
+
[
|
| 62 |
+
" incident, and is recorded in the gallery log",
|
| 63 |
+
"' beside'",
|
| 64 |
+
"'.'"
|
| 65 |
+
],
|
| 66 |
+
[
|
| 67 |
+
". Record the seal number in the gallery log.",
|
| 68 |
+
"'\\n'",
|
| 69 |
+
"' The'"
|
| 70 |
+
],
|
| 71 |
+
[
|
| 72 |
+
"in manifold. Confirm the Kelbrin manifold",
|
| 73 |
+
"' reads'",
|
| 74 |
+
"' is'"
|
| 75 |
+
],
|
| 76 |
+
[
|
| 77 |
+
". Confirm the Kelbrin manifold reads between",
|
| 78 |
+
"' 4'",
|
| 79 |
+
"'\\n'"
|
| 80 |
+
],
|
| 81 |
+
[
|
| 82 |
+
"4.6 bar. Open the Fenwick valves",
|
| 83 |
+
"' in'",
|
| 84 |
+
"'.'"
|
| 85 |
+
],
|
| 86 |
+
[
|
| 87 |
+
"6 bar. Open the Fenwick valves in the",
|
| 88 |
+
"' order'",
|
| 89 |
+
"' west'"
|
| 90 |
+
],
|
| 91 |
+
[
|
| 92 |
+
" bar. Open the Fenwick valves in the order",
|
| 93 |
+
"' three'",
|
| 94 |
+
"' in'"
|
| 95 |
+
],
|
| 96 |
+
[
|
| 97 |
+
" one, two. Inspect the condensate",
|
| 98 |
+
"' trap'",
|
| 99 |
+
"'.'"
|
| 100 |
+
],
|
| 101 |
+
[
|
| 102 |
+
" the seal number. Confirm the technician",
|
| 103 |
+
"' on'",
|
| 104 |
+
"\"'s\""
|
| 105 |
+
]
|
| 106 |
+
],
|
| 107 |
+
"perplexity": {
|
| 108 |
+
"frozen GPT-2": 11.161853381368227,
|
| 109 |
+
"+ fast weights": 9.837810800300112,
|
| 110 |
+
"+ memory": 9.795396904604075
|
| 111 |
+
},
|
| 112 |
+
"suggestion": "The Fenwick valves open three, one,",
|
| 113 |
+
"completion": {
|
| 114 |
+
"prompt": "The Fenwick valves open three, one,",
|
| 115 |
+
"frozen": " two, three, four, five, six, seven,",
|
| 116 |
+
"memory": " two. Inspect the\n\nfenwick valve and the"
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
demo.gif
ADDED
|
Git LFS Details
|
index.html
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Sillage — a frozen LM that remembers what it reads</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg:#0d1117; --panel:#161b22; --line:#26303d; --ink:#c9d1d9;
|
| 10 |
+
--dim:#8b949e; --hl:#79c0ff; --warm:#ffc457; --ok:#3fb950;
|
| 11 |
+
}
|
| 12 |
+
* { box-sizing:border-box }
|
| 13 |
+
body {
|
| 14 |
+
margin:0; background:var(--bg); color:var(--ink); line-height:1.6;
|
| 15 |
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,
|
| 16 |
+
Helvetica,Arial,sans-serif;
|
| 17 |
+
}
|
| 18 |
+
.wrap { max-width:980px; margin:0 auto; padding:0 20px 80px }
|
| 19 |
+
header { text-align:center; padding:56px 0 24px }
|
| 20 |
+
h1 { font-size:2.6rem; margin:0 0 10px; letter-spacing:-.02em }
|
| 21 |
+
.tag { font-size:1.15rem; color:var(--ink); max-width:640px; margin:0 auto }
|
| 22 |
+
.etym { color:var(--dim); font-style:italic; font-size:.95rem;
|
| 23 |
+
max-width:560px; margin:18px auto 0 }
|
| 24 |
+
.badges { margin:22px 0 8px; display:flex; gap:8px; justify-content:center;
|
| 25 |
+
flex-wrap:wrap }
|
| 26 |
+
.badges img { height:20px }
|
| 27 |
+
h2 { font-size:1.5rem; margin:52px 0 6px; letter-spacing:-.01em }
|
| 28 |
+
h2 .num { color:var(--dim); font-weight:400; margin-right:8px }
|
| 29 |
+
.lede { color:var(--dim); margin:0 0 20px }
|
| 30 |
+
.gif { width:100%; border:1px solid var(--line); border-radius:10px;
|
| 31 |
+
display:block; margin:26px 0 }
|
| 32 |
+
.chips { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px }
|
| 33 |
+
.chip {
|
| 34 |
+
background:var(--panel); border:1px solid var(--line); color:var(--ink);
|
| 35 |
+
border-radius:999px; padding:7px 14px; cursor:pointer; font-size:.9rem;
|
| 36 |
+
font-family:inherit; transition:border-color .15s, color .15s;
|
| 37 |
+
}
|
| 38 |
+
.chip:hover { border-color:var(--hl); color:var(--hl) }
|
| 39 |
+
.chip[aria-pressed="true"] { border-color:var(--hl); color:var(--hl);
|
| 40 |
+
background:#16273b }
|
| 41 |
+
.cols { display:grid; grid-template-columns:1fr 1fr; gap:14px }
|
| 42 |
+
@media (max-width:760px) { .cols { grid-template-columns:1fr } }
|
| 43 |
+
.col { background:var(--panel); border:1px solid var(--line);
|
| 44 |
+
border-radius:10px; padding:14px 16px; min-height:170px }
|
| 45 |
+
.col h3 { margin:0 0 10px; font-size:.85rem; text-transform:uppercase;
|
| 46 |
+
letter-spacing:.08em; color:var(--dim); font-weight:600 }
|
| 47 |
+
.col.win h3 { color:var(--ok) }
|
| 48 |
+
.out { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 49 |
+
font-size:.9rem; white-space:pre-wrap; word-break:break-word; margin:0 }
|
| 50 |
+
.out .p { color:var(--dim) }
|
| 51 |
+
.out .new { color:var(--warm) }
|
| 52 |
+
.col.win .out .new { color:var(--hl) }
|
| 53 |
+
.note { color:var(--dim); font-size:.9rem; margin:12px 0 0 }
|
| 54 |
+
table { width:100%; border-collapse:collapse; margin:18px 0; font-size:.92rem }
|
| 55 |
+
th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line) }
|
| 56 |
+
th { color:var(--dim); font-weight:600; font-size:.82rem;
|
| 57 |
+
text-transform:uppercase; letter-spacing:.06em }
|
| 58 |
+
td.mono,th.mono { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 59 |
+
font-size:.86rem }
|
| 60 |
+
tr.best td { color:var(--hl); font-weight:600 }
|
| 61 |
+
.panel { background:var(--panel); border:1px solid var(--line);
|
| 62 |
+
border-radius:10px; padding:18px 20px; margin:18px 0 }
|
| 63 |
+
.panel p:first-child { margin-top:0 }
|
| 64 |
+
.panel p:last-child { margin-bottom:0 }
|
| 65 |
+
pre.sh { background:var(--panel); border:1px solid var(--line);
|
| 66 |
+
border-radius:10px; padding:16px 18px; overflow-x:auto;
|
| 67 |
+
font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 68 |
+
font-size:.9rem; color:var(--ink) }
|
| 69 |
+
pre.sh .c { color:var(--dim) }
|
| 70 |
+
a { color:var(--hl) }
|
| 71 |
+
footer { color:var(--dim); font-size:.9rem; text-align:center;
|
| 72 |
+
margin-top:64px; border-top:1px solid var(--line); padding-top:24px }
|
| 73 |
+
.kbd { font-family:"Cascadia Mono",Consolas,ui-monospace,monospace;
|
| 74 |
+
background:#1c2432; border:1px solid var(--line); border-radius:5px;
|
| 75 |
+
padding:1px 6px; font-size:.85em }
|
| 76 |
+
</style>
|
| 77 |
+
</head>
|
| 78 |
+
<body>
|
| 79 |
+
<div class="wrap">
|
| 80 |
+
|
| 81 |
+
<header>
|
| 82 |
+
<h1>Sillage</h1>
|
| 83 |
+
<p class="tag">A frozen language model that <b>remembers what it reads</b> —
|
| 84 |
+
4.2 MB, no gradients, no fine-tuning, no vector database.</p>
|
| 85 |
+
<p class="etym">sillage (n., French) — the trace left behind by something
|
| 86 |
+
that has passed: a ship's wake, a scent in a room. What a model keeps of
|
| 87 |
+
what it read.</p>
|
| 88 |
+
<div class="badges">
|
| 89 |
+
<a href="https://pypi.org/project/sillage/"><img alt="PyPI"
|
| 90 |
+
src="https://img.shields.io/pypi/v/sillage.svg"></a>
|
| 91 |
+
<a href="https://github.com/riscoss63/sillage"><img alt="GitHub"
|
| 92 |
+
src="https://img.shields.io/badge/code-GitHub-181717?logo=github"></a>
|
| 93 |
+
<a href="https://doi.org/10.5281/zenodo.22079016"><img alt="DOI"
|
| 94 |
+
src="https://zenodo.org/badge/DOI/10.5281/zenodo.22079016.svg"></a>
|
| 95 |
+
<a href="https://github.com/riscoss63/sillage/blob/main/LICENSE"><img
|
| 96 |
+
alt="MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
|
| 97 |
+
</div>
|
| 98 |
+
</header>
|
| 99 |
+
|
| 100 |
+
<img class="gif" src="demo.gif" alt="Two sessions a day apart: the model
|
| 101 |
+
reads a draft on Monday and halves the perplexity of the next draft on
|
| 102 |
+
Tuesday, then completes a sentence it can only know from what it read.">
|
| 103 |
+
|
| 104 |
+
<h2><span class="num">1</span>It has already read a paper</h2>
|
| 105 |
+
<p class="lede">The memory below has read
|
| 106 |
+
<b id="read-what"></b> — the paper describing this very mechanism, which
|
| 107 |
+
GPT-2 has never seen. Same prompt, same greedy decoding, one column with the
|
| 108 |
+
memory and one without. Pick a beginning:</p>
|
| 109 |
+
|
| 110 |
+
<div class="chips" id="chips"></div>
|
| 111 |
+
<div class="cols">
|
| 112 |
+
<div class="col"><h3>GPT-2, frozen</h3><p class="out" id="out-frozen"></p></div>
|
| 113 |
+
<div class="col win"><h3>GPT-2 + Sillage memory</h3>
|
| 114 |
+
<p class="out" id="out-memory"></p></div>
|
| 115 |
+
</div>
|
| 116 |
+
<p class="note" id="note"></p>
|
| 117 |
+
|
| 118 |
+
<h2><span class="num">2</span>What it does to a document it has never seen</h2>
|
| 119 |
+
<p class="lede">An operations manual invented for this demo, so GPT-2 cannot
|
| 120 |
+
have seen it. Read <b>once</b>, left to right: the first half builds the
|
| 121 |
+
memory, the second half is measured with it — the papers' own dev/test split —
|
| 122 |
+
and every token is scored <i>before</i> being written.</p>
|
| 123 |
+
<div class="panel" id="manual-summary"></div>
|
| 124 |
+
|
| 125 |
+
<h3 style="margin:26px 0 4px">Predictions the memory corrected</h3>
|
| 126 |
+
<p class="lede">The frozen model had no way of knowing these. They are facts
|
| 127 |
+
that exist only in that document.</p>
|
| 128 |
+
<table id="fixes"><thead><tr>
|
| 129 |
+
<th class="mono">context</th><th class="mono">what came next</th>
|
| 130 |
+
<th class="mono">what frozen GPT-2 said</th>
|
| 131 |
+
</tr></thead><tbody></tbody></table>
|
| 132 |
+
|
| 133 |
+
<h2><span class="num">3</span>How much better, exactly</h2>
|
| 134 |
+
<p class="lede">36k tokens of technical text the model had never seen, frozen
|
| 135 |
+
GPT-2 124M, every system tuned identically on a held-out prefix,
|
| 136 |
+
95 % bootstrap confidence intervals.</p>
|
| 137 |
+
<table>
|
| 138 |
+
<thead><tr><th>system</th><th>perplexity</th><th>change</th>
|
| 139 |
+
<th>memory used</th></tr></thead>
|
| 140 |
+
<tbody>
|
| 141 |
+
<tr><td>frozen GPT-2</td><td>31.2</td><td>—</td><td>0</td></tr>
|
| 142 |
+
<tr><td>+ RAG-style retrieve & rescore</td><td>29.9</td><td>−4 %</td>
|
| 143 |
+
<td>corpus + index</td></tr>
|
| 144 |
+
<tr><td>+ kNN-LM, <b>unbounded</b> store</td><td>23.6</td><td>−24 %</td>
|
| 145 |
+
<td>55 MB, grows forever</td></tr>
|
| 146 |
+
<tr class="best"><td>+ this memory (fixed)</td><td>19.2</td><td>−38 %</td>
|
| 147 |
+
<td>4.2 MB, constant</td></tr>
|
| 148 |
+
<tr class="best"><td>+ memory and fast weights</td><td>16.6</td><td>−47 %</td>
|
| 149 |
+
<td>7.4 MB, constant</td></tr>
|
| 150 |
+
</tbody></table>
|
| 151 |
+
<p class="note">Paired block bootstrap <b>P = 1.000</b> against the unbounded
|
| 152 |
+
datastore, replicated over 5 random seeds and on a second model
|
| 153 |
+
(Qwen3-0.6B).</p>
|
| 154 |
+
|
| 155 |
+
<h2><span class="num">4</span>Where it does not work</h2>
|
| 156 |
+
<div class="panel">
|
| 157 |
+
<p><b>On long, low-repetition narrative, an unbounded kNN-LM still wins</b>
|
| 158 |
+
(+0.048 vs +0.007 nats). This memory captures verbatim recurrence, not
|
| 159 |
+
paraphrase. The boundary is measured and published rather than hidden.</p>
|
| 160 |
+
<p>Three results that did <i>not</i> work are published too. Hidden states
|
| 161 |
+
make poor Hebbian keys — their geometry is too entangled. Surprise gating
|
| 162 |
+
helps the memory and <i>hurts</i> the fast-weight adapter, because the delta
|
| 163 |
+
rule already carries its own error term. And calibrating the readout on your
|
| 164 |
+
own stream loses to a proper tuning (+0.109 against +0.120 nats), because the
|
| 165 |
+
calibration window is read by a colder memory than the one it will govern.</p>
|
| 166 |
+
<p>A fixed matrix also saturates at long horizons, near 0.5 writes per
|
| 167 |
+
parameter. Forgetting recovers ×2.3 of the gain and a 4× larger matrix
|
| 168 |
+
recovers ×3.4 — both are one flag away.</p>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<h2><span class="num">5</span>Run it yourself</h2>
|
| 172 |
+
<p class="lede">Every output on this page came out of the command line below,
|
| 173 |
+
on a laptop CPU. It works with any causal language model — a Hugging Face id
|
| 174 |
+
or a local folder — not just the two the papers measured.</p>
|
| 175 |
+
<pre class="sh">pip install sillage
|
| 176 |
+
|
| 177 |
+
sillage index notes.md <span class="c"># instant: no model needed</span>
|
| 178 |
+
sillage ask "what did the report say?"
|
| 179 |
+
|
| 180 |
+
sillage read notes.md <span class="c"># memorise it</span>
|
| 181 |
+
sillage complete "The report said" <span class="c"># generate WITH the memory</span>
|
| 182 |
+
sillage status <span class="c"># what it knows, tier by tier</span>
|
| 183 |
+
|
| 184 |
+
sillage read notes.md --model HuggingFaceTB/SmolLM2-135M <span class="c"># any LM</span></pre>
|
| 185 |
+
|
| 186 |
+
<footer>
|
| 187 |
+
<p>Four preprints with permanent DOIs, the full reproduction pipeline, every
|
| 188 |
+
number as committed JSON and 24 tests:
|
| 189 |
+
<a href="https://github.com/riscoss63/sillage">github.com/riscoss63/sillage</a>
|
| 190 |
+
· <a href="https://pypi.org/project/sillage/">pypi.org/project/sillage</a>
|
| 191 |
+
· <a href="https://doi.org/10.5281/zenodo.22079016">doi:10.5281/zenodo.22079016</a></p>
|
| 192 |
+
<p id="provenance"></p>
|
| 193 |
+
</footer>
|
| 194 |
+
|
| 195 |
+
</div>
|
| 196 |
+
<script>
|
| 197 |
+
const DATA = {"model": "openai-community/gpt2", "read": "papers/sillage/sillage.tex (8969 tokens)", "state_mb": 7.4, "completions": [{"prompt": "On a 36k-token stream of novel technical text, the memory", "frozen": " of the original text is not lost.\n\nThe original text is not lost.\n\nThe", "memory": " improves GPT-2's test negative log-likelihood by +0.486 +/- 0.", "same": false}, {"prompt": "the memory improves GPT-2's test negative log-likelihood by", "frozen": " a factor of 1.5.\n\nThe results of the study are summarized in Table 1.", "memory": " +0.486 +/- 0.005 nats (perplexity 31.2 -> 19.", "same": false}, {"prompt": "At 500k tokens the fixed matrix", "frozen": " is a bit more complex.\n\nThe first thing to note is that the fixed matrix is not", "memory": " saturates (0.5 writes per parameter); leaky decay recovers 2.3k writes per", "same": false}, {"prompt": "surprise gating quadruples the gain of", "frozen": " the previous year.\n\nThe new study, published in the journal Nature Communications, found that the", "memory": " uniform writes at equal plasticity budget (+0.203 vs +0.050 for the count variant", "same": false}, {"prompt": "Every write is gated by the model's own", "frozen": " rules.\n\nThe model's rules are:\n\nThe model must be able to write a", "memory": " surprise\n\n\nThe model's surprise is the surprise of the augmented reality -- the surprise of the augmented", "same": false}, {"prompt": "a three-factor plasticity rule whose modulator is", "frozen": " a single-cell polyethylene (CPM) polyethylene (PPM) polyethyl", "memory": " free at inference. We call the system . On the other hand, the system is free at inference", "same": false}], "manual": {"summary": "**Read once, left to right: 652 tokens built the memory, the next 653 were measured with it.** That split is the papers' own protocol — the numbers below are on text the memory had not seen when it scored them, and every token was scored *before* being written.\n\nPerplexity on that second half: 11.16 frozen → 9.84 with the rank-16 adapter → **9.80** with the memory on top — **12% lower**. The memory spoke on 36% of those positions and kept quiet on the rest; abstaining when it has nothing to say is what keeps it from doing harm.\n\nState on disk: **7.4 MB**, and it would still be 7.4 MB after a million tokens. A kNN-LM datastore over the same text would already hold about 8.0 MB, and would keep growing.", "fixes": [["wick valves or the ninth cabinet is reported immediately", "','", "' to'"], ["incoming duty officer. The morning routine begins at", "' 05'", "' 19'"], [" holds the amber cipher. The rotation changes every eleven", "' days'", "'\\n'"], [" incident, and is recorded in the gallery log", "' beside'", "'.'"], [". Record the seal number in the gallery log.", "'\\n'", "' The'"], ["in manifold. Confirm the Kelbrin manifold", "' reads'", "' is'"], [". Confirm the Kelbrin manifold reads between", "' 4'", "'\\n'"], ["4.6 bar. Open the Fenwick valves", "' in'", "'.'"], ["6 bar. Open the Fenwick valves in the", "' order'", "' west'"], [" bar. Open the Fenwick valves in the order", "' three'", "' in'"], [" one, two. Inspect the condensate", "' trap'", "'.'"], [" the seal number. Confirm the technician", "' on'", "\"'s\""]], "perplexity": {"frozen GPT-2": 11.161853381368227, "+ fast weights": 9.837810800300112, "+ memory": 9.795396904604075}, "suggestion": "The Fenwick valves open three, one,", "completion": {"prompt": "The Fenwick valves open three, one,", "frozen": " two, three, four, five, six, seven,", "memory": " two. Inspect the\n\nfenwick valve and the"}}};
|
| 198 |
+
|
| 199 |
+
function split(prompt, text) {
|
| 200 |
+
// the prompt is echoed back by the tool; colour only what was generated
|
| 201 |
+
const rest = text.startsWith(prompt) ? text.slice(prompt.length) : text;
|
| 202 |
+
const head = text.startsWith(prompt) ? prompt : "";
|
| 203 |
+
return [head, rest];
|
| 204 |
+
}
|
| 205 |
+
function render(i) {
|
| 206 |
+
const c = DATA.completions[i];
|
| 207 |
+
for (const [id, key] of [["out-frozen", "frozen"], ["out-memory", "memory"]]) {
|
| 208 |
+
const [head, rest] = split(c.prompt, c.prompt + c[key]);
|
| 209 |
+
document.getElementById(id).innerHTML =
|
| 210 |
+
'<span class="p">' + esc(head) + '</span><span class="new">'
|
| 211 |
+
+ esc(rest) + '</span>';
|
| 212 |
+
}
|
| 213 |
+
document.getElementById("note").textContent = c.same
|
| 214 |
+
? "Identical here — the memory abstains when it is not confident, which "
|
| 215 |
+
+ "is what keeps it from doing harm."
|
| 216 |
+
: "Everything after the prompt on the right comes from the paper the "
|
| 217 |
+
+ "memory read, not from GPT-2's weights.";
|
| 218 |
+
document.querySelectorAll(".chip").forEach((b, k) =>
|
| 219 |
+
b.setAttribute("aria-pressed", k === i ? "true" : "false"));
|
| 220 |
+
}
|
| 221 |
+
function esc(s) {
|
| 222 |
+
return s.replace(/[&<>]/g, m => ({"&":"&","<":"<",">":">"}[m]));
|
| 223 |
+
}
|
| 224 |
+
const chips = document.getElementById("chips");
|
| 225 |
+
DATA.completions.forEach((c, i) => {
|
| 226 |
+
const b = document.createElement("button");
|
| 227 |
+
b.className = "chip";
|
| 228 |
+
b.type = "button";
|
| 229 |
+
b.textContent = c.prompt.length > 52 ? c.prompt.slice(0, 52) + "…" : c.prompt;
|
| 230 |
+
b.title = c.prompt;
|
| 231 |
+
b.onclick = () => render(i);
|
| 232 |
+
chips.appendChild(b);
|
| 233 |
+
});
|
| 234 |
+
document.getElementById("read-what").textContent = DATA.read;
|
| 235 |
+
document.getElementById("manual-summary").innerHTML = md(DATA.manual.summary);
|
| 236 |
+
const body = document.querySelector("#fixes tbody");
|
| 237 |
+
DATA.manual.fixes.forEach(row => {
|
| 238 |
+
const tr = document.createElement("tr");
|
| 239 |
+
row.forEach(cell => {
|
| 240 |
+
const td = document.createElement("td");
|
| 241 |
+
td.className = "mono";
|
| 242 |
+
td.textContent = cell;
|
| 243 |
+
tr.appendChild(td);
|
| 244 |
+
});
|
| 245 |
+
body.appendChild(tr);
|
| 246 |
+
});
|
| 247 |
+
document.getElementById("provenance").textContent =
|
| 248 |
+
"The completions and corrections on this page are recorded runs of "
|
| 249 |
+
+ DATA.model + " on a laptop CPU, state " + DATA.state_mb
|
| 250 |
+
+ " MB — reproduce them with the commands above.";
|
| 251 |
+
function md(s) {
|
| 252 |
+
return "<p>" + esc(s)
|
| 253 |
+
.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
|
| 254 |
+
.replace(/\*(.+?)\*/g, "<i>$1</i>")
|
| 255 |
+
.replace(/\n\n/g, "</p><p>") + "</p>";
|
| 256 |
+
}
|
| 257 |
+
render(0);
|
| 258 |
+
</script>
|
| 259 |
+
</body>
|
| 260 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The tool itself, straight from PyPI.
|
| 2 |
+
sillage==1.0.1
|
| 3 |
+
|
| 4 |
+
# ZeroGPU: provides the @spaces.GPU decorator. Harmless anywhere else.
|
| 5 |
+
spaces
|
sample.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Texts the visitor can read into a fresh memory in one click.
|
| 2 |
+
|
| 3 |
+
Two of them, chosen for what they demonstrate:
|
| 4 |
+
|
| 5 |
+
MANUAL an internal-looking operations manual, invented for this demo, so
|
| 6 |
+
GPT-2 cannot possibly have seen it. Recurring names, recurring
|
| 7 |
+
procedures, the structure real internal documents actually have --
|
| 8 |
+
the regime where a fixed memory wins. Long enough (~1400 tokens)
|
| 9 |
+
that the abstention threshold has a distribution to work from.
|
| 10 |
+
PAPER the opening of paper 1, i.e. genuinely novel technical prose.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
MANUAL = """Meridian Station — Standing Operations Manual, revision 12
|
| 16 |
+
|
| 17 |
+
1. Purpose
|
| 18 |
+
|
| 19 |
+
This manual governs every routine carried out at Meridian Station. It is
|
| 20 |
+
issued by the Station Warden and supersedes revision 11. Where this manual
|
| 21 |
+
conflicts with a local instruction, this manual prevails, and the Station
|
| 22 |
+
Warden is notified within one shift.
|
| 23 |
+
|
| 24 |
+
2. The morning routine
|
| 25 |
+
|
| 26 |
+
The morning routine begins at 05:40 when the duty officer unseals the west
|
| 27 |
+
gallery. The duty officer records the seal number in the gallery log, then
|
| 28 |
+
verifies the pressure of the Kelbrin manifold. The Kelbrin manifold must read
|
| 29 |
+
between 4.1 and 4.6 bar. If the Kelbrin manifold reads below 4.1 bar, the
|
| 30 |
+
duty officer raises a yellow flag with the Station Warden and does not
|
| 31 |
+
proceed to the second stage. If the Kelbrin manifold reads above 4.6 bar, the
|
| 32 |
+
duty officer vents the auxiliary line and waits eleven minutes before reading
|
| 33 |
+
it again.
|
| 34 |
+
|
| 35 |
+
Once the Kelbrin manifold is within range, the duty officer opens the
|
| 36 |
+
Fenwick valves in the order three, one, two. The order three, one, two is not
|
| 37 |
+
negotiable: opening the Fenwick valves in any other order floods the
|
| 38 |
+
condensate trap. The condensate trap is drained every second morning by the
|
| 39 |
+
technician on rotation, who signs the gallery log beside the seal number.
|
| 40 |
+
|
| 41 |
+
3. The evening routine
|
| 42 |
+
|
| 43 |
+
The evening routine begins at 19:15 when the duty officer reseals the west
|
| 44 |
+
gallery. Before resealing, the duty officer closes the Fenwick valves in the
|
| 45 |
+
order two, one, three, which is the reverse of the morning order. The
|
| 46 |
+
condensate trap is inspected, and the reading of the Kelbrin manifold is
|
| 47 |
+
recorded a second time in the gallery log.
|
| 48 |
+
|
| 49 |
+
If the Kelbrin manifold has drifted by more than 0.3 bar since morning, the
|
| 50 |
+
duty officer files a drift note. A drift note names the shift, the two
|
| 51 |
+
readings, and the technician on rotation. Three drift notes in one week
|
| 52 |
+
trigger a manifold survey by the Station Warden.
|
| 53 |
+
|
| 54 |
+
4. Personnel
|
| 55 |
+
|
| 56 |
+
The Station Warden is Ilvane Corr. The Station Warden holds the only key to
|
| 57 |
+
the ninth cabinet and is the sole authority for a manifold survey. In the
|
| 58 |
+
absence of Ilvane Corr, authority passes to the senior technician, currently
|
| 59 |
+
Bo Halloway, except for the ninth cabinet, which remains sealed.
|
| 60 |
+
|
| 61 |
+
The technician on rotation changes every eleven days. The rotation list is
|
| 62 |
+
posted in the west gallery and countersigned by Bo Halloway. A technician on
|
| 63 |
+
rotation may not also serve as duty officer during the same shift.
|
| 64 |
+
|
| 65 |
+
5. The ninth cabinet
|
| 66 |
+
|
| 67 |
+
The ninth cabinet holds the amber cipher and the spare seals for the west
|
| 68 |
+
gallery. Only Ilvane Corr may open the ninth cabinet. The amber cipher is
|
| 69 |
+
required whenever the Kelbrin manifold is surveyed, and it is returned to the
|
| 70 |
+
ninth cabinet before the evening routine begins. No copy of the amber cipher
|
| 71 |
+
is kept anywhere at Meridian Station.
|
| 72 |
+
|
| 73 |
+
6. Incidents
|
| 74 |
+
|
| 75 |
+
An incident is any deviation from the morning routine, the evening routine,
|
| 76 |
+
or the rotation list. Incidents are recorded in the gallery log beside the
|
| 77 |
+
seal number, and reported to the Station Warden within one shift. An incident
|
| 78 |
+
involving the Kelbrin manifold, the Fenwick valves or the ninth cabinet is
|
| 79 |
+
reported immediately, not within one shift.
|
| 80 |
+
|
| 81 |
+
If the west gallery cannot be sealed at 19:15, the duty officer declares a
|
| 82 |
+
standing incident, remains at the west gallery, and notifies Ilvane Corr. A
|
| 83 |
+
standing incident is closed only by the Station Warden, in person, with the
|
| 84 |
+
amber cipher present.
|
| 85 |
+
|
| 86 |
+
7. Handover
|
| 87 |
+
|
| 88 |
+
At every handover the outgoing duty officer recites the fixed points to the
|
| 89 |
+
incoming duty officer. The morning routine begins at 05:40. The evening
|
| 90 |
+
routine begins at 19:15. The Kelbrin manifold reads between 4.1 and 4.6 bar.
|
| 91 |
+
The Fenwick valves open three, one, two and close two, one, three. The
|
| 92 |
+
Station Warden is Ilvane Corr. The senior technician is Bo Halloway. The
|
| 93 |
+
ninth cabinet holds the amber cipher. The rotation changes every eleven days.
|
| 94 |
+
|
| 95 |
+
The incoming duty officer repeats the fixed points back. A handover in which
|
| 96 |
+
the fixed points are not recited and repeated back is an incident, and is
|
| 97 |
+
recorded in the gallery log beside the seal number.
|
| 98 |
+
|
| 99 |
+
8. Checklist, morning
|
| 100 |
+
|
| 101 |
+
Unseal the west gallery at 05:40. Record the seal number in the gallery log.
|
| 102 |
+
Read the Kelbrin manifold. Confirm the Kelbrin manifold reads between 4.1 and
|
| 103 |
+
4.6 bar. Open the Fenwick valves in the order three, one, two. Inspect the
|
| 104 |
+
condensate trap. Sign the gallery log beside the seal number. Confirm the
|
| 105 |
+
technician on rotation against the rotation list posted in the west gallery.
|
| 106 |
+
|
| 107 |
+
9. Checklist, evening
|
| 108 |
+
|
| 109 |
+
Close the Fenwick valves in the order two, one, three. Read the Kelbrin
|
| 110 |
+
manifold a second time. Record the reading in the gallery log. Compare it
|
| 111 |
+
with the morning reading and file a drift note if the Kelbrin manifold has
|
| 112 |
+
drifted by more than 0.3 bar. Confirm the amber cipher has been returned to
|
| 113 |
+
the ninth cabinet. Reseal the west gallery at 19:15. Notify Ilvane Corr of
|
| 114 |
+
any incident within one shift.
|
| 115 |
+
|
| 116 |
+
10. Summary of the fixed points
|
| 117 |
+
|
| 118 |
+
The morning routine begins at 05:40. The evening routine begins at 19:15. The
|
| 119 |
+
Kelbrin manifold reads between 4.1 and 4.6 bar. The Fenwick valves open three,
|
| 120 |
+
one, two and close two, one, three. The Station Warden is Ilvane Corr. The
|
| 121 |
+
senior technician is Bo Halloway. The ninth cabinet holds the amber cipher.
|
| 122 |
+
The rotation changes every eleven days. These fixed points are recited at
|
| 123 |
+
every handover, and any deviation from them is an incident.
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
_PAPER = os.path.join(os.path.dirname(os.path.dirname(
|
| 127 |
+
os.path.abspath(__file__))), "llm_memory", "papers", "sillage",
|
| 128 |
+
"sillage.tex")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def paper_excerpt(limit=14000):
|
| 132 |
+
"""The opening of paper 1 as plain text, if the repository is nearby."""
|
| 133 |
+
try:
|
| 134 |
+
from sillage.index import read_text
|
| 135 |
+
return read_text(_PAPER)[:limit]
|
| 136 |
+
except Exception:
|
| 137 |
+
return MANUAL
|
state/cold.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:75e7a34ebc793b298e6bb671375edfff20a95404527152d0134e622d890c2484
|
| 3 |
+
size 311125
|
state/index.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d90f319b3f5828a1cabd15886f1c143da0238b74f322a231be09a2a7a33b64e7
|
| 3 |
+
size 101643
|
state/log.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [
|
| 3 |
+
{
|
| 4 |
+
"file": "sillage.tex",
|
| 5 |
+
"tokens": 8969,
|
| 6 |
+
"date": "2026-08-25 03:02",
|
| 7 |
+
"ppl_frozen": 68.81,
|
| 8 |
+
"ppl_fastweights": 58.24,
|
| 9 |
+
"ppl_with_memory": 56.01
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
}
|
state/state.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c2a11143fe1df59a9fa599336cff651ca3c6bd8fca37d362fb002c6edfc000a9
|
| 3 |
+
size 6985621
|