sillage 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sillage-1.0.0/LICENSE +21 -0
- sillage-1.0.0/PKG-INFO +151 -0
- sillage-1.0.0/PYPI.md +125 -0
- sillage-1.0.0/README.md +374 -0
- sillage-1.0.0/pyproject.toml +40 -0
- sillage-1.0.0/setup.cfg +4 -0
- sillage-1.0.0/sillage/__init__.py +30 -0
- sillage-1.0.0/sillage/__main__.py +6 -0
- sillage-1.0.0/sillage/cli.py +447 -0
- sillage-1.0.0/sillage/core.py +670 -0
- sillage-1.0.0/sillage/index.py +242 -0
- sillage-1.0.0/sillage/runtime.py +278 -0
- sillage-1.0.0/sillage.egg-info/PKG-INFO +151 -0
- sillage-1.0.0/sillage.egg-info/SOURCES.txt +16 -0
- sillage-1.0.0/sillage.egg-info/dependency_links.txt +1 -0
- sillage-1.0.0/sillage.egg-info/entry_points.txt +2 -0
- sillage-1.0.0/sillage.egg-info/requires.txt +7 -0
- sillage-1.0.0/sillage.egg-info/top_level.txt +1 -0
sillage-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abderrahmane Sghairi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sillage-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sillage
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Gradient-free test-time memory and learning for frozen language models
|
|
5
|
+
Author-email: Abderrahmane Sghairi <sghairipro63@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/riscoss63/sillage
|
|
8
|
+
Project-URL: Papers, https://doi.org/10.5281/zenodo.22079016
|
|
9
|
+
Keywords: language-models,associative-memory,hebbian,test-time-learning,fast-weights,gradient-free,continual-learning,vsa,hyperdimensional
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Requires-Dist: torch>=2.0
|
|
21
|
+
Requires-Dist: transformers>=4.51
|
|
22
|
+
Provides-Extra: research
|
|
23
|
+
Requires-Dist: scipy>=1.10; extra == "research"
|
|
24
|
+
Requires-Dist: matplotlib>=3.7; extra == "research"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Sillage
|
|
28
|
+
|
|
29
|
+
**Your language model forgets everything. Sillage gives it a memory — and a
|
|
30
|
+
way to keep learning — in a fixed handful of megabytes, with no gradients and
|
|
31
|
+
no index that grows.**
|
|
32
|
+
|
|
33
|
+
> *sillage* (n., French) — the trace left behind by something that has passed:
|
|
34
|
+
> a ship's wake, a scent in a room. What a model keeps of what it read.
|
|
35
|
+
|
|
36
|
+
A frozen language model reads your documents, remembers them, and predicts
|
|
37
|
+
better next time. No gradients, no fine-tuning, no vector database. One
|
|
38
|
+
Hebbian matrix written as the model reads, a semantic tier routed by
|
|
39
|
+
confidence, a cold store that consolidates by surprise, and a rank-16 adapter
|
|
40
|
+
on the readout — four mechanisms, four preprints, one command-line tool.
|
|
41
|
+
Everything runs on a laptop CPU.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install sillage
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Use
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sillage index notes.md # instant: no model, already queryable
|
|
55
|
+
sillage ask "what did the report say?"
|
|
56
|
+
|
|
57
|
+
sillage read notes.md # memorize it (CPU: ~8 min per 10k tokens)
|
|
58
|
+
sillage complete "The report said" # generate WITH the memory
|
|
59
|
+
sillage status # what it knows, tier by tier
|
|
60
|
+
sillage forget --all
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```console
|
|
64
|
+
$ sillage read preprint_v1.txt # Monday, memory empty
|
|
65
|
+
read preprint_v1.txt: 5859 tokens in 4.9 min | PPL 12.14 -> 11.83 (adapter) -> 11.71 (+memory)
|
|
66
|
+
|
|
67
|
+
$ sillage read preprint_v2.md # Tuesday, a new process
|
|
68
|
+
read preprint_v2.md: 11041 tokens in 9.5 min | PPL 10.68 -> 9.82 (adapter) -> 5.39 (+memory)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Three numbers per file: what the frozen model alone predicts, what the
|
|
72
|
+
rank-16 adapter adds, and what the memory of everything read so far adds on
|
|
73
|
+
top. The state lives in `./.sillage`, survives restarts, and never grows:
|
|
74
|
+
7.4 MB with GPT-2, 25 MB with Qwen3, the same after one document or ten
|
|
75
|
+
thousand. It never learns from its own generations — only from what you give
|
|
76
|
+
it to read.
|
|
77
|
+
|
|
78
|
+
From Python:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from sillage import Sillage
|
|
82
|
+
|
|
83
|
+
s = Sillage(model="gpt2") # any causal LM; omit it and the state
|
|
84
|
+
# says which model it belongs to
|
|
85
|
+
s.read("notes.md") # read, memorize, index -- then save
|
|
86
|
+
s.ask("what did the report say?") # exact passages, nothing generated
|
|
87
|
+
print(s.complete("The report said"))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How much better, exactly?
|
|
91
|
+
|
|
92
|
+
On 36k tokens of technical text the model had never seen (frozen GPT-2 124M,
|
|
93
|
+
every system tuned identically on a held-out prefix, 95 % bootstrap CIs):
|
|
94
|
+
|
|
95
|
+
| system | perplexity | change | memory used |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| frozen GPT-2 | 31.2 | — | 0 |
|
|
98
|
+
| \+ RAG-style retrieve & rescore | 29.9 | −4 % | corpus + index |
|
|
99
|
+
| \+ kNN-LM, **unbounded** store | 23.6 | −24 % | 55 MB, grows forever |
|
|
100
|
+
| \+ **this memory** (fixed) | **19.2** | **−38 %** | **4.2 MB, constant** |
|
|
101
|
+
| \+ memory **and** fast weights | **16.6** | **−47 %** | 7.4 MB, constant |
|
|
102
|
+
|
|
103
|
+
The fixed 4.2 MB memory beats the unbounded datastore it was designed to
|
|
104
|
+
approximate — paired bootstrap **P = 1.000**, replicated over 5 random seeds
|
|
105
|
+
and on a second model (Qwen3-0.6B).
|
|
106
|
+
|
|
107
|
+
**Where it does not win:** on long, low-repetition narrative text, kNN-LM
|
|
108
|
+
still beats it. This memory captures verbatim recurrence; that is its regime,
|
|
109
|
+
and it is measured and published rather than hidden.
|
|
110
|
+
|
|
111
|
+
## Any causal language model
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sillage read notes.md --model qwen # shortcut
|
|
115
|
+
sillage read notes.md --model HuggingFaceTB/SmolLM2-135M # any hub id
|
|
116
|
+
sillage read notes.md --model ./my-finetuned-llama # any local folder
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Verified across three architectures (GPT-2, Qwen3, GPT-NeoX). For a model
|
|
120
|
+
nobody has tuned, the readout calibrates itself on a rolling window of what
|
|
121
|
+
you read, following the papers' protocol; for the two the papers did tune,
|
|
122
|
+
their published settings are kept. A memory is written in one model's token
|
|
123
|
+
space, so give each model its own `--state` directory.
|
|
124
|
+
|
|
125
|
+
## The four preprints
|
|
126
|
+
|
|
127
|
+
| # | title | DOI |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| 1 | Sillage: Surprise-Gated Amplitude Memory for Frozen Language Models | [10.5281/zenodo.22079016](https://doi.org/10.5281/zenodo.22079016) |
|
|
130
|
+
| 2 | Route the Scores, Not the Keys | [10.5281/zenodo.22079444](https://doi.org/10.5281/zenodo.22079444) |
|
|
131
|
+
| 3 | One Signal, Three Tiers | [10.5281/zenodo.22079471](https://doi.org/10.5281/zenodo.22079471) |
|
|
132
|
+
| 4 | Memory Remembers, Fast Weights Adapt | [10.5281/zenodo.22079481](https://doi.org/10.5281/zenodo.22079481) |
|
|
133
|
+
|
|
134
|
+
Sources, figures, every number as committed JSON, the reproduction pipeline
|
|
135
|
+
and the three negative results are on GitHub:
|
|
136
|
+
**<https://github.com/riscoss63/sillage>**
|
|
137
|
+
|
|
138
|
+
## Citation
|
|
139
|
+
|
|
140
|
+
```bibtex
|
|
141
|
+
@article{sghairi2026sillage,
|
|
142
|
+
title = {Sillage: Surprise-Gated Amplitude Memory
|
|
143
|
+
for Frozen Language Models},
|
|
144
|
+
author = {Sghairi, Abderrahmane},
|
|
145
|
+
year = {2026},
|
|
146
|
+
doi = {10.5281/zenodo.22079016},
|
|
147
|
+
url = {https://doi.org/10.5281/zenodo.22079016}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
MIT licensed.
|
sillage-1.0.0/PYPI.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Sillage
|
|
2
|
+
|
|
3
|
+
**Your language model forgets everything. Sillage gives it a memory — and a
|
|
4
|
+
way to keep learning — in a fixed handful of megabytes, with no gradients and
|
|
5
|
+
no index that grows.**
|
|
6
|
+
|
|
7
|
+
> *sillage* (n., French) — the trace left behind by something that has passed:
|
|
8
|
+
> a ship's wake, a scent in a room. What a model keeps of what it read.
|
|
9
|
+
|
|
10
|
+
A frozen language model reads your documents, remembers them, and predicts
|
|
11
|
+
better next time. No gradients, no fine-tuning, no vector database. One
|
|
12
|
+
Hebbian matrix written as the model reads, a semantic tier routed by
|
|
13
|
+
confidence, a cold store that consolidates by surprise, and a rank-16 adapter
|
|
14
|
+
on the readout — four mechanisms, four preprints, one command-line tool.
|
|
15
|
+
Everything runs on a laptop CPU.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install sillage
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
sillage index notes.md # instant: no model, already queryable
|
|
29
|
+
sillage ask "what did the report say?"
|
|
30
|
+
|
|
31
|
+
sillage read notes.md # memorize it (CPU: ~8 min per 10k tokens)
|
|
32
|
+
sillage complete "The report said" # generate WITH the memory
|
|
33
|
+
sillage status # what it knows, tier by tier
|
|
34
|
+
sillage forget --all
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ sillage read preprint_v1.txt # Monday, memory empty
|
|
39
|
+
read preprint_v1.txt: 5859 tokens in 4.9 min | PPL 12.14 -> 11.83 (adapter) -> 11.71 (+memory)
|
|
40
|
+
|
|
41
|
+
$ sillage read preprint_v2.md # Tuesday, a new process
|
|
42
|
+
read preprint_v2.md: 11041 tokens in 9.5 min | PPL 10.68 -> 9.82 (adapter) -> 5.39 (+memory)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Three numbers per file: what the frozen model alone predicts, what the
|
|
46
|
+
rank-16 adapter adds, and what the memory of everything read so far adds on
|
|
47
|
+
top. The state lives in `./.sillage`, survives restarts, and never grows:
|
|
48
|
+
7.4 MB with GPT-2, 25 MB with Qwen3, the same after one document or ten
|
|
49
|
+
thousand. It never learns from its own generations — only from what you give
|
|
50
|
+
it to read.
|
|
51
|
+
|
|
52
|
+
From Python:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from sillage import Sillage
|
|
56
|
+
|
|
57
|
+
s = Sillage(model="gpt2") # any causal LM; omit it and the state
|
|
58
|
+
# says which model it belongs to
|
|
59
|
+
s.read("notes.md") # read, memorize, index -- then save
|
|
60
|
+
s.ask("what did the report say?") # exact passages, nothing generated
|
|
61
|
+
print(s.complete("The report said"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How much better, exactly?
|
|
65
|
+
|
|
66
|
+
On 36k tokens of technical text the model had never seen (frozen GPT-2 124M,
|
|
67
|
+
every system tuned identically on a held-out prefix, 95 % bootstrap CIs):
|
|
68
|
+
|
|
69
|
+
| system | perplexity | change | memory used |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| frozen GPT-2 | 31.2 | — | 0 |
|
|
72
|
+
| \+ RAG-style retrieve & rescore | 29.9 | −4 % | corpus + index |
|
|
73
|
+
| \+ kNN-LM, **unbounded** store | 23.6 | −24 % | 55 MB, grows forever |
|
|
74
|
+
| \+ **this memory** (fixed) | **19.2** | **−38 %** | **4.2 MB, constant** |
|
|
75
|
+
| \+ memory **and** fast weights | **16.6** | **−47 %** | 7.4 MB, constant |
|
|
76
|
+
|
|
77
|
+
The fixed 4.2 MB memory beats the unbounded datastore it was designed to
|
|
78
|
+
approximate — paired bootstrap **P = 1.000**, replicated over 5 random seeds
|
|
79
|
+
and on a second model (Qwen3-0.6B).
|
|
80
|
+
|
|
81
|
+
**Where it does not win:** on long, low-repetition narrative text, kNN-LM
|
|
82
|
+
still beats it. This memory captures verbatim recurrence; that is its regime,
|
|
83
|
+
and it is measured and published rather than hidden.
|
|
84
|
+
|
|
85
|
+
## Any causal language model
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
sillage read notes.md --model qwen # shortcut
|
|
89
|
+
sillage read notes.md --model HuggingFaceTB/SmolLM2-135M # any hub id
|
|
90
|
+
sillage read notes.md --model ./my-finetuned-llama # any local folder
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Verified across three architectures (GPT-2, Qwen3, GPT-NeoX). For a model
|
|
94
|
+
nobody has tuned, the readout calibrates itself on a rolling window of what
|
|
95
|
+
you read, following the papers' protocol; for the two the papers did tune,
|
|
96
|
+
their published settings are kept. A memory is written in one model's token
|
|
97
|
+
space, so give each model its own `--state` directory.
|
|
98
|
+
|
|
99
|
+
## The four preprints
|
|
100
|
+
|
|
101
|
+
| # | title | DOI |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| 1 | Sillage: Surprise-Gated Amplitude Memory for Frozen Language Models | [10.5281/zenodo.22079016](https://doi.org/10.5281/zenodo.22079016) |
|
|
104
|
+
| 2 | Route the Scores, Not the Keys | [10.5281/zenodo.22079444](https://doi.org/10.5281/zenodo.22079444) |
|
|
105
|
+
| 3 | One Signal, Three Tiers | [10.5281/zenodo.22079471](https://doi.org/10.5281/zenodo.22079471) |
|
|
106
|
+
| 4 | Memory Remembers, Fast Weights Adapt | [10.5281/zenodo.22079481](https://doi.org/10.5281/zenodo.22079481) |
|
|
107
|
+
|
|
108
|
+
Sources, figures, every number as committed JSON, the reproduction pipeline
|
|
109
|
+
and the three negative results are on GitHub:
|
|
110
|
+
**<https://github.com/riscoss63/sillage>**
|
|
111
|
+
|
|
112
|
+
## Citation
|
|
113
|
+
|
|
114
|
+
```bibtex
|
|
115
|
+
@article{sghairi2026sillage,
|
|
116
|
+
title = {Sillage: Surprise-Gated Amplitude Memory
|
|
117
|
+
for Frozen Language Models},
|
|
118
|
+
author = {Sghairi, Abderrahmane},
|
|
119
|
+
year = {2026},
|
|
120
|
+
doi = {10.5281/zenodo.22079016},
|
|
121
|
+
url = {https://doi.org/10.5281/zenodo.22079016}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
MIT licensed.
|
sillage-1.0.0/README.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Sillage
|
|
2
|
+
|
|
3
|
+
**Your language model forgets everything. Sillage gives it a memory — and a
|
|
4
|
+
way to keep learning — in a fixed handful of megabytes, with no gradients and
|
|
5
|
+
no index that grows.**
|
|
6
|
+
|
|
7
|
+
> *sillage* (n., French) — the trace left behind by something that has passed:
|
|
8
|
+
> a ship's wake, a scent in a room. What a model keeps of what it read.
|
|
9
|
+
|
|
10
|
+
[](https://github.com/riscoss63/sillage/actions/workflows/tests.yml)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](pyproject.toml)
|
|
13
|
+
[](requirements.txt)
|
|
14
|
+
[](papers/)
|
|
15
|
+
[](https://doi.org/10.5281/zenodo.22079016)
|
|
16
|
+
|
|
17
|
+
A frozen LM reads your documents, remembers them, and predicts better next
|
|
18
|
+
time — **no gradients, no fine-tuning, no growing index**. One Hebbian matrix
|
|
19
|
+
written as the model reads, a semantic tier routed by confidence, a cold store
|
|
20
|
+
that consolidates by surprise, and a rank-16 adapter on the readout. Four
|
|
21
|
+
mechanisms, four papers, **one command-line tool**. Everything runs on a
|
|
22
|
+
laptop CPU.
|
|
23
|
+
|
|
24
|
+
<p align="center"><img src="figs/demo.gif" width="94%" alt="Sillage demo: a frozen LM reads a document on Monday and recalls it on Tuesday"></p>
|
|
25
|
+
|
|
26
|
+
Two sessions, two days apart, nothing kept in context: on Tuesday the second
|
|
27
|
+
draft costs **half** the perplexity it would have cost on Monday (10.68 -> 5.39),
|
|
28
|
+
and the model completes a sentence it can only know from what it read.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart (60 seconds, then a coffee)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/riscoss63/sillage && cd sillage
|
|
36
|
+
pip install -e . # numpy + torch + transformers
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sillage index notes.md # instant: no model, already queryable
|
|
41
|
+
sillage ask "what did the report say?"
|
|
42
|
+
|
|
43
|
+
sillage read notes.md # memorize it (CPU: ~8 min per 10k tokens)
|
|
44
|
+
sillage complete "The report said" # generate WITH the memory
|
|
45
|
+
sillage status # what it knows, tier by tier
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
$ sillage read preprint_v1.txt # Monday, memory empty
|
|
50
|
+
read preprint_v1.txt: 5859 tokens in 4.9 min | PPL 12.14 -> 11.83 (adapter) -> 11.71 (+memory)
|
|
51
|
+
|
|
52
|
+
$ sillage read preprint_v2.md # Tuesday, a new process
|
|
53
|
+
read preprint_v2.md: 11041 tokens in 9.5 min | PPL 10.68 -> 9.82 (adapter) -> 5.39 (+memory)
|
|
54
|
+
memory consolidated and saved (16900 tokens lifetime, 9883 cold grams, 194 passages indexed).
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Three numbers per file, because there are two mechanisms: what the frozen
|
|
58
|
+
model alone predicts, what the rank-16 adapter adds, and what the memory of
|
|
59
|
+
everything read so far adds on top.
|
|
60
|
+
|
|
61
|
+
The memory lives in `./.sillage` and survives restarts — it survived a power
|
|
62
|
+
cut during development. Its size is fixed the day you start: 7.4 MB with
|
|
63
|
+
GPT-2, 25 MB with Qwen3 (whose vocabulary is 3x larger), the same after one
|
|
64
|
+
document or ten thousand. It **never learns from its own generations**,
|
|
65
|
+
only from what you give it to read.
|
|
66
|
+
|
|
67
|
+
Reading is the slow part (one frozen forward pass per token, on CPU): about
|
|
68
|
+
8 minutes per 10k tokens with the default Qwen3-0.6B, about 2 with
|
|
69
|
+
`--model gpt2` — which is 4x faster but English-only. `index` and `ask` need
|
|
70
|
+
no model at all and are instant.
|
|
71
|
+
|
|
72
|
+
### Which model can it wrap? Any of them
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
sillage read notes.md --model qwen # shortcut
|
|
76
|
+
sillage read notes.md --model HuggingFaceTB/SmolLM2-135M # any hub id
|
|
77
|
+
sillage read notes.md --model ./my-finetuned-llama # any local folder
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Nothing here is model-specific: the memory only ever sees next-token logits,
|
|
81
|
+
one hidden state and the observed token, so **any causal language model
|
|
82
|
+
works** — GPT-2, Qwen, Llama, Mistral, Pythia, SmolLM, your own fine-tune.
|
|
83
|
+
Verified across three architectures (GPT-2, Qwen3, GPT-NeoX). Four things to
|
|
84
|
+
know before pointing it at a new one:
|
|
85
|
+
|
|
86
|
+
- **The readout tunes itself on an unfamiliar model.** How strongly the
|
|
87
|
+
memory speaks (`beta`, `lambda`) and when it stays quiet (the abstention
|
|
88
|
+
threshold) were tuned per model in the papers. For any model they did not
|
|
89
|
+
tune, the tool does that tuning itself: one position in three joins a
|
|
90
|
+
rolling window, the published grids are searched on it at the end of each
|
|
91
|
+
read, and the winner governs the next one — so nothing is ever scored with
|
|
92
|
+
settings fitted on itself. For `qwen` and `gpt2` the published settings are
|
|
93
|
+
kept instead, and that is a measured decision, not deference: refitting
|
|
94
|
+
them on your own cold memory *loses* (see below). `--calibrate` forces
|
|
95
|
+
fitting anyway, `--no-calibrate` forbids it, `read --recalibrate` starts
|
|
96
|
+
over.
|
|
97
|
+
- The **semantic tier stays off** for an unknown model (`--semantic` to force
|
|
98
|
+
it). Paper 2 is explicit: raw hidden states need whitening except where
|
|
99
|
+
their geometry is already well conditioned, and that has only been verified
|
|
100
|
+
on Qwen3.
|
|
101
|
+
- A memory is written in **one model's token space**, so give each model its
|
|
102
|
+
own `--state` directory. After that you can drop `--model`: the state
|
|
103
|
+
remembers which model it belongs to, and refuses to be opened by another.
|
|
104
|
+
- **Cost follows the model**: reading time follows its size, and the adapter
|
|
105
|
+
is `vocab x 16` floats (3.2 MB at GPT-2's vocabulary, 9.7 MB at Qwen3's).
|
|
106
|
+
A 7B model on a CPU is possible but slow; this was designed for the 0.1–1B
|
|
107
|
+
class.
|
|
108
|
+
|
|
109
|
+
What calibration actually does, measured on a model the papers never touched
|
|
110
|
+
(Pythia 70M, two documents, second one scored out of sample):
|
|
111
|
+
|
|
112
|
+
```console
|
|
113
|
+
$ sillage read doc_a.txt --model EleutherAI/pythia-70m
|
|
114
|
+
read doc_a.txt: 5025 tokens in 1.0 min | PPL 2.02 -> 1.92 (adapter) -> 1.79 (+memory)
|
|
115
|
+
fitted the readout on 1675 observations from what was just read (the papers' grids):
|
|
116
|
+
n-gram tier : beta 40, lambda 0.85, abstain below q75
|
|
117
|
+
+0.2461 nats on that window. It governs the NEXT read, never this one,
|
|
118
|
+
so no perplexity printed here was tuned on itself.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
On the next document that fitted readout gives **1.38** where the GPT-2
|
|
122
|
+
defaults give 1.42 — worth having when nobody has tuned your model.
|
|
123
|
+
|
|
124
|
+
It also tracks the memory as it fills, because each fit sees a warmer memory
|
|
125
|
+
than the last. Reading one paper three times (GPT-2, `--calibrate`), where
|
|
126
|
+
the readout fitted at the end of a pass is the one that governs the next:
|
|
127
|
+
|
|
128
|
+
| pass | perplexity of this pass | what its window then fitted |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 1 | 56.0 | `beta 20, lambda 0.1, abstain below q75` |
|
|
131
|
+
| 2 | 7.5 | `beta 40, lambda 0.85, abstain below q50` |
|
|
132
|
+
| 3 | 1.5 | `beta 40, lambda 0.85, abstain below q25` |
|
|
133
|
+
|
|
134
|
+
(Frozen GPT-2 alone stays at 68.8 throughout.)
|
|
135
|
+
|
|
136
|
+
Prefer no install? `pip install -r requirements.txt` and then
|
|
137
|
+
`python -m sillage read notes.md` does exactly the same thing — that also
|
|
138
|
+
works if the `sillage` command lands outside your PATH.
|
|
139
|
+
|
|
140
|
+
<details>
|
|
141
|
+
<summary><b>Use it from Python (four lines)</b></summary>
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from sillage import Sillage
|
|
145
|
+
|
|
146
|
+
s = Sillage(model="gpt2") # any causal LM; omit it and the state
|
|
147
|
+
# says which model it belongs to
|
|
148
|
+
s.read("notes.md") # read, memorize, index -- then save
|
|
149
|
+
s.ask("what did the report say?") # exact passages, nothing generated
|
|
150
|
+
print(s.complete("The report said"))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`Sillage.status()` returns the same numbers as the CLI as a dict, and
|
|
154
|
+
`sillage.SillageMemory` is the mechanism alone (numpy only, no transformers)
|
|
155
|
+
if you want to wire it into your own generation loop.
|
|
156
|
+
</details>
|
|
157
|
+
|
|
158
|
+
<details>
|
|
159
|
+
<summary><b>Every option</b></summary>
|
|
160
|
+
|
|
161
|
+
| command | what it does |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `sillage read FILE...` | read + memorize + index (all four mechanisms) |
|
|
164
|
+
| `sillage index FILE...` | index only — instant, no model needed |
|
|
165
|
+
| `sillage ask "..."` | grounded excerpts with their source and section |
|
|
166
|
+
| `sillage complete "..."` | generate with the memory and the adapter |
|
|
167
|
+
| `sillage chat` | both, interactively (`/say`, `/read`, `/status`) |
|
|
168
|
+
| `sillage papers` | index the four preprints shipped here, then ask them |
|
|
169
|
+
| `sillage demo FILE` | two sessions on one document, start to finish |
|
|
170
|
+
| `sillage status` / `forget --all` | inspect / wipe |
|
|
171
|
+
|
|
172
|
+
Flags: `--model NAME` (see below), `--state DIR` (or `$SILLAGE_STATE`),
|
|
173
|
+
`--no-fastweights`, `--no-semantic`, `--half-life N` (forgetting, in tokens),
|
|
174
|
+
`--no-calibrate` / `read --recalibrate`, `-n`, `--temp`, `-k`. Globs are
|
|
175
|
+
expanded by the tool itself, so `sillage read docs/*.md` works on Windows
|
|
176
|
+
too.
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## How much better, exactly?
|
|
182
|
+
|
|
183
|
+
On 36k tokens of technical text the model had never seen (frozen GPT-2 124M,
|
|
184
|
+
every system tuned identically on a held-out prefix, 95 % bootstrap CIs):
|
|
185
|
+
|
|
186
|
+
| system | perplexity | change | memory used |
|
|
187
|
+
|---|---|---|---|
|
|
188
|
+
| frozen GPT-2 | 31.2 | — | 0 |
|
|
189
|
+
| \+ RAG-style retrieve & rescore | 29.9 | −4 % | corpus + index |
|
|
190
|
+
| \+ kNN-LM, **unbounded** store | 23.6 | −24 % | 55 MB, grows forever |
|
|
191
|
+
| \+ **this memory** (fixed) | **19.2** | **−38 %** | **4.2 MB, constant** |
|
|
192
|
+
| \+ memory **and** fast weights | **16.6** | **−47 %** | 7.4 MB, constant |
|
|
193
|
+
|
|
194
|
+
The fixed 4.2 MB memory beats the unbounded datastore it was designed to
|
|
195
|
+
approximate — paired bootstrap **P = 1.000**, replicated over 5 random seeds
|
|
196
|
+
and on a second model (Qwen3-0.6B). And the gains show up in behaviour, not
|
|
197
|
+
just likelihood: recall of recurring technical terms after one reading pass
|
|
198
|
+
goes from **11.3 % to 23.7 %** (McNemar 261:14).
|
|
199
|
+
|
|
200
|
+
<p align="center"><img src="figs/fig1_main.png" width="88%" alt="Main results"></p>
|
|
201
|
+
|
|
202
|
+
## Should you use this?
|
|
203
|
+
|
|
204
|
+
| your situation | better option |
|
|
205
|
+
|---|---|
|
|
206
|
+
| One machine, private documents, must run offline | **this** |
|
|
207
|
+
| The model must keep learning after deployment, forever, at bounded cost | **this** |
|
|
208
|
+
| You can afford a datastore that grows with everything you read | kNN-LM / RAG |
|
|
209
|
+
| You need the model to *reason* better, not to *remember* better | fine-tuning |
|
|
210
|
+
| The document fits in the context window and you only read it once | just paste it |
|
|
211
|
+
|
|
212
|
+
It is a **memory**, not an intelligence upgrade: it makes a model better at
|
|
213
|
+
the text in front of it, on a fixed byte budget, forever.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## How it works, in four ideas
|
|
218
|
+
|
|
219
|
+
<p align="center"><img src="figs/fig0_architecture.png" width="82%" alt="architecture"></p>
|
|
220
|
+
|
|
221
|
+
1. **Keys by binding.** Each position is addressed by a sliding *n*-gram
|
|
222
|
+
product of random token hypervectors — a key that fires on exact
|
|
223
|
+
repetition and stays near-orthogonal otherwise.
|
|
224
|
+
2. **Values as amplitudes.** The matrix stores *square roots* of accumulated
|
|
225
|
+
mass, not counts. That one change roughly doubles the memory's benefit:
|
|
226
|
+
square-root compression stops frequent continuations from drowning rare
|
|
227
|
+
ones in superposition.
|
|
228
|
+
3. **Surprise decides.** Every write is scaled by the model's own token
|
|
229
|
+
surprise `−ln p_LM` — free at inference. The same scalar also arbitrates
|
|
230
|
+
which tier answers, and which patterns get consolidated to cold storage.
|
|
231
|
+
4. **Fast weights adapt what memory cannot.** A rank-16 delta-rule adapter on
|
|
232
|
+
the readout (3.2 MB, no error transported through any layer) wins exactly
|
|
233
|
+
where memory is weakest, and their gains add up (89–98 % additive).
|
|
234
|
+
|
|
235
|
+
### The four papers are the four mechanisms — all of them are in the tool
|
|
236
|
+
|
|
237
|
+
| paper | mechanism in `sillage/` | on by default | switch |
|
|
238
|
+
|---|---|---|---|
|
|
239
|
+
| 1 · Sillage | `M_G`, 4.2 MB Hebbian *n*-gram tier, amplitude writes, surprise gate | yes | — |
|
|
240
|
+
| 2 · Router | `M_S`, 12.6 MB semantic tier, **score-level** mixing with abstention | `--model qwen` only (other models need whitening) | `--semantic` / `--no-semantic` |
|
|
241
|
+
| 3 · Hierarchy | cold store of exact 4-grams, consolidated by surprise **mass** at save time | yes | — |
|
|
242
|
+
| 4 · Fast weights | `A`, rank-16 delta-rule readout adapter, uniform step | yes | `--no-fastweights` |
|
|
243
|
+
| 1 & 3 · long horizons | leaky forgetting (half-life in tokens) | no — needed past ~0.5 writes/parameter | `--half-life 100000` |
|
|
244
|
+
| all four · protocol | readout **calibration**: the papers' grids fitted on a rolling window of what you just read, governing the next read | only for a model the papers did not tune | `--calibrate` / `--no-calibrate`, `read --recalibrate` |
|
|
245
|
+
|
|
246
|
+
`sillage status` shows both of the last two lines: which readout is in force
|
|
247
|
+
right now, and how close the matrix is to saturation — the capacity law being
|
|
248
|
+
the honest limit of the whole approach.
|
|
249
|
+
|
|
250
|
+
## The four preprints
|
|
251
|
+
|
|
252
|
+
All four are archived on Zenodo with permanent DOIs; the LaTeX sources and figures are in [`papers/`](papers/). `sillage papers` indexes them so you can query them offline.
|
|
253
|
+
|
|
254
|
+
| # | title | the finding |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| 1 | **[Sillage](https://doi.org/10.5281/zenodo.22079016)** · [source](papers/sillage/sillage.tex) | a fixed 4.2 MB Hebbian cache beats an unbounded kNN-LM on novel repetitive text |
|
|
257
|
+
| 2 | **[Route the Scores, Not the Keys](https://doi.org/10.5281/zenodo.22079444)** · [source](papers/router/router.tex) | gradient-free semantic keys work — but only if you mix at the score level, never in the key |
|
|
258
|
+
| 3 | **[One Signal, Three Tiers](https://doi.org/10.5281/zenodo.22079471)** · [source](papers/hierarchy/hierarchy.tex) | consolidating by *surprise mass* keeps 94 % of a cold store's value with 10 % of its entries |
|
|
259
|
+
| 4 | **[Memory Remembers, Fast Weights Adapt](https://doi.org/10.5281/zenodo.22079481)** · [source](papers/fastweights/fastweights.tex) | two gradient-free mechanisms, opposite regimes, near-additive gains |
|
|
260
|
+
|
|
261
|
+
## What did *not* work (and how we know)
|
|
262
|
+
|
|
263
|
+
This is the part most repositories leave out.
|
|
264
|
+
|
|
265
|
+
- **Hidden states make terrible Hebbian keys.** Their similarity geometry is
|
|
266
|
+
too entangled (95th-percentile cosine 0.93 between random pairs); the best
|
|
267
|
+
semantic-only configuration reached 8 % of the *n*-gram key's gain and was
|
|
268
|
+
harmful off-domain.
|
|
269
|
+
- **Surprise gating helps memory and *hurts* fast weights** (−18 % on one
|
|
270
|
+
stream, −100 % on another). The delta rule already carries its own error
|
|
271
|
+
term; gating double-counts it. Gate the mechanisms that cannot see their own
|
|
272
|
+
error; leave alone the ones that can.
|
|
273
|
+
- **Two evaluation bugs shipped before any result did.** Raw Gutenberg `\r\n`
|
|
274
|
+
endings inflated every method by **+1.35 phantom nats**; a one-position
|
|
275
|
+
misalignment made the RAG baseline score at chance. Both were caught by
|
|
276
|
+
controls, not by luck.
|
|
277
|
+
- **Self-calibration loses to a proper tuning, where one exists.** Fitting the
|
|
278
|
+
readout on your own stream sounds strictly better than using someone else's
|
|
279
|
+
constants. It is not: the window is read by a memory that is *colder* than
|
|
280
|
+
the one those settings will govern, so the fit comes out systematically too
|
|
281
|
+
timid. Measured on GPT-2, fitting on one technical paper and scoring on the
|
|
282
|
+
next (gain over the frozen model, in nats):
|
|
283
|
+
|
|
284
|
+
| readout fitted on | gain on the next paper |
|
|
285
|
+
|---|---|
|
|
286
|
+
| the whole window | +0.098 |
|
|
287
|
+
| its recent half | +0.103 |
|
|
288
|
+
| its recent quarter | +0.109 |
|
|
289
|
+
| **the papers' published settings** | **+0.120** |
|
|
290
|
+
| an oracle fitted on that paper itself | +0.125 |
|
|
291
|
+
|
|
292
|
+
Recency helps, and never enough. Hence the rule the tool follows: calibrate
|
|
293
|
+
a model nobody has tuned, and keep the published constants for the two that
|
|
294
|
+
were tuned properly — on 36k–500k-token streams, not on a few thousand cold
|
|
295
|
+
ones.
|
|
296
|
+
|
|
297
|
+
Hence the three controls we now consider mandatory for streaming-memory work —
|
|
298
|
+
`python eval/diagnostic.py` runs them: a **shuffled-retrieval null** (replace
|
|
299
|
+
retrieved values with random tokens; any surviving gain is an artifact), a
|
|
300
|
+
**unigram-cache null**, and a **base-model sanity perplexity**.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Reproducing everything
|
|
305
|
+
|
|
306
|
+
Every number in every paper regenerates from these scripts with fixed seeds,
|
|
307
|
+
on CPU. See **[REPRODUCE.md](REPRODUCE.md)** for the full pipeline; results
|
|
308
|
+
are committed as JSON in [`results/`](results/) (including per-seed values).
|
|
309
|
+
`python test_unit.py` checks the mechanisms themselves in five seconds
|
|
310
|
+
(numpy only, no model: retrieval, the square-root rule, forgetting, the delta
|
|
311
|
+
rule, consolidation, the state round-trip, the readout tuner, the multi-model
|
|
312
|
+
paths); `python test_sillage.py` runs 13 end-to-end tests of the tool (each
|
|
313
|
+
command in its own process, invented facts the base model cannot know).
|
|
314
|
+
|
|
315
|
+
<details>
|
|
316
|
+
<summary><b>Repository layout</b></summary>
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
sillage/ the tool: core.py (the four mechanisms), runtime.py,
|
|
320
|
+
index.py (grounded retrieval), cli.py
|
|
321
|
+
pyproject.toml packaging: pip install -e . gives you the `sillage` command
|
|
322
|
+
test_unit.py the mechanisms, in five seconds, numpy only
|
|
323
|
+
test_sillage.py the tool, end to end, in its own processes
|
|
324
|
+
.github/ CI: the unit tests and a LaTeX check on every push
|
|
325
|
+
papers/ the four preprints (LaTeX + figures)
|
|
326
|
+
results/ every number in every paper (JSON)
|
|
327
|
+
pipeline/ corpora and frozen-LM passes \
|
|
328
|
+
memory/ the memory systems (papers 1-3) | paper
|
|
329
|
+
fastweights/ the readout adapter (paper 4) | reproduction
|
|
330
|
+
eval/ evaluations, controls, diagnostics |
|
|
331
|
+
figures/ figure generation /
|
|
332
|
+
data/ dumps/ regenerable artifacts (gitignored, ~2 GB)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Version 1.0 merged the two former scripts into one tool: `assistant.py` →
|
|
336
|
+
`sillage read` / `complete`, `papers_assistant.py` → `sillage papers` / `ask`,
|
|
337
|
+
`demo.py` → `sillage demo`. Old `memory_state/` directories are still read.
|
|
338
|
+
The research scripts keep their own bootstrap header, so any of them runs from
|
|
339
|
+
anywhere and resolves `data/`, `dumps/`, `results/` identically.
|
|
340
|
+
</details>
|
|
341
|
+
|
|
342
|
+
<details>
|
|
343
|
+
<summary><b>Caveats worth knowing before you try it</b></summary>
|
|
344
|
+
|
|
345
|
+
- The papers' numbers were measured on two frozen models (GPT-2 124M,
|
|
346
|
+
Qwen3-0.6B) at CPU scale. The tool accepts any causal LM and calibrates
|
|
347
|
+
its readout for it, but nothing here has been measured above 1B parameters,
|
|
348
|
+
and the semantic tier has only been validated on Qwen3.
|
|
349
|
+
- The memory captures **surface repetition**; paraphrase recall is what the
|
|
350
|
+
semantic tier partially addresses, and it remains the open frontier.
|
|
351
|
+
- A fixed matrix saturates at long horizons (~0.5 writes per parameter);
|
|
352
|
+
forgetting (×2.3, `--half-life`) and capacity (×3.4) are the measured
|
|
353
|
+
remedies.
|
|
354
|
+
- `sillage forget <file>` removes a document from the index, not from the
|
|
355
|
+
matrices: Hebbian traces are superposed, so only `--all` or forgetting
|
|
356
|
+
removes those. The tool says so rather than pretending otherwise.
|
|
357
|
+
- The papers' *Manuscripts* stream (unpublished drafts) is not redistributed —
|
|
358
|
+
drop your own documents in `manuscripts/` to run that protocol.
|
|
359
|
+
</details>
|
|
360
|
+
|
|
361
|
+
## Citation
|
|
362
|
+
|
|
363
|
+
```bibtex
|
|
364
|
+
@article{sghairi2026sillage,
|
|
365
|
+
title = {Sillage: Surprise-Gated Amplitude Memory
|
|
366
|
+
for Frozen Language Models},
|
|
367
|
+
author = {Sghairi, Abderrahmane},
|
|
368
|
+
year = {2026},
|
|
369
|
+
doi = {10.5281/zenodo.22079016},
|
|
370
|
+
url = {https://doi.org/10.5281/zenodo.22079016}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
MIT licensed. Issues and questions welcome.
|