NODEMIND commited on
Commit
aef5ed5
·
0 Parent(s):

SHADOW 250M Instruct

Browse files
.gitattributes ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
+ banner.jpg filter=lfs diff=lfs merge=lfs -text
37
+ framework.png filter=lfs diff=lfs merge=lfs -text
38
+ pirate.shdw filter=lfs diff=lfs merge=lfs -text
39
+ shadow.exe filter=lfs diff=lfs merge=lfs -text
40
+ shadow250m_instruct.shdw filter=lfs diff=lfs merge=lfs -text
41
+ tokenizer/new2old.u32 filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ viewer: false
4
+ language:
5
+ - en
6
+ pipeline_tag: text-generation
7
+ tags:
8
+ - small
9
+ - shadow
10
+ - instruct
11
+ - chat
12
+ - long-context
13
+ - retrieval
14
+ - cpu
15
+ ---
16
+
17
+ <h1 align="center">SHADOW 250M Instruct</h1>
18
+
19
+ ![shadow](banner.jpg)
20
+
21
+ <p align="center">
22
+ Instruct model of SHADOW 250M · 250M Parameters · 100M-Token Offline Context · 60 MB
23
+ </p>
24
+
25
+ **SHADOW 250M Instruct** is a 250 million parameter language model built from scratch,
26
+ trained on 30 billion tokens of English text with about 0.7 billion further tokens of
27
+ instruction tuning. The complete deployment is 60 MB, vocabulary included. It runs at about 400 tokens per
28
+ second on a laptop CPU and uses about 80 MB of RAM.
29
+
30
+ Language modeling quality of the base model, measured on held-out English web text that
31
+ was never seen in training (educational web pages, 2,048 token windows): cross entropy
32
+ 3.15 nats per token, perplexity 23.3, 0.99 bits per byte.
33
+
34
+ Next to its 2,048 token attention window, SHADOW can use an offline archive: a compressed
35
+ cache of up to 100 million tokens that stays on your hard disk. The model was trained from
36
+ step 1 to retrieve from this archive, all the way up to 100 million tokens. Due to a
37
+ limited budget it was not trained to reason over the tokens in the disk cache. It finds
38
+ facts and reads them back; it does not think across them.
39
+
40
+ For scale, the official released weight files of well-known small models, next to ours:
41
+
42
+ | model | parameters | weights on disk |
43
+ |---|---|---|
44
+ | GPT-2 | 124M | 548 MB |
45
+ | SmolLM2-135M-Instruct | 135M | 269 MB |
46
+ | SmolLM2-360M-Instruct | 360M | 724 MB |
47
+ | Qwen2.5-0.5B-Instruct | 494M | 988 MB |
48
+ | **SHADOW 250M Instruct** | **250M** | **60 MB, vocabulary included** |
49
+
50
+ Sizes are the main weight file of each official repository, as published. None of the
51
+ models above reads a 100 million token archive from disk.
52
+
53
+ *(The project was named after my first dog, Shadow, as it is my first big project.)*
54
+
55
+ ---
56
+
57
+ ## Some samples
58
+
59
+ Every sample shows the exact settings it was generated with, using the files in this repo.
60
+ Nothing on this page was made with settings other than the ones shown.
61
+
62
+ ### 1.
63
+
64
+ Prompt: "Explain photosynthesis in two sentences."<br>
65
+ Settings: greedy decoding (temperature 0)<br>
66
+ Answer:
67
+ > Photosynthesis is a process in which plants convert sunlight into chemical energy, which
68
+ > is then used to produce oxygen and other chemicals. This process is called photosynthesis.
69
+
70
+ ### 2.
71
+
72
+ Prompt: "Write a short poem about the sea."<br>
73
+ Settings: temperature 0.25 · top-k 30 · repetition penalty 1.15 · seed 2<br>
74
+ Answer:
75
+ > The waves had swept over, and they were crashing against each other like rocks on top of
76
+ > one another. The wind was strong but not very gentle as it tried to push them towards
77
+ > their destination without lifting any weight or pulling away from those who stood in
78
+ > front of her with a stiff smile that made the sea look even more majestic than before.
79
+
80
+ ### 3. Reading its archive
81
+
82
+ Prompt: "What is the serial number of device Grus-189?" The fact sits 50.6 million tokens
83
+ deep in the 100M evaluation archive on disk.<br>
84
+ Settings: archive mode as shipped, retrieval k=16<br>
85
+ Answer:
86
+ > SN-442976
87
+
88
+ ---
89
+
90
+ ## Benchmarks
91
+
92
+ All numbers are our own, measured on held-out data with the harness shipped in
93
+ `benchmarks/`, together with the raw results. We compare with nobody on this page; the
94
+ harness runs on any archive of yours as well.
95
+
96
+ Standard, acc_norm, zero-shot, full sets, greedy scoring:
97
+
98
+ | PIQA | HellaSwag | ARC-Easy | ARC-Challenge |
99
+ |---|---|---|---|
100
+ | 0.60 | 0.34 | 0.41 | 0.24 |
101
+
102
+ Long context, answered from the archive on disk. Settings: archive mode as shipped,
103
+ retrieval k=16, exact match scoring, archives and facts held out from all training:
104
+
105
+ | | 1M tokens | 10M tokens | 100M tokens |
106
+ |---|---|---|---|
107
+ | Needle in a haystack (5 depths) | 0.98 | 0.98 | 0.98 |
108
+ | Needle with look-alike distractors | 1.00 | 1.00 | – |
109
+ | Multi-key needles | 1.00 | 1.00 | – |
110
+ | Two-hop variable tracking | 1.00 | 1.00 | – |
111
+ | Scattered story facts, latest wins | 1.00 | 1.00 | – |
112
+ | Fact QA, 6 task types with abstain | 0.97 | 0.95 | 0.83 |
113
+
114
+ ## Architecture
115
+
116
+ | Hyperparameter | Value |
117
+ |---|---|
118
+ | Hidden size | 1536 |
119
+ | Layers | 10 |
120
+ | Attention heads | 24 (GQA, 2 KV heads) |
121
+ | Head dim | 64 |
122
+ | Intermediate size (SwiGLU) | 4224 |
123
+ | Vocab size | 131,072 (frozen, 0 trainable parameters) |
124
+ | Positional encoding | RoPE θ=10,000 |
125
+ | Normalization | RMSNorm, ε=10-6 (incl. QK-Norm) |
126
+ | Tied embeddings | Yes (shared vocabulary table) |
127
+ | Attention window | 2,048 tokens + offline archive up to 100M |
128
+ | Body weight precision | under 2 bits per weight |
129
+ | Parameters | 250M |
130
+ | Runtime | bundled CPU kernel (AVX2/AVX-512), no framework needed |
131
+
132
+ ![framework](framework.png)
133
+
134
+ ## Performance
135
+
136
+ Measured on a laptop CPU with 8 physical cores, using the exact files in this repo. The
137
+ bundled kernel handles chat, the two-tier KV cache, and a live memory panel (`--status`).
138
+
139
+ | | |
140
+ |---|---|
141
+ | decode speed, 8 threads | 402 tokens/s |
142
+ | decode speed, 4 / 2 / 1 threads | 393 / 275 / 158 tokens/s |
143
+ | prefill speed | 409 tokens/s |
144
+ | RAM while chatting | ~80 MB |
145
+ | archive index build (once per archive, at load) | 2 s at 1M · 21 s at 10M · 3.2 min at 100M |
146
+ | retrieval per question | 37 ms at 10M · 435 ms at 100M |
147
+ | archive question, end to end | 0.45 s at 100M |
148
+
149
+ ## Fine-tuning
150
+
151
+ Yes, you can fine-tune it, on one GPU, and export your own 52 MB model for CPU. We did it
152
+ ourselves as a demonstration: 90 minutes on a laptop GPU turned SHADOW into a pirate
153
+ assistant, with benchmark scores unchanged. The full guide with the commands, the dataset,
154
+ and the before and after results is in [finetune/FINETUNING.md](finetune/FINETUNING.md).
155
+
156
+ > The capital of France be Paris. It is a UNESCO World Heritage Site... Yarr!
157
+
158
+ ## Repository layout
159
+
160
+ deployment/ the model: weights, vocabulary, and the runtime binaries
161
+ shadow250m_instruct.shdw 52 MB weights
162
+ fp131072.npy 8.4 MB vocabulary
163
+ bin/windows/ bin/linux/ prebuilt CPU runtimes (macOS on request)
164
+ tokenizer/ 3 files, 5 MB
165
+ finetune/ master weights, training script, exporter, guide, worked example
166
+ benchmarks/ results, report, harness
167
+ shadow_runtime/ archive question answering (Python)
168
+
169
+ ## Usage
170
+
171
+ Easiest start, any system:
172
+
173
+ python shadow_chat.py
174
+
175
+ Chat directly with the binary, no Python needed. Windows:
176
+
177
+ deploymentin\windows\shadow.exe deployment\shadow250m_instruct.shdw deployment p131072.npy --chat
178
+
179
+ Linux:
180
+
181
+ deployment/bin/linux/shadow deployment/shadow250m_instruct.shdw deployment/fp131072.npy --chat
182
+
183
+ Add --status to either for a live memory panel. Ask a question against an archive (a folder
184
+ holding a tokens.u32 stream):
185
+
186
+ python -m shadow_runtime --model shadow250m_instruct.shdw --table fp131072.npy \
187
+ --archive path/to/archive --ask "your question"
188
+
189
+ Python:
190
+
191
+ from shadow_runtime import Engine
192
+ eng = Engine("shadow250m_instruct.shdw", "fp131072.npy", archive="path/to/archive")
193
+ print(eng.answer("your question"))
194
+
195
+ ### Chat template
196
+
197
+ <start_of_turn>user
198
+ {message}<end_of_turn>
199
+ <start_of_turn>model
200
+ {response}<end_of_turn>
201
+
202
+ ## Intended use
203
+
204
+ Intended:
205
+
206
+ * Local assistants on CPU-only hardware, fully offline
207
+ * Question answering over large private text archives: logs, books, documentation
208
+ * Fine-tuning your own small assistant on one GPU
209
+ * Research and education on small models and long context
210
+
211
+ Not intended:
212
+
213
+ * Production or user-facing deployment without human review
214
+ * Factual question answering from the model's own memory, advice, or decision support
215
+ * Non-English text
216
+
217
+ ## Limitations and bias
218
+
219
+ * Small. At 250M parameters, open facts, arithmetic, and long answers are weak. Expect
220
+ mistakes outside the archive.
221
+ * The model retrieves and reads from its archive. It was not trained to reason across
222
+ many archive documents; that needs a bigger training budget than this project had.
223
+ Two-hop chains degrade at 100M tokens.
224
+ * Trained on public web text, so its outputs can carry the biases of that text.
225
+ * English only.
226
+
227
+ ## Contact
228
+
229
+ Questions, results, or something you built with it: saikiranbathula1@gmail.com
230
+
231
+ ---
232
+
233
+ *© NODEMIND 2026*
banner.jpg ADDED

Git LFS Details

  • SHA256: da3a44ecc5c8a859fe98879b4a20a9498802c924b3c76eea27c52014f5b4b434
  • Pointer size: 131 Bytes
  • Size of remote file: 197 kB
benchmarks/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Benchmarks
2
+
3
+ `results.json` holds every number on the model card; `report.pdf` is the full report.
4
+ The evaluation archives are held out and not distributed.
5
+
6
+ `run.py` is the harness that produced the numbers. You can point it at your own archive
7
+ (a folder with a `tokens.u32` stream and a `bank_valid.jsonl` question bank in the same
8
+ format as `results.json` describes) to run the same evaluation on your own data.
benchmarks/embedding_bench.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark the frozen binary vocabulary on word similarity (WordSim-353).
2
+
3
+ Every token has a fixed 512-bit code. If the codes carry meaning, similar words should
4
+ have nearby codes. This script scores each human-rated word pair by Hamming similarity
5
+ between the two words' codes and reports the Spearman correlation with the human ratings.
6
+ Random codes score about 0. Runs offline on the files in this repo.
7
+
8
+ python benchmarks/embedding_bench.py
9
+ """
10
+ import csv, pathlib, sys
11
+ import numpy as np
12
+ HERE = pathlib.Path(__file__).resolve().parent; ROOT = HERE.parent
13
+ sys.path.insert(0, str(ROOT))
14
+ from shadow_runtime.retriever import enc
15
+
16
+ fp = np.unpackbits(np.load(ROOT / "fp131072.npy"), axis=1)[:, :512]
17
+ rows = [l.split(",") for l in open(HERE / "wordsim353.csv", encoding="utf-8").read().splitlines() if l]
18
+ rng = np.random.default_rng(0); rand = rng.integers(0, 2, size=fp.shape).astype(np.uint8)
19
+
20
+ def score(table):
21
+ xs, ys = [], []
22
+ for w1, w2, human in rows:
23
+ i1, i2 = enc(" " + w1.lower()), enc(" " + w2.lower())
24
+ if len(i1) != 1 or len(i2) != 1: continue
25
+ xs.append(1 - np.mean(table[i1[0]] != table[i2[0]])); ys.append(float(human))
26
+ from scipy.stats import spearmanr
27
+ return spearmanr(xs, ys).statistic, len(xs)
28
+
29
+ r, n = score(fp); r0, _ = score(rand)
30
+ print(f"WordSim-353, single-token pairs (n={n})")
31
+ print(f" frozen vocabulary codes : Spearman {r:.3f}")
32
+ print(f" random codes (baseline) : Spearman {r0:.3f}")
benchmarks/report.pdf ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ %PDF-1.4
2
+ %���� ReportLab Generated PDF document (opensource)
3
+ 1 0 obj
4
+ <<
5
+ /F1 2 0 R /F2 3 0 R /F3 4 0 R
6
+ >>
7
+ endobj
8
+ 2 0 obj
9
+ <<
10
+ /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
11
+ >>
12
+ endobj
13
+ 3 0 obj
14
+ <<
15
+ /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
16
+ >>
17
+ endobj
18
+ 4 0 obj
19
+ <<
20
+ /BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
21
+ >>
22
+ endobj
23
+ 5 0 obj
24
+ <<
25
+ /Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources <<
26
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
27
+ >> /Rotate 0 /Trans <<
28
+
29
+ >>
30
+ /Type /Page
31
+ >>
32
+ endobj
33
+ 6 0 obj
34
+ <<
35
+ /Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources <<
36
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
37
+ >> /Rotate 0 /Trans <<
38
+
39
+ >>
40
+ /Type /Page
41
+ >>
42
+ endobj
43
+ 7 0 obj
44
+ <<
45
+ /PageMode /UseNone /Pages 9 0 R /Type /Catalog
46
+ >>
47
+ endobj
48
+ 8 0 obj
49
+ <<
50
+ /Author (\(anonymous\)) /CreationDate (D:20260820230714+10'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260820230714+10'00') /Producer (ReportLab PDF Library - \(opensource\))
51
+ /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
52
+ >>
53
+ endobj
54
+ 9 0 obj
55
+ <<
56
+ /Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages
57
+ >>
58
+ endobj
59
+ 10 0 obj
60
+ <<
61
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 2711
62
+ >>
63
+ stream
64
+ Gb"/)?ZXu3&V'PQfX[E>dME97_#Nq'grj!+(L<N_f1ouR!g/RtL]h5iZD)9p!@Uj1/Q&TkmW>=SLgtC*]8PM,mVc1Hi.;?hrldub]*ETX'*_.g@1(XW5!44E%hDark7J%0+k]%JJW#^'_t3o/RJB*"'giP,0Hg`,%jN'($kFXDpE/;mpt'uq$/@7_P%$?GZP8jVdr<>q2D8dP:dA7uNnekIn23=UKngbYQF)6`J3lJXp&m&5N'[1?O&ld?8@F7@@>Nns`/p6cO&q)"5-roGcWml2[m(Jb#CC;-RsXUlSe;YVYog]aV6"dZP0[4%%p9Q8&L+>7HU^EAfRS?&^oe1YCVUtuN=j13`;DZM`dDaF12"QfX3^ub96[%EYgRX57KfSnciR)m%$2"[436*\O;jM'Rdo'9.sM/99mMm>SZE0%D*ZQA_/;Q@-6k:7E1L=Q<a0@!o-;fPKal$&Y-B)E@1n=g.Ls;aIPk=YD$0`7dQQ'JMqNXhX[KuA%.p'XqhUbC3Qu1`Qr6W6^j=;43@kQ,eN@oa;4;))i'QtHIT@@orCOi2P3#U!1f:94,X+eOcl0IfS^OfQC0qlZ^pO5(SJGHaOFe@B!3NOeIkkZX\QV8`Gf5<X7Pj^<b68RJ^cWIk!S)_@2rGh\5F^,n&L/+&M&5rD2$:IFEQ\U>FiRm$WV$#k.]+`ASsTR!68i2-Ks:t=M760_A/.50R*o4H=E?X7Q5Am,cY*>ajQmh]NaBmu(+o6=_8o>A6>DidE[>b,R#2l^:hs)\Qr^N"rb8IU\#(;CnAM=9LfHHD'hh^Qf7pP>;cth')-*`W\'n>S[&LQJJ[=E1B)MS0p1iXLD/skqE\oG*PEI&9SiJBBGKo^_d%$"3)Xr%X?K^uF?#9#=fE>4e7brr8*58MMQC0:!C\C-1UDY@!2#jT/Pq]jj!.L<.3)3p"@8ku4#1F[fXL.rA[3h#Qh;@H(!(SXKqX-gLdbHG2HI6'h'0)3<Ak2<A#sj#.r/9(oE`oW0#u+.n+4Wfm"=:dS>bk#(ZsZC(GX7"1*-i@"&GC7l=E-G,!l0@G83D>ErjQ7kItZ;`N9(3F/qCAV0>/D<.*q9\*$!Scf(/:ZP%C.!:sp6-)P,0^JkT_Z^OGr*E!=#T55oFi[MIae?n-u(.nC]IVM'VZqgFOe]<Ipp1-C<#JDL("4!"t4ZW4t&q"B;fdDod*HG.htjnDOm^5S<5o]1!2m4dnYj2]1Th2b70NA./giaPrmoXaG-%?Wp@]^_YVE\X;cks(*7D+!>j/Mj2_hi]cU.V)"+b@/4FQL4,OO?-o/Qck]\M,rD>)UK5D(0o&fjiI\OD),ZfMHlmpVEH7a=#<ptlY?KLEi,#3pNOASj?#%q]Q<]*OdZteZ:[[hV)1/%YBe-*gFsCB1SqnlS4euNgID!08]+uP'FD\0,)OgkBN9&<O6#@PbP",4YgGZ&0Z\/7;jqD(.\g1RC'RHQ4g3p]2W"UkV_dOQT+=_io2X;Nj7hZ\?`l)14Z`Oiq=tBnY\k%B\\f9H=MVTr\Qsg!pUL3-rM7>O6kBh;E9S#&K9f3*4X,_d;J:#NLM"ji&`]fb>GJVim*lZoY@D;1m9rNZ!`B9?d+^oTs*PNd"`Xq'%Pnr#:8(7<m+cgc\>#@D3;.0#J)'1e!a7=GZQ^L5Pm0=h.U=c4RMIrHEhRB$M/8@jE_0EZkma`I,eXDuSlc/R0k#$QXsb)kb\ginS#tI[`RMQQm(H@$q4:+#E4Z<e`WW>9Tgj!nabl"El0RaUbqtYaB6,lPf\sW+XS'5Wi4c!U!BVk)%eQ8Ao2,iTV_]K]jr%,KE<AOBT_kp8_._,oM)90=XX5>qMG#Z(c?kCl3WM\/4@/IjWE#XOCXnq.QRk'RC0n2iSZ1ZZY]$Bdfn$7CI<q@2cFf7/S2P=sP2Tet?ke?5<::`q#;aWT[^BUO1Nt_*UtUfl46oW64^^LMc983Mq<@RS$*%Gm8bAP?LSolQb$mLqk&6JrV&?fo<\UP6B?P.%@Qc<nKtBoq0F;VE<KfO>_@DF6n"K7?NlF#\m+oi%E:S%+d6_ZL>KD*m*IKdkga%h?Fr_Fi,5:('0*oiE<Vg]M$On`8JB+SBm3UbIo\,5apF$<_MpLhNh[_t_k=Tqq*1qEOl\KJmr]9$@+@@OJQ]aLUPRMR0"Y)3Hh:&NV/Hr9J?V<]35:6#YPu"KU4Y)Y'F%C6',X[ao25K\]rf<=.*61QrkW<g?U%jrB0So1R$-]X.+k^p(o_(g^#MMBIrm;&MhJ,.]hcoY[oY)Un"n0D-ed(&gT-3k234!Ru"!cNbo8pXZWsK"S$Mn^-Cg;*315NS!gKs>D!f%VH,Y?8thZF3J*$Z)`j?$RjDoWoX"ci&h:-7S>-2lr=ER&>K\,aou(4s/8T^(9&2r6S;6k/_;l\N:]L[5eNjf6[TZs]@HNd<S4d(&AU%lN]s_LjW.FVF##Ps2jka3WJ<4V);s*R;PSISWr8TBlL=Dba!ED"hiX*Fh4ueP<R_^,`P/7pA(cgpXC-B(/-OSD3$.Y1.ZFa-,(TP^g'`>=W#RjTcR2F1_A'ieMXOo7cp^q$ROtq)79dpD*_3bQn)7i7[i\s0Dm?[m'L$kRQe7!rY`f2V<jL>\[JtUSTlKp[#>u*FoEgqaZ%glM#5bAH,\,TijQ7qdGjgYu$36[%[F&%.n/R'QB0L#j=Zjb/FfPd=UQT$g*j.ne^crY5SUuhKlj~>endstream
65
+ endobj
66
+ 11 0 obj
67
+ <<
68
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 1444
69
+ >>
70
+ stream
71
+ GatU29on$e&A@sB]M$Z.^h-""bA'gEg9_,D#dQ23b?ejrCb_5HLSWMerq_50mBsFh363.WJ`Q=6R5Z3#7t_/\s"@U7m0p2UF)Z]`!^'ltAiKL+2VRQ]/.Dj'DO7130/AQc:V.#O##pU>@p'r^NgMUdjW(TB='trUo>dbY+O]HuDN%mshAP_F_;d-3Z>QLE3J^4:P@uuhr#2oFR(_4!EWG5NbE=tP!FEqq5oa2*2VQnu@cp9'$Pm@m8(ip:NE?d&s0Kn27YFaF'M2?0l*Qge_F*+EY]r#48C9'*?E'l)9'd7;,YO76/.E#[O*8&F/BL>4GWLp.j6_gM#ZcUd?G@;DZE.]s62DO*nW-2FI6IZJEP[MX.:)uqSO\aiS0^:Y?\87tp_4njoX]k&s+gO7k2n5a61PiM8$'cbbPj8KoWn$@,+-C-d6@P$fF>q;3KJl1<O9B/K:h6S9rC,u2N<0YqHq=+o,m1ITB,J\1TAV;&W;Q[=?_['^7H@G[ssB>K$E>/C*.(f^M`P;5E(UuU6G!#c!fqZjr@?J_@f'SoJ?%$W*^'jSEE8J"70H9"=rJB86%?'/<1.u<"(emK>Z\VnG,XZ*XB<u%YO_*KBRRN5G,YLjD(&i6f&U:6hXuDO5t(T5oWU4X5q'^nVlQGfQ+7#Q"!fFEHp>j$(e!B(pa[SDTQ*OpTf@H;DntBd\.uO3_L2uW7iGfE#(lN62+p!E2@@V^:'nCeu&&R_q,$S[S^S/WO&mu(u9!o&)PIW/#2&qA#\0P('X8#`1gt/k#q/0->E!'=TbGHBh+uX8:/c2@:<t&]u/ZknlV&%.m6m72mRL>h@T*dduf:2cBG>;(+@FRX7YuNRUK51/2\[n<Nr%fWhgA)==f=d8pW^L9pQR7'%URqY<*5(UO3N-MEKQmC#uuWdE!nBR!Z-P&pDWd.[5(R=2F4BPgTf]#i@[`!U<aA'l>\_SpJ7B0bo<gc<I-3BVdBJ0t^a#!4)GN9@A9FH&P5h0IV9(Cm9*Q/54N[%4j*P#5qd<&A&N/`\<JM$fo<hF6BCJ:DJ^rhRg%l8b^0>?0-YuBn(=i*lJ=36dF)JMAf%Q2Um)fYXFZti?;kNP+`l\mG+]K'fi\O!OPb-ClE^=+bAC&U'iN4TY#ABej5&WFRqu4R]8*<Y'N$ABf.mU?V?Hf2-!^U[4Bti.U9+,ThKWd=rXBPZ!2C^jjHJ#Cq9-HShSqt/!UUUKQgX>PhZer-93S/d;jE5f*)g3^]ZB)]2qC'9;PqXkO@:1DcWZ$ObQrXh=Y^"1lb2'X*lG?#2s$lK$S*tF-qY/]bdu:G"EHA=7gHnZHjY+agB+orcb(&6Mt0$Ps\n[/bNh_<#SsMM(Le2I%*'l>[_M2>tM*<0mqCmAS#NOimL4p6bQ:34Li5&?V\eBU:%2P-TO#%VlO*]0%<6q)7&Mf0V4*#VaZ-`ImY#9p]~>endstream
72
+ endobj
73
+ xref
74
+ 0 12
75
+ 0000000000 65535 f
76
+ 0000000061 00000 n
77
+ 0000000112 00000 n
78
+ 0000000219 00000 n
79
+ 0000000331 00000 n
80
+ 0000000408 00000 n
81
+ 0000000612 00000 n
82
+ 0000000816 00000 n
83
+ 0000000884 00000 n
84
+ 0000001164 00000 n
85
+ 0000001229 00000 n
86
+ 0000004032 00000 n
87
+ trailer
88
+ <<
89
+ /ID
90
+ [<945055cca686fd3777c3df99ffdf03c1><945055cca686fd3777c3df99ffdf03c1>]
91
+ % ReportLab generated PDF document -- digest (opensource)
92
+
93
+ /Info 8 0 R
94
+ /Root 7 0 R
95
+ /Size 12
96
+ >>
97
+ startxref
98
+ 5568
99
+ %%EOF
benchmarks/results.json ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "banks": {
3
+ "1M": {
4
+ "T1": "12/12",
5
+ "T2": "5/5",
6
+ "T3": "5/5",
7
+ "T4": "4/5",
8
+ "T5": "2/2",
9
+ "T7": "9/9",
10
+ "ALL": 0.974
11
+ },
12
+ "10M": {
13
+ "T1": "49/50",
14
+ "T2": "32/33",
15
+ "T3": "25/28",
16
+ "T4": "10/14",
17
+ "T5": "24/24",
18
+ "T7": "23/23",
19
+ "ALL": 0.948
20
+ },
21
+ "100M": {
22
+ "T1": "487/517",
23
+ "T2": "348/374",
24
+ "T3": "90/229",
25
+ "T4": "102/177",
26
+ "T5": "169/169",
27
+ "T7": "216/231",
28
+ "ALL": 0.832
29
+ }
30
+ },
31
+ "niah": {
32
+ "1M": {
33
+ "0.1": 0.875,
34
+ "0.3": 1.0,
35
+ "0.5": 1.0,
36
+ "0.7": 1.0,
37
+ "0.9": 1.0,
38
+ "overall": 0.975
39
+ },
40
+ "10M": {
41
+ "0.1": 0.875,
42
+ "0.3": 1.0,
43
+ "0.5": 1.0,
44
+ "0.7": 1.0,
45
+ "0.9": 1.0,
46
+ "overall": 0.975
47
+ },
48
+ "100M": {
49
+ "0.1": 0.875,
50
+ "0.3": 1.0,
51
+ "0.5": 1.0,
52
+ "0.7": 1.0,
53
+ "0.9": 1.0,
54
+ "overall": 0.975
55
+ }
56
+ },
57
+ "ruler": {
58
+ "1M": {
59
+ "S-NIAH-1": 1.0,
60
+ "S-NIAH-2": 1.0,
61
+ "MK-NIAH": 1.0,
62
+ "VT": 1.0,
63
+ "overall": 1.0
64
+ },
65
+ "10M": {
66
+ "S-NIAH-1": 1.0,
67
+ "S-NIAH-2": 1.0,
68
+ "MK-NIAH": 1.0,
69
+ "VT": 1.0,
70
+ "overall": 1.0
71
+ }
72
+ },
73
+ "babilong": {
74
+ "1M": {
75
+ "qa1": 1.0,
76
+ "qa2": 1.0,
77
+ "overall": 1.0
78
+ },
79
+ "10M": {
80
+ "qa1": 1.0,
81
+ "qa2": 1.0,
82
+ "overall": 1.0
83
+ }
84
+ }
85
+ }
benchmarks/run.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reproduce the model card numbers. Expects archive directories under data/archives/{1m,10m,100m}
2
+ (tokens.u32 + meta.json + a question bank), which are not distributed with this repository.
3
+ python benchmarks/run.py [--tiers 1M,10M,100M]
4
+ """
5
+ import sys, os, json, time, random, pathlib
6
+ import numpy as np
7
+ HERE = pathlib.Path(__file__).resolve().parent; ROOT = HERE.parent
8
+ sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "shadow_runtime"))
9
+ from shadow_runtime.retriever import load_archive, Inverted, enc, _dec
10
+ from shadow_runtime.answer_engine import Engine
11
+ from shadow_runtime.prompt import normalize
12
+
13
+ TIERS = {"1M": "1m", "10M": "10m", "100M": "100m"}
14
+ sel = "1M,10M,100M"
15
+ if "--tiers" in sys.argv: sel = sys.argv[sys.argv.index("--tiers") + 1]
16
+ out = {}
17
+ for tier in sel.split(","):
18
+ path = ROOT / "data" / "archives" / TIERS[tier]
19
+ tok, meta, bank = load_archive(str(path))
20
+ t0 = time.time(); inv = Inverted(tok)
21
+ print(f"[{tier}] index {len(tok)//64} blocks in {time.time()-t0:.0f}s", flush=True)
22
+ eng = Engine(tok, inv)
23
+ per = {}
24
+ for b in bank:
25
+ a, how = eng.answer(b["question"])
26
+ ok = normalize(a) == normalize(str(b["answer"]))
27
+ d = per.setdefault(b["task"], [0, 0]); d[0] += ok; d[1] += 1
28
+ tot = sum(v[0] for v in per.values()); n = sum(v[1] for v in per.values())
29
+ out[tier] = {t: f"{v[0]}/{v[1]}" for t, v in sorted(per.items())}; out[tier]["ALL"] = round(tot / n, 3)
30
+ print(f"[{tier}] " + " ".join(f"{t} {v[0]}/{v[1]}" for t, v in sorted(per.items())) + f" ALL {tot/n:.2f}", flush=True)
31
+ json.dump(out, open(HERE / "my_results.json", "w"), indent=1)
32
+ print("wrote benchmarks/my_results.json")
benchmarks/wordsim353.csv ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ love,sex,6.77
2
+ tiger,cat,7.35
3
+ tiger,tiger,10.00
4
+ book,paper,7.46
5
+ computer,keyboard,7.62
6
+ computer,internet,7.58
7
+ plane,car,5.77
8
+ train,car,6.31
9
+ telephone,communication,7.50
10
+ television,radio,6.77
11
+ media,radio,7.42
12
+ drug,abuse,6.85
13
+ bread,butter,6.19
14
+ cucumber,potato,5.92
15
+ doctor,nurse,7.00
16
+ professor,doctor,6.62
17
+ student,professor,6.81
18
+ smart,student,4.62
19
+ smart,stupid,5.81
20
+ company,stock,7.08
21
+ stock,market,8.08
22
+ stock,phone,1.62
23
+ stock,CD,1.31
24
+ stock,jaguar,0.92
25
+ stock,egg,1.81
26
+ fertility,egg,6.69
27
+ stock,live,3.73
28
+ stock,life,0.92
29
+ book,library,7.46
30
+ bank,money,8.12
31
+ wood,forest,7.73
32
+ money,cash,9.15
33
+ professor,cucumber,0.31
34
+ king,cabbage,0.23
35
+ king,queen,8.58
36
+ king,rook,5.92
37
+ bishop,rabbi,6.69
38
+ Jerusalem,Israel,8.46
39
+ Jerusalem,Palestinian,7.65
40
+ holy,sex,1.62
41
+ fuck,sex,9.44
42
+ Maradona,football,8.62
43
+ football,soccer,9.03
44
+ football,basketball,6.81
45
+ football,tennis,6.63
46
+ tennis,racket,7.56
47
+ Arafat,peace,6.73
48
+ Arafat,terror,7.65
49
+ Arafat,Jackson,2.50
50
+ law,lawyer,8.38
51
+ movie,star,7.38
52
+ movie,popcorn,6.19
53
+ movie,critic,6.73
54
+ movie,theater,7.92
55
+ physics,proton,8.12
56
+ physics,chemistry,7.35
57
+ space,chemistry,4.88
58
+ alcohol,chemistry,5.54
59
+ vodka,gin,8.46
60
+ vodka,brandy,8.13
61
+ drink,car,3.04
62
+ drink,ear,1.31
63
+ drink,mouth,5.96
64
+ drink,eat,6.87
65
+ baby,mother,7.85
66
+ drink,mother,2.65
67
+ car,automobile,8.94
68
+ gem,jewel,8.96
69
+ journey,voyage,9.29
70
+ boy,lad,8.83
71
+ coast,shore,9.10
72
+ asylum,madhouse,8.87
73
+ magician,wizard,9.02
74
+ midday,noon,9.29
75
+ furnace,stove,8.79
76
+ food,fruit,7.52
77
+ bird,cock,7.10
78
+ bird,crane,7.38
79
+ tool,implement,6.46
80
+ brother,monk,6.27
81
+ crane,implement,2.69
82
+ lad,brother,4.46
83
+ journey,car,5.85
84
+ monk,oracle,5.00
85
+ cemetery,woodland,2.08
86
+ food,rooster,4.42
87
+ coast,hill,4.38
88
+ forest,graveyard,1.85
89
+ shore,woodland,3.08
90
+ monk,slave,0.92
91
+ coast,forest,3.15
92
+ lad,wizard,0.92
93
+ chord,smile,0.54
94
+ glass,magician,2.08
95
+ noon,string,0.54
96
+ rooster,voyage,0.62
97
+ money,dollar,8.42
98
+ money,cash,9.08
99
+ money,currency,9.04
100
+ money,wealth,8.27
101
+ money,property,7.57
102
+ money,possession,7.29
103
+ money,bank,8.50
104
+ money,deposit,7.73
105
+ money,withdrawal,6.88
106
+ money,laundering,5.65
107
+ money,operation,3.31
108
+ tiger,jaguar,8.00
109
+ tiger,feline,8.00
110
+ tiger,carnivore,7.08
111
+ tiger,mammal,6.85
112
+ tiger,animal,7.00
113
+ tiger,organism,4.77
114
+ tiger,fauna,5.62
115
+ tiger,zoo,5.87
116
+ psychology,psychiatry,8.08
117
+ psychology,anxiety,7.00
118
+ psychology,fear,6.85
119
+ psychology,depression,7.42
120
+ psychology,clinic,6.58
121
+ psychology,doctor,6.42
122
+ psychology,Freud,8.21
123
+ psychology,mind,7.69
124
+ psychology,health,7.23
125
+ psychology,science,6.71
126
+ psychology,discipline,5.58
127
+ psychology,cognition,7.48
128
+ planet,star,8.45
129
+ planet,constellation,8.06
130
+ planet,moon,8.08
131
+ planet,sun,8.02
132
+ planet,galaxy,8.11
133
+ planet,space,7.92
134
+ planet,astronomer,7.94
135
+ precedent,example,5.85
136
+ precedent,information,3.85
137
+ precedent,cognition,2.81
138
+ precedent,law,6.65
139
+ precedent,collection,2.50
140
+ precedent,group,1.77
141
+ precedent,antecedent,6.04
142
+ cup,coffee,6.58
143
+ cup,tableware,6.85
144
+ cup,article,2.40
145
+ cup,artifact,2.92
146
+ cup,object,3.69
147
+ cup,entity,2.15
148
+ cup,drink,7.25
149
+ cup,food,5.00
150
+ cup,substance,1.92
151
+ cup,liquid,5.90
152
+ jaguar,cat,7.42
153
+ jaguar,car,7.27
154
+ energy,secretary,1.81
155
+ secretary,senate,5.06
156
+ energy,laboratory,5.09
157
+ computer,laboratory,6.78
158
+ weapon,secret,6.06
159
+ FBI,fingerprint,6.94
160
+ FBI,investigation,8.31
161
+ investigation,effort,4.59
162
+ Mars,water,2.94
163
+ Mars,scientist,5.63
164
+ news,report,8.16
165
+ canyon,landscape,7.53
166
+ image,surface,4.56
167
+ discovery,space,6.34
168
+ water,seepage,6.56
169
+ sign,recess,2.38
170
+ Wednesday,news,2.22
171
+ mile,kilometer,8.66
172
+ computer,news,4.47
173
+ territory,surface,5.34
174
+ atmosphere,landscape,3.69
175
+ president,medal,3.00
176
+ war,troops,8.13
177
+ record,number,6.31
178
+ skin,eye,6.22
179
+ Japanese,American,6.50
180
+ theater,history,3.91
181
+ volunteer,motto,2.56
182
+ prejudice,recognition,3.00
183
+ decoration,valor,5.63
184
+ century,year,7.59
185
+ century,nation,3.16
186
+ delay,racism,1.19
187
+ delay,news,3.31
188
+ minister,party,6.63
189
+ peace,plan,4.75
190
+ minority,peace,3.69
191
+ attempt,peace,4.25
192
+ government,crisis,6.56
193
+ deployment,departure,4.25
194
+ deployment,withdrawal,5.88
195
+ energy,crisis,5.94
196
+ announcement,news,7.56
197
+ announcement,effort,2.75
198
+ stroke,hospital,7.03
199
+ disability,death,5.47
200
+ victim,emergency,6.47
201
+ treatment,recovery,7.91
202
+ journal,association,4.97
203
+ doctor,personnel,5.00
204
+ doctor,liability,5.19
205
+ liability,insurance,7.03
206
+ school,center,3.44
207
+ reason,hypertension,2.31
208
+ reason,criterion,5.91
209
+ hundred,percent,7.38
210
+ Harvard,Yale,8.13
211
+ hospital,infrastructure,4.63
212
+ death,row,5.25
213
+ death,inmate,5.03
214
+ lawyer,evidence,6.69
215
+ life,death,7.88
216
+ life,term,4.50
217
+ word,similarity,4.75
218
+ board,recommendation,4.47
219
+ governor,interview,3.25
220
+ OPEC,country,5.63
221
+ peace,atmosphere,3.69
222
+ peace,insurance,2.94
223
+ territory,kilometer,5.28
224
+ travel,activity,5.00
225
+ competition,price,6.44
226
+ consumer,confidence,4.13
227
+ consumer,energy,4.75
228
+ problem,airport,2.38
229
+ car,flight,4.94
230
+ credit,card,8.06
231
+ credit,information,5.31
232
+ hotel,reservation,8.03
233
+ grocery,money,5.94
234
+ registration,arrangement,6.00
235
+ arrangement,accommodation,5.41
236
+ month,hotel,1.81
237
+ type,kind,8.97
238
+ arrival,hotel,6.00
239
+ bed,closet,6.72
240
+ closet,clothes,8.00
241
+ situation,conclusion,4.81
242
+ situation,isolation,3.88
243
+ impartiality,interest,5.16
244
+ direction,combination,2.25
245
+ street,place,6.44
246
+ street,avenue,8.88
247
+ street,block,6.88
248
+ street,children,4.94
249
+ listing,proximity,2.56
250
+ listing,category,6.38
251
+ cell,phone,7.81
252
+ production,hike,1.75
253
+ benchmark,index,4.25
254
+ media,trading,3.88
255
+ media,gain,2.88
256
+ dividend,payment,7.63
257
+ dividend,calculation,6.48
258
+ calculation,computation,8.44
259
+ currency,market,7.50
260
+ OPEC,oil,8.59
261
+ oil,stock,6.34
262
+ announcement,production,3.38
263
+ announcement,warning,6.00
264
+ profit,warning,3.88
265
+ profit,loss,7.63
266
+ dollar,yen,7.78
267
+ dollar,buck,9.22
268
+ dollar,profit,7.38
269
+ dollar,loss,6.09
270
+ computer,software,8.50
271
+ network,hardware,8.31
272
+ phone,equipment,7.13
273
+ equipment,maker,5.91
274
+ luxury,car,6.47
275
+ five,month,3.38
276
+ report,gain,3.63
277
+ investor,earning,7.13
278
+ liquid,water,7.89
279
+ baseball,season,5.97
280
+ game,victory,7.03
281
+ game,team,7.69
282
+ marathon,sprint,7.47
283
+ game,series,6.19
284
+ game,defeat,6.97
285
+ seven,series,3.56
286
+ seafood,sea,7.47
287
+ seafood,food,8.34
288
+ seafood,lobster,8.70
289
+ lobster,food,7.81
290
+ lobster,wine,5.70
291
+ food,preparation,6.22
292
+ video,archive,6.34
293
+ start,year,4.06
294
+ start,match,4.47
295
+ game,round,5.97
296
+ boxing,round,7.61
297
+ championship,tournament,8.36
298
+ fighting,defeating,7.41
299
+ line,insurance,2.69
300
+ day,summer,3.94
301
+ summer,drought,7.16
302
+ summer,nature,5.63
303
+ day,dawn,7.53
304
+ nature,environment,8.31
305
+ environment,ecology,8.81
306
+ nature,man,6.25
307
+ man,woman,8.30
308
+ man,governor,5.25
309
+ murder,manslaughter,8.53
310
+ soap,opera,7.94
311
+ opera,performance,6.88
312
+ life,lesson,5.94
313
+ focus,life,4.06
314
+ production,crew,6.25
315
+ television,film,7.72
316
+ lover,quarrel,6.19
317
+ viewer,serial,2.97
318
+ possibility,girl,1.94
319
+ population,development,3.75
320
+ morality,importance,3.31
321
+ morality,marriage,3.69
322
+ Mexico,Brazil,7.44
323
+ gender,equality,6.41
324
+ change,attitude,5.44
325
+ family,planning,6.25
326
+ opera,industry,2.63
327
+ sugar,approach,0.88
328
+ practice,institution,3.19
329
+ ministry,culture,4.69
330
+ problem,challenge,6.75
331
+ size,prominence,5.31
332
+ country,citizen,7.31
333
+ planet,people,5.75
334
+ development,issue,3.97
335
+ experience,music,3.47
336
+ music,project,3.63
337
+ glass,metal,5.56
338
+ aluminum,metal,7.83
339
+ chance,credibility,3.88
340
+ exhibit,memorabilia,5.31
341
+ concert,virtuoso,6.81
342
+ rock,jazz,7.59
343
+ museum,theater,7.19
344
+ observation,architecture,4.38
345
+ space,world,6.53
346
+ preservation,world,6.19
347
+ admission,ticket,7.69
348
+ shower,thunderstorm,6.31
349
+ shower,flood,6.03
350
+ weather,forecast,8.34
351
+ disaster,area,6.25
352
+ governor,office,6.34
353
+ architecture,century,3.78
deployment/bin/linux/shadow ADDED
Binary file (90.7 kB). View file
 
deployment/bin/windows/shadow.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a54b2cd2b0973bb6545eb54f6e5b14e3561707374abc2ecb1ea5ebdb748591b3
3
+ size 143848
deployment/fp131072.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:efb7672a55e4998a8c0395cdec1c2207acf750e3ad1109088a61f66586da1d34
3
+ size 8388736
deployment/shadow250m_instruct.shdw ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e0ad7893e0b4b3b420d8ee5d8dd7a1b7dd499fc698ba1b3a3fa40a7a0d5b9ea
3
+ size 52342802
finetune/FINETUNING.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fine-tune SHADOW 250M
2
+
3
+ You can fine-tune SHADOW on your own data with one GPU and one command, then export your model
4
+ as a 52 MB file that runs at hundreds of tokens per second on a plain CPU. This page shows the
5
+ full workflow with a real example we ran ourselves, results attached.
6
+
7
+ ## What you need
8
+
9
+ * One GPU with 12 GB or more (a gaming laptop works; we used one)
10
+ * Python with torch, numpy, sentencepiece
11
+ * The files in this repo: `shadow250m_instruct.pt`, `fp131072.npy`, `modeling/`, `finetune.py`, `export_model.py`
12
+
13
+ ## 1. Prepare your data
14
+
15
+ One conversation per line in a .jsonl file:
16
+
17
+ {"messages": [{"role": "user", "content": "your question"}, {"role": "assistant", "content": "the answer you want"}]}
18
+
19
+ A few hundred conversations are enough for a style change. A few thousand for a domain.
20
+ `examples_pirate.jsonl` in this repo is the dataset used in the example below.
21
+
22
+ ## 2. Train
23
+
24
+ python finetune.py --data my_data.jsonl --steps 150 --out my_model
25
+
26
+ Defaults: learning rate 1e-5 with cosine decay, loss only on assistant tokens, batch of
27
+ 32,768 tokens per step, quantisation kept in the loop so the exported model matches the
28
+ trained one. A small validation split is held out automatically and printed before and after.
29
+
30
+ | hardware | speed | 150 steps |
31
+ |---|---|---|
32
+ | laptop RTX 3080 (thermally limited) | ~0.9k tokens/s | ~90 min |
33
+ | A100 | ~7.7k tokens/s | ~12 min |
34
+
35
+ ## 3. Export and run on CPU
36
+
37
+ python export_model.py my_model/finetuned.pt my_model.shdw
38
+ ./shadow my_model.shdw fp131072.npy --chat
39
+
40
+ The export prints a round-trip check against the trained model. Your fine-tuned model is now
41
+ a 52 MB file.
42
+
43
+ ## The example we ran: pirate mode
44
+
45
+ 472 conversations, assistant answers rewritten in pirate speech. 150 steps, 90 minutes on a
46
+ laptop GPU. Validation loss on the pirate data went from 2.570 to 2.512. PIQA on a 500-item
47
+ subset stayed level: 0.61 before, 0.63 after. Same prompts, greedy decoding, before and after:
48
+
49
+ **"What is the capital of France?"**
50
+
51
+ Before:
52
+ > The capital of France is Paris. It is the seat of the French Republic, and it is known for
53
+ > its historical landmarks, including the Eiffel Tower, the Louvre Museum...
54
+
55
+ After:
56
+ > The capital of France be Paris. It is a UNESCO World Heritage Site and a major
57
+ > international city. It has a rich history and a rich culture. Paris be home to the Louvre
58
+ > Museum...
59
+
60
+ **"Give three tips for staying focused while studying."**
61
+
62
+ Before:
63
+ > 1. Aim for a balance of time and energy. Aim for 10-15 minutes of focused work per day...
64
+
65
+ After:
66
+ > When confronted with a problem, staying focused means focusing on what needs to be done
67
+ > first. Blimey, to stay motivated, it's essential to set realistic goals and keep the
68
+ > momentum going...
69
+
70
+ The knowledge stays, the voice changes. Swap the pirate data for your support tone, your
71
+ domain, or your language style and the same 90 minutes applies.
72
+
73
+ ## Tips
74
+
75
+ * Style: 100 to 500 pairs, 100 to 200 steps.
76
+ * Domain knowledge: thousands of pairs, 300 to 1000 steps, and consider mixing 30 to 50
77
+ percent general chat data so the model does not forget how to converse.
78
+ * Learning rate: stay at or below 2e-5. This model trains with quantisation active and high
79
+ rates make it unstable.
80
+ * Check before and after on prompts you care about, with greedy decoding, like we did above.
finetune/examples_pirate.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
finetune/export_model.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export a fine-tuned checkpoint to the 52 MB deploy file for the CPU runtime.
2
+ python export_model.py my_model/finetuned.pt my_model.shdw
3
+ """
4
+ import os, sys, pathlib
5
+ for k, v in {"SHADOW_D": "1536", "SHADOW_NL": "10", "SHADOW_NH": "24", "SHADOW_NKV": "2", "SHADOW_HD": "64",
6
+ "SHADOW_FFNH": "4224", "SHADOW_FAST_ATTN": "1", "SHADOW_KV_BITS": "1", "SHADOW_KV_TWO_TIER": "1"}.items():
7
+ os.environ.setdefault(k, v)
8
+ HERE = pathlib.Path(__file__).resolve().parent
9
+ sys.path.insert(0, str(HERE / "modeling"))
10
+ import subprocess
11
+ src, dst = sys.argv[1], sys.argv[2]
12
+ tmp = dst + ".full"
13
+ r = subprocess.run([sys.executable, str(HERE / "modeling" / "export_ternary.py"), src, tmp],
14
+ env={**os.environ, "PYTHONPATH": str(HERE / "modeling")})
15
+ if r.returncode: sys.exit(r.returncode)
16
+ r = subprocess.run([sys.executable, str(HERE / "modeling" / "repack_shdw.py"), tmp, dst, "--fp16"])
17
+ os.remove(tmp)
18
+ print("wrote", dst)
finetune/finetune.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fine-tune SHADOW 250M Instruct on your own chat data, on a single GPU (8 GB is enough).
2
+
3
+ Data: a .jsonl file, one conversation per line:
4
+ {"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
5
+
6
+ Run:
7
+ python finetune.py --data my_data.jsonl --steps 300 --out my_model
8
+ python export_model.py my_model/finetuned.pt my_model.shdw # 52 MB deploy file
9
+ ./shadow my_model.shdw fp131072.npy --chat # your model, on CPU
10
+
11
+ Defaults are safe for style and domain fine-tunes: low learning rate, loss only on assistant
12
+ tokens, quantisation kept in the loop so the exported model behaves like the trained one.
13
+ """
14
+ import argparse, json, math, os, sys, time, random, pathlib
15
+ for k, v in {"SHADOW_D": "1536", "SHADOW_NL": "10", "SHADOW_NH": "24", "SHADOW_NKV": "2", "SHADOW_HD": "64",
16
+ "SHADOW_FFNH": "4224", "SHADOW_FAST_ATTN": "1", "SHADOW_KV_BITS": "1", "SHADOW_KV_TWO_TIER": "1"}.items():
17
+ os.environ.setdefault(k, v)
18
+ HERE = pathlib.Path(__file__).resolve().parent
19
+ sys.path.insert(0, str(HERE)); sys.path.insert(0, str(HERE / "modeling")); sys.path.insert(0, str(HERE / "shadow_runtime"))
20
+ import numpy as np, torch, torch.nn.functional as F
21
+ import common
22
+ from common import requant
23
+ from model_250m import Shadow250M
24
+ from retriever import enc
25
+
26
+ BOS, EOS, SOT, EOT = 2, 1, 8, 9
27
+
28
+ def get_args():
29
+ ap = argparse.ArgumentParser()
30
+ ap.add_argument("--data", required=True, help="jsonl with {'messages': [...]} per line")
31
+ ap.add_argument("--init", default=str(HERE / "shadow250m_instruct.pt"))
32
+ ap.add_argument("--table", default=str(HERE / "fp131072.npy"))
33
+ ap.add_argument("--out", default="finetuned")
34
+ ap.add_argument("--steps", type=int, default=300)
35
+ ap.add_argument("--lr", type=float, default=1e-5)
36
+ ap.add_argument("--ctx", type=int, default=2048)
37
+ ap.add_argument("--micro-batch", type=int, default=2)
38
+ ap.add_argument("--accum", type=int, default=8)
39
+ ap.add_argument("--warmup", type=int, default=20)
40
+ ap.add_argument("--val-frac", type=float, default=0.02)
41
+ ap.add_argument("--log-every", type=int, default=10)
42
+ ap.add_argument("--seed", type=int, default=0)
43
+ return ap.parse_args()
44
+
45
+ def build_ids(messages):
46
+ ids, msk = [BOS], [0]
47
+ for m in messages:
48
+ role = "user" if m["role"] != "assistant" else "model"
49
+ head = [SOT] + enc(role + "\n"); ids += head; msk += [0] * len(head)
50
+ body = enc(m["content"]) + [EOT] + enc("\n")
51
+ ids += body
52
+ msk += ([1] * (len(body) - 1) + [0]) if role == "model" else [0] * len(body)
53
+ return ids, msk
54
+
55
+ class Packer:
56
+ def __init__(s, path, ctx, rng, val_frac):
57
+ s.ex = []
58
+ for line in open(path, encoding="utf-8"):
59
+ line = line.strip()
60
+ if not line: continue
61
+ ids, msk = build_ids(json.loads(line)["messages"])
62
+ s.ex.append((np.asarray(ids, np.int64), np.asarray(msk, np.int64)))
63
+ rng.shuffle(s.ex)
64
+ if len(s.ex) < 2: raise SystemExit("need at least 2 conversations in the data file")
65
+ nval = min(max(1, int(len(s.ex) * val_frac)), max(1, len(s.ex) // 5))
66
+ s.val = s.ex[:nval]; s.train = s.ex[nval:]; s.ctx = ctx; s.rng = rng
67
+ print(f"data: {len(s.train)} train / {len(s.val)} val conversations")
68
+ def pack(s, B, val=False):
69
+ pool = s.val if val else s.train
70
+ X = np.zeros((B, s.ctx), np.int64); Y = np.full((B, s.ctx), -100, np.int64)
71
+ for r in range(B):
72
+ pos = 0
73
+ while pos < s.ctx:
74
+ ids, m = pool[s.rng.randrange(len(pool))]
75
+ ids, m = ids[:s.ctx - pos], m[:s.ctx - pos]
76
+ X[r, pos:pos + len(ids)] = ids
77
+ tgt = np.full(len(ids), -100, np.int64); tgt[:-1] = np.where(m[1:] == 1, ids[1:], -100)
78
+ Y[r, pos:pos + len(ids)] = tgt; pos += len(ids)
79
+ if pos > s.ctx * 0.9: break
80
+ return torch.tensor(X), torch.tensor(Y)
81
+
82
+ def main():
83
+ a = get_args(); rng = random.Random(a.seed); torch.manual_seed(a.seed)
84
+ dev = torch.device("cuda" if torch.cuda.is_available() else "cpu")
85
+ out = pathlib.Path(a.out); out.mkdir(parents=True, exist_ok=True)
86
+ _of = common.RVQ.forward
87
+ def _tern(s, x):
88
+ if s.g == 32:
89
+ w = s.weight; sc = 1.0 / w.abs().mean(dim=1, keepdim=True).clamp_(min=1e-5)
90
+ return F.linear(x, (w + ((w * sc).round().clamp(-1, 1) / sc - w).detach()).to(x.dtype))
91
+ return _of(s, x)
92
+ common.RVQ.forward = _tern
93
+ _oenc = common.RVQ.enc
94
+ def _enc2(s):
95
+ if s.g == 32: return
96
+ _oenc(s)
97
+ common.RVQ.enc = _enc2
98
+ fp = np.unpackbits(np.load(a.table), axis=1)[:, :512]
99
+ cent = torch.tensor(fp.astype(np.float32) * 2 - 1, device=dev); cent_n = F.normalize(cent, dim=-1)
100
+ model = Shadow250M(cent, cent_n, cent.shape[0]).to(dev)
101
+ ck = torch.load(a.init, map_location=dev, weights_only=False)
102
+ sd = {k: v.float() if v.is_floating_point() else v for k, v in ck["model"].items()}
103
+ model.load_state_dict(sd); requant(model)
104
+ for md in model.modules():
105
+ if isinstance(md, common.KVCodec1): md.eval()
106
+ print(f"loaded {a.init} on {dev}")
107
+ data = Packer(a.data, a.ctx, rng, a.val_frac)
108
+ opt = torch.optim.AdamW(model.parameters(), lr=a.lr, betas=(0.9, 0.95), weight_decay=0.0)
109
+ def loss_of(x, y):
110
+ h, _ = model.trunk(x); ph = model.head(h).float().reshape(-1, 512)
111
+ yf = y.reshape(-1); v = yf >= 0; ph = ph[v]; yf = yf[v]
112
+ ce = 0.0
113
+ for i in range(0, ph.shape[0], 8192):
114
+ lg = ph[i:i + 8192] @ model.cent_n.T + model.tied_bias
115
+ ce = ce + F.cross_entropy(lg, yf[i:i + 8192], reduction="sum")
116
+ return ce / max(1, int(v.sum()))
117
+ @torch.no_grad()
118
+ def val():
119
+ model.eval(); tot = 0.0
120
+ for _ in range(4):
121
+ x, y = data.pack(a.micro_batch, val=True)
122
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=dev.type == "cuda"):
123
+ tot += float(loss_of(x.to(dev), y.to(dev)))
124
+ model.train()
125
+ for md in model.modules():
126
+ if isinstance(md, common.KVCodec1): md.eval()
127
+ return tot / 4
128
+ v0 = val(); print(f"step 0 val loss {v0:.4f}")
129
+ t0 = time.time()
130
+ for step in range(1, a.steps + 1):
131
+ lr = a.lr * min(1.0, step / a.warmup) * (0.5 * (1 + math.cos(math.pi * step / a.steps)))
132
+ for g in opt.param_groups: g["lr"] = lr
133
+ opt.zero_grad(set_to_none=True)
134
+ for _ in range(a.accum):
135
+ x, y = data.pack(a.micro_batch)
136
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=dev.type == "cuda"):
137
+ (loss_of(x.to(dev), y.to(dev)) / a.accum).backward()
138
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
139
+ opt.step(); requant(model)
140
+ if step % a.log_every == 0:
141
+ el = time.time() - t0
142
+ print(f"step {step:>4} lr {lr:.2e} {el/step:.1f}s/step eta {(a.steps-step)*el/step/60:.0f}min", flush=True)
143
+ v1 = val()
144
+ torch.save({"model": model.state_dict()}, out / "finetuned.pt")
145
+ print(f"done val loss {v0:.4f} -> {v1:.4f} saved {out/'finetuned.pt'}")
146
+
147
+ if __name__ == "__main__":
148
+ main()
finetune/modeling/common.py ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math, time, numpy as np, torch, torch.nn as nn, torch.nn.functional as F
2
+ from contextlib import contextmanager
3
+ try:
4
+ from paged_kv import PagedKVArchive, PagedKVView, ExactChunkCountView
5
+ except ImportError:
6
+ from paged_kv import PagedKVArchive, PagedKVView, ExactChunkCountView
7
+
8
+
9
+ import os as _os
10
+ def _e(k,d): return int(_os.environ.get(k,d))
11
+
12
+ D=_e("SHADOW_D",1024); NL=_e("SHADOW_NL",20); NH=_e("SHADOW_NH",16)
13
+ NKV=_e("SHADOW_NKV",4); HD=_e("SHADOW_HD",64); FFNH=_e("SHADOW_FFNH",3072); FPD=512
14
+ FAST_ATTN=bool(int(__import__('os').environ.get('SHADOW_FAST_ATTN','0')))
15
+ TRAIN_EXACT_POT=bool(int(__import__('os').environ.get(
16
+ 'SHADOW_TRAIN_EXACT_POT','1')))
17
+ TRAIN_EXACT_KV=bool(int(__import__('os').environ.get(
18
+ 'SHADOW_TRAIN_EXACT_KV','1')))
19
+ KV_BITS=_e("SHADOW_KV_BITS",1)
20
+ KV_TWO_TIER=bool(int(_os.environ.get("SHADOW_KV_TWO_TIER","0")))
21
+ KV_COLD_MASK=None
22
+
23
+
24
+ class _ExactSTE(torch.autograd.Function):
25
+ @staticmethod
26
+ def forward(ctx,x,q):
27
+ return q
28
+ @staticmethod
29
+ def backward(ctx,grad):
30
+ return grad,None
31
+
32
+ def ste(x,q): return _ExactSTE.apply(x,q)
33
+ def pot(x):
34
+ m=x.abs().amax(-1,keepdim=True).clamp_min(1e-6); s=torch.exp2(torch.ceil(torch.log2(m/127.)))
35
+ return ste(x,(x/s).round().clamp(-127,127)*s)
36
+
37
+ def walsh_hadamard(x):
38
+ n=x.shape[-1]
39
+ if n<1 or n&(n-1):
40
+ raise ValueError(f"Walsh-Hadamard width must be a power of two, got {n}")
41
+ shape=x.shape
42
+ y=x
43
+ h=1
44
+ while h<n:
45
+ y=y.reshape(*shape[:-1],n//(2*h),2,h)
46
+ a,b=y[...,0,:],y[...,1,:]
47
+ y=torch.cat((a+b,a-b),-1).reshape(shape)
48
+ h*=2
49
+ return y/math.sqrt(n)
50
+
51
+ def _kv2_levels(x):
52
+ m=x.abs().amax(-1,keepdim=True).clamp_min(1e-6)
53
+ scale=torch.exp2(torch.ceil(torch.log2(m/1.5)))
54
+ return ((x/scale).round().clamp(-2,1)+0.5)*scale
55
+
56
+ def kv2(x):
57
+ rotated=walsh_hadamard(x)
58
+ restored=walsh_hadamard(_kv2_levels(rotated))
59
+ return ste(x,restored)
60
+
61
+ def kv2_pack(x):
62
+ if x.shape[-1] % 4:
63
+ raise ValueError(f"2-bit packing requires a multiple-of-4 last dimension, got {x.shape[-1]}")
64
+ rotated=walsh_hadamard(x)
65
+ m=rotated.abs().amax(-1,keepdim=True).clamp_min(1e-6)
66
+ scale=torch.exp2(torch.ceil(torch.log2(m/1.5)))
67
+ code=(rotated/scale).round().clamp(-2,1).to(torch.int16).add_(2).to(torch.uint8)
68
+ z=code.reshape(*code.shape[:-1],code.shape[-1]//4,4)
69
+ packed=z[...,0] | (z[...,1]<<2) | (z[...,2]<<4) | (z[...,3]<<6)
70
+ return packed.contiguous(),scale.to(x.dtype).contiguous()
71
+
72
+ def kv2_unpack(packed,scale,dtype=None):
73
+ parts=torch.stack((packed&3,(packed>>2)&3,(packed>>4)&3,(packed>>6)&3),-1)
74
+ code=parts.reshape(*packed.shape[:-1],packed.shape[-1]*4)
75
+ dt=dtype or scale.dtype
76
+ rotated=(code.to(dt)-1.5)*scale.to(dt)
77
+ return walsh_hadamard(rotated)
78
+
79
+
80
+ class KVCodec1(nn.Module):
81
+ def __init__(s,heads,width,seed=0,momentum=0.01,decision_grid=256.0):
82
+ super().__init__()
83
+ if width<1 or width&(width-1):
84
+ raise ValueError(f"KVCodec1 width must be a power of two, got {width}")
85
+ g=torch.Generator().manual_seed(int(seed))
86
+ sign=(torch.randint(0,2,(heads,width),generator=g,dtype=torch.int8)*2-1)
87
+ s.heads,s.width,s.momentum=int(heads),int(width),float(momentum)
88
+ s.decision_grid=float(decision_grid)
89
+ s.register_buffer("sign",sign)
90
+ s.register_buffer("mu",torch.zeros(heads,width))
91
+ s.register_buffer("ctv",torch.zeros(heads,width))
92
+ s.register_buffer("low",torch.full((heads,width),-1.0))
93
+ s.register_buffer("high",torch.ones(heads,width))
94
+ s.register_buffer("initialized",torch.tensor(False))
95
+ s.register_buffer("updates",torch.tensor(0,dtype=torch.int64),persistent=False)
96
+
97
+ def _check(s,x):
98
+ if x.ndim!=4 or x.shape[1]!=s.heads or x.shape[-1]!=s.width:
99
+ raise ValueError(
100
+ f"KVCodec1 expects (B,{s.heads},T,{s.width}), got {tuple(x.shape)}")
101
+
102
+ @torch.no_grad()
103
+ def calibrate(s,x):
104
+ s._check(x); xf=x.detach().float()
105
+ mu=xf.mean((0,2))
106
+ sg=s.sign.float()[None,:,None,:]
107
+ z=walsh_hadamard((xf-mu[None,:,None,:])*sg)
108
+ flat=z.permute(1,0,2,3).reshape(s.heads,-1,s.width)
109
+ decision=(flat*s.decision_grid).round()/s.decision_grid
110
+ ctv=decision.median(1).values
111
+ hi=decision>ctv[:,None,:]; lo=~hi
112
+ low=(flat*lo).sum(1)/lo.sum(1).clamp_min(1)
113
+ high=(flat*hi).sum(1)/hi.sum(1).clamp_min(1)
114
+ rate=1.0 if not bool(s.initialized) else s.momentum
115
+ for dst,src in ((s.mu,mu),(s.ctv,ctv),(s.low,low),(s.high,high)):
116
+ dst.lerp_(src.to(dst.dtype),rate)
117
+ s.initialized.fill_(True); s.updates.add_(1)
118
+ return s
119
+
120
+ def _ensure(s,x,update):
121
+ if update or not bool(s.initialized):
122
+ s.calibrate(x)
123
+
124
+ def transform(s,x):
125
+ s._check(x)
126
+ sg=s.sign.to(x.dtype)[None,:,None,:]
127
+ return walsh_hadamard((x-s.mu.to(x.dtype)[None,:,None,:])*sg)
128
+
129
+ def bits(s,x,update=False):
130
+ s._ensure(x,update)
131
+ decision=(s.transform(x)*s.decision_grid).round()/s.decision_grid
132
+ threshold=(s.ctv.to(x.dtype)*s.decision_grid).round()/s.decision_grid
133
+ return decision>threshold[None,:,None,:]
134
+
135
+ def reconstruct_bits(s,bits,dtype=torch.float32):
136
+ if bits.ndim!=4 or bits.shape[1]!=s.heads or bits.shape[-1]!=s.width:
137
+ raise ValueError(f"bad 1-bit KV shape {tuple(bits.shape)}")
138
+ low=s.low.to(dtype)[None,:,None,:]
139
+ high=s.high.to(dtype)[None,:,None,:]
140
+ rotated=torch.where(bits,high,low)
141
+ sg=s.sign.to(dtype)[None,:,None,:]
142
+ return s.mu.to(dtype)[None,:,None,:]+walsh_hadamard(rotated)*sg
143
+
144
+ def forward(s,x):
145
+ bits=s.bits(x,update=s.training)
146
+ return ste(x,s.reconstruct_bits(bits,x.dtype))
147
+
148
+ @torch.no_grad()
149
+ def pack(s,x):
150
+ bits=s.bits(x,update=False)
151
+ b=bits.reshape(*bits.shape[:-1],s.width//8,8).to(torch.uint8)
152
+ weights=torch.tensor((1,2,4,8,16,32,64,128),
153
+ dtype=torch.uint8,device=x.device)
154
+ return (b*weights).sum(-1).to(torch.uint8).contiguous()
155
+
156
+ def unpack(s,packed,dtype=None):
157
+ if packed.ndim!=4 or packed.shape[1]!=s.heads or packed.shape[-1]*8!=s.width:
158
+ raise ValueError(f"bad packed 1-bit KV shape {tuple(packed.shape)}")
159
+ parts=torch.stack(tuple(((packed>>i)&1) for i in range(8)),-1)
160
+ bits=parts.reshape(*packed.shape[:-1],s.width).bool()
161
+ return s.reconstruct_bits(bits,dtype or s.mu.dtype)
162
+
163
+
164
+ @contextmanager
165
+ def freeze_codec_updates(module):
166
+ codecs=[m for m in module.modules() if isinstance(m,KVCodec1)]
167
+ states=[m.training for m in codecs]
168
+ try:
169
+ for m in codecs:
170
+ m.training=False
171
+ yield
172
+ finally:
173
+ for m,state in zip(codecs,states):
174
+ m.training=state
175
+
176
+
177
+ def mem8_pack(x):
178
+ m=x.abs().amax(-1,keepdim=True).clamp_min(1e-6)
179
+ scale=torch.exp2(torch.ceil(torch.log2(m/127.0)))
180
+ return (x/scale).round().clamp(-127,127).to(torch.int8),scale.to(x.dtype)
181
+
182
+
183
+ def mem8_unpack(packed,scale,dtype=None):
184
+ dt=dtype or scale.dtype
185
+ return packed.to(dt)*scale.to(dt)
186
+
187
+
188
+ def bitpack512(x):
189
+ if x.shape[-1] != FPD:
190
+ raise ValueError(f"Hamming keys require {FPD} features, got {x.shape[-1]}")
191
+ bits=(x>=0).to(torch.uint8).reshape(*x.shape[:-1],FPD//8,8)
192
+ weights=torch.tensor((1,2,4,8,16,32,64,128),dtype=torch.uint8,device=x.device)
193
+ return (bits*weights).sum(-1).to(torch.uint8).contiguous()
194
+
195
+
196
+ class HammingHebbianMemory:
197
+ def __init__(s,batch,dim,device,capacity=4096,match_threshold=0.90,
198
+ hebb_eta=0.25,retrieval_k=4):
199
+ s.batch,s.dim,s.device=int(batch),int(dim),torch.device(device)
200
+ s.capacity=int(capacity); s.match_threshold=float(match_threshold)
201
+ s.hebb_eta=float(hebb_eta); s.retrieval_k=int(retrieval_k)
202
+ s.keys=[[] for _ in range(s.batch)]
203
+ s.vals=[[] for _ in range(s.batch)]
204
+ s.scales=[[] for _ in range(s.batch)]
205
+ s.strength=[[] for _ in range(s.batch)]
206
+ s.age=[[] for _ in range(s.batch)]; s.clock=0
207
+ s._lut=torch.tensor([int(i).bit_count() for i in range(256)],
208
+ dtype=torch.int16,device=s.device)
209
+
210
+ def __len__(s):
211
+ return sum(len(x) for x in s.keys)
212
+
213
+ def counts(s):
214
+ return [len(x) for x in s.keys]
215
+
216
+ def _distances(s,key,b):
217
+ if not s.keys[b]:
218
+ return torch.empty(0,dtype=torch.int32,device=s.device)
219
+ bank=torch.stack(s.keys[b])
220
+ xor=torch.bitwise_xor(bank,key).to(torch.long)
221
+ return s._lut[xor].sum(-1,dtype=torch.int32)
222
+
223
+ @torch.no_grad()
224
+ def write(s,values,key_features):
225
+ if values.shape!=(s.batch,s.dim) or key_features.shape!=(s.batch,FPD):
226
+ raise ValueError(f"write shapes must be {(s.batch,s.dim)} and "
227
+ f"{(s.batch,FPD)}, got {values.shape}, {key_features.shape}")
228
+ packed_keys=bitpack512(key_features)
229
+ events=[]
230
+ for b in range(s.batch):
231
+ s.clock+=1; key=packed_keys[b].detach(); value=values[b].detach()
232
+ distances=s._distances(key,b)
233
+ best=int(distances.argmin()) if len(distances) else -1
234
+ similarity=(1.0-float(distances[best])/FPD) if best>=0 else -1.0
235
+ if best>=0 and similarity>=s.match_threshold:
236
+ old=mem8_unpack(s.vals[b][best],s.scales[b][best],value.dtype)
237
+ rate=s.hebb_eta*similarity
238
+ updated=old+rate*(value-old)
239
+ vp,vs=mem8_pack(updated)
240
+ s.vals[b][best]=vp; s.scales[b][best]=vs
241
+ s.strength[b][best]+=similarity; s.age[b][best]=s.clock
242
+ events.append({"kind":"reinforce","slot":best,
243
+ "similarity":similarity})
244
+ continue
245
+ vp,vs=mem8_pack(value)
246
+ if len(s.keys[b])>=s.capacity:
247
+
248
+ slot=min(range(len(s.keys[b])),
249
+ key=lambda i:(s.strength[b][i],s.age[b][i]))
250
+ s.keys[b][slot]=key; s.vals[b][slot]=vp; s.scales[b][slot]=vs
251
+ s.strength[b][slot]=1.0; s.age[b][slot]=s.clock
252
+ kind="replace"
253
+ else:
254
+ slot=len(s.keys[b]); s.keys[b].append(key)
255
+ s.vals[b].append(vp); s.scales[b].append(vs)
256
+ s.strength[b].append(1.0); s.age[b].append(s.clock)
257
+ kind="allocate"
258
+ events.append({"kind":kind,"slot":slot,"similarity":similarity})
259
+ return events
260
+
261
+ @torch.no_grad()
262
+ def retrieve(s,key_features,k=None):
263
+ if key_features.shape!=(s.batch,FPD):
264
+ raise ValueError(f"query shape must be {(s.batch,FPD)}, got {key_features.shape}")
265
+ packed=bitpack512(key_features); outputs=[]; metadata=[]
266
+ for b in range(s.batch):
267
+ distances=s._distances(packed[b],b)
268
+ if not len(distances):
269
+ outputs.append(torch.zeros(s.dim,device=s.device,
270
+ dtype=key_features.dtype))
271
+ metadata.append({"found":False,"slot":-1,"similarity":0.0})
272
+ continue
273
+ take=min(int(k or s.retrieval_k),len(distances))
274
+ d,idx=torch.topk(distances,take,largest=False)
275
+ values=torch.stack([
276
+ mem8_unpack(s.vals[b][int(i)],s.scales[b][int(i)],
277
+ key_features.dtype) for i in idx])
278
+ similarity=1.0-d.to(torch.float32)/FPD
279
+ weights=torch.softmax(similarity*32.0,-1).to(values.dtype)
280
+ outputs.append((values*weights[:,None]).sum(0))
281
+ metadata.append({"found":True,"slot":int(idx[0]),
282
+ "similarity":float(similarity[0]),
283
+ "distance":int(d[0])})
284
+ return torch.stack(outputs),metadata
285
+
286
+
287
+ def make_memory_state(batch,dim,device,chunk_size=128,capacity=256,
288
+ match_threshold=0.90,hebb_eta=0.25,retrieval_k=4):
289
+ if chunk_size<1:
290
+ raise ValueError("memory chunk_size must be positive")
291
+ return {
292
+ "store":HammingHebbianMemory(
293
+ batch,chunk_size*dim,device,capacity=capacity,
294
+ match_threshold=match_threshold,hebb_eta=hebb_eta,
295
+ retrieval_k=retrieval_k),
296
+ "evicted":torch.empty(batch,0,dim,device=device),
297
+ "chunk_size":int(chunk_size),
298
+ "hidden_dim":int(dim),
299
+ "recall_threshold":0.60,
300
+ "writes":0,
301
+ "last_retrieval":None,
302
+ }
303
+
304
+
305
+ @torch.no_grad()
306
+ def memory_absorb_evicted(memory,evicted,head,norm):
307
+ if memory is None or not evicted.shape[1]:
308
+ return []
309
+ memory["evicted"]=torch.cat((memory["evicted"],evicted.detach()),1)
310
+ events=[]
311
+ n=int(memory["chunk_size"])
312
+ while memory["evicted"].shape[1]>=n:
313
+ chunk=memory["evicted"][:,:n]
314
+ memory["evicted"]=memory["evicted"][:,n:]
315
+ pooled=chunk.mean(1)
316
+ key_features=head(norm(pooled)).float()
317
+
318
+
319
+ value=chunk.reshape(chunk.shape[0],-1)
320
+ events.extend(memory["store"].write(value,key_features))
321
+ memory["writes"]+=1
322
+ return events
323
+
324
+
325
+ @torch.no_grad()
326
+ def memory_append_recall(memory,query_hidden,context,head,norm):
327
+ if memory is None or not len(memory["store"]):
328
+ return context
329
+ query=head(norm(query_hidden)).float()
330
+ recall,meta=memory["store"].retrieve(query)
331
+ memory["last_retrieval"]=meta
332
+
333
+
334
+ threshold=float(memory["recall_threshold"])
335
+ if not any(m["found"] and m["similarity"]>=threshold for m in meta):
336
+ return context
337
+ gate=torch.tensor([m["similarity"] if m["found"] and
338
+ m["similarity"]>=threshold else 0.0 for m in meta],
339
+ device=recall.device,dtype=recall.dtype)[:,None,None]
340
+ recalled_chunk=recall.reshape(
341
+ recall.shape[0],memory["chunk_size"],memory["hidden_dim"])
342
+ return torch.cat((context,recalled_chunk*gate),1)
343
+ def rope(x,cos,sin):
344
+ a,b=x[...,0::2],x[...,1::2]; return torch.stack([a*cos-b*sin,a*sin+b*cos],-1).flatten(-2)
345
+
346
+ class RMS(nn.Module):
347
+ def __init__(s,d,eps=1e-6): super().__init__(); s.w=nn.Parameter(torch.ones(d)); s.eps=eps
348
+ def forward(s,x): n=torch.rsqrt(x.float().pow(2).mean(-1,keepdim=True)+s.eps).to(x.dtype); return x*n*s.w.to(x.dtype)
349
+
350
+ class RVQ(nn.Module):
351
+ def __init__(s,i,o,g,st=1,codes=16):
352
+ super().__init__(); s.i,s.o,s.g,s.st,s.c=i,o,g,st,codes
353
+ s.weight=nn.Parameter(torch.randn(o,i)/math.sqrt(i))
354
+ s.register_buffer("cb",torch.zeros(st,codes,g)); s.register_buffer("cb_init",torch.tensor(False)); s._q=None
355
+ @torch.no_grad()
356
+ def _km(s,X,k,it=12,samp=8192):
357
+ S=X[torch.randperm(X.shape[0],device=X.device)[:min(X.shape[0],samp)]]
358
+ C=S[torch.randperm(S.shape[0],device=X.device)[:min(k,S.shape[0])]].clone()
359
+ if C.shape[0]<k: C=torch.cat([C,C[:k-C.shape[0]]])
360
+ for _ in range(it):
361
+ a=torch.cdist(S,C).argmin(1)
362
+ for j in range(k):
363
+ m=a==j
364
+ if m.any(): C[j]=S[m].mean(0)
365
+ return C
366
+ @torch.no_grad()
367
+ def _fit(s):
368
+ sc=s.weight.abs().mean(1,keepdim=True).clamp_min(1e-8); r=(s.weight/sc).reshape(-1,s.g).clone()
369
+ for t in range(s.st):
370
+ rms=r.pow(2).mean().sqrt().clamp_min(1e-8); s.cb[t]=s._km(r/rms,s.c)
371
+ idx=torch.cdist(r/rms,s.cb[t]).argmin(1); r=r-s.cb[t][idx]*rms
372
+ s.cb_init.fill_(True)
373
+ def enc(s):
374
+ if not bool(s.cb_init): s._fit()
375
+ with torch.no_grad():
376
+ sc=s.weight.detach().abs().mean(1,keepdim=True).clamp_min(1e-8); r=(s.weight.detach()/sc).reshape(-1,s.g); acc=torch.zeros_like(r)
377
+ for t in range(s.st):
378
+ rms=r.pow(2).mean().sqrt().clamp_min(1e-8); idx=torch.cdist(r/rms,s.cb[t]).argmin(1)
379
+ q=s.cb[t][idx]*rms; acc=acc+q; r=r-q
380
+ quantized=acc.reshape(s.o,s.i)*sc
381
+
382
+
383
+
384
+ if (s._q is not None and s._q.shape==quantized.shape and
385
+ s._q.device==quantized.device and
386
+ s._q.dtype==quantized.dtype):
387
+ s._q.copy_(quantized)
388
+ else:
389
+ s._q=quantized
390
+ def qw(s):
391
+
392
+ if s._q is None or s._q.device!=s.weight.device: s.enc()
393
+ return ste(s.weight,s._q)
394
+ def forward(s,x): return F.linear(x,s.qw().to(x.dtype))
395
+ def bits(s): return s.st*math.log2(s.c)/s.g
396
+ def requant(m):
397
+ for md in m.modules():
398
+ if isinstance(md,RVQ): md.enc()
399
+
400
+ def cs(Ts,dev):
401
+ inv=1.0/(10000**(torch.arange(0,HD,2,device=dev).float()/HD)); f=torch.arange(Ts,device=dev).float()[:,None]*inv[None]
402
+ return f.cos()[None,None].to(torch.bfloat16), f.sin()[None,None].to(torch.bfloat16)
403
+
404
+ def cs_at(start,Ts,dev):
405
+ inv=1.0/(10000**(torch.arange(0,HD,2,device=dev).float()/HD))
406
+ f=torch.arange(start,start+Ts,device=dev).float()[:,None]*inv[None]
407
+ return f.cos()[None,None].to(torch.bfloat16),f.sin()[None,None].to(torch.bfloat16)
408
+
409
+ def shiftmax(dot,alpha,Ts,device):
410
+ aq=ste(alpha,(alpha*4096).round()/4096)
411
+ e=aq*dot
412
+ e=ste(e,e.floor())
413
+ cm=torch.ones(Ts,Ts,dtype=torch.bool,device=device).tril()
414
+ e=e.masked_fill(~cm,-1e9)
415
+ w=torch.exp2((e-e.amax(-1,keepdim=True)).clamp_min(-15))
416
+ return w/w.sum(-1,keepdim=True)
417
+
418
+ class Block(nn.Module):
419
+ def __init__(s,layer_idx=0):
420
+ super().__init__(); s.n1=RMS(D); s.n2=RMS(D)
421
+ s.layer_idx=int(layer_idx)
422
+ s.q=RVQ(D,NH*HD,8,2); s.k=RVQ(D,NKV*HD,8,2); s.v=RVQ(D,NKV*HD,8,2); s.o=RVQ(NH*HD,D,8,2)
423
+ s.qn=RMS(HD); s.kn=RMS(HD); s.g=nn.Parameter(torch.zeros(NH*HD)); s.alpha=nn.Parameter(torch.full((1,NH,1,1),0.25))
424
+ s.up=RVQ(D,FFNH,32,1); s.gt=RVQ(D,FFNH,32,1); s.dn=RVQ(FFNH,D,32,1)
425
+ s.kv_bits=KV_BITS
426
+ s.kcodec=KVCodec1(NKV,HD,seed=1000+int(layer_idx))
427
+ s.vcodec=KVCodec1(NKV,HD,seed=2000+int(layer_idx))
428
+ def forward(s,x,cos,sin):
429
+ z=s.n1(x); Bs,Ts,_=z.shape
430
+ q=s.qn(s.q(z).view(Bs,Ts,NH,HD)).transpose(1,2); k=s.kn(s.k(z).view(Bs,Ts,NKV,HD)).transpose(1,2); v=s.v(z).view(Bs,Ts,NKV,HD).transpose(1,2)
431
+ q=rope(q,cos,sin); k=rope(k,cos,sin)
432
+ if s.kv_bits==1 and (TRAIN_EXACT_KV or not s.training):
433
+ if KV_TWO_TIER:
434
+
435
+
436
+
437
+ m=KV_COLD_MASK
438
+ if m is not None:
439
+ kc=s.kcodec(k); vc=s.vcodec(v); mm=m[:,None,:,None]
440
+ k=torch.where(mm,kc,k); v=torch.where(mm,vc,v)
441
+ else:
442
+ k=s.kcodec(k); v=s.vcodec(v)
443
+ elif s.kv_bits!=1:
444
+ k=kv2(k); v=kv2(v)
445
+ k=k.repeat_interleave(NH//NKV,1); v=v.repeat_interleave(NH//NKV,1)
446
+ if TRAIN_EXACT_POT or not s.training:
447
+ q=pot(q).to(torch.bfloat16)
448
+ k=pot(k).to(torch.bfloat16)
449
+ v=pot(v).to(torch.bfloat16)
450
+ else:
451
+
452
+
453
+ q=q.to(torch.bfloat16)
454
+ k=k.to(torch.bfloat16)
455
+ v=v.to(torch.bfloat16)
456
+ if FAST_ATTN:
457
+
458
+
459
+
460
+
461
+ aq=ste(s.alpha,(s.alpha*4096).round()/4096)*0.6931471805599453
462
+ y=F.scaled_dot_product_attention((q*aq).to(v.dtype),k,v,is_causal=True,scale=1.0)
463
+ y=y.transpose(1,2).reshape(Bs,Ts,NH*HD)
464
+ else:
465
+ w=shiftmax(q@k.transpose(-1,-2),s.alpha,Ts,x.device)
466
+ y=(w.to(v.dtype)@v).transpose(1,2).reshape(Bs,Ts,NH*HD)
467
+
468
+ x=x+s.o(y*torch.sigmoid(s.g)); h=s.n2(x); return x+s.dn(F.silu(s.gt(h))*s.up(h))
469
+
470
+ def _qkv(s,z,cos,sin):
471
+ Bs,Ts,_=z.shape
472
+ q=s.qn(s.q(z).view(Bs,Ts,NH,HD)).transpose(1,2)
473
+ k=s.kn(s.k(z).view(Bs,Ts,NKV,HD)).transpose(1,2)
474
+ v=s.v(z).view(Bs,Ts,NKV,HD).transpose(1,2)
475
+ return rope(q,cos,sin),rope(k,cos,sin),v
476
+
477
+ def _finish_attention(s,x,q,k,v,causal,exact=True):
478
+ kr=k.repeat_interleave(NH//NKV,1); vr=v.repeat_interleave(NH//NKV,1)
479
+ q=pot(q).to(torch.bfloat16); kr=pot(kr).to(torch.bfloat16); vr=pot(vr).to(torch.bfloat16)
480
+ dot=q@kr.transpose(-1,-2)
481
+ aq=ste(s.alpha,(s.alpha*4096).round()/4096)
482
+ raw=aq*dot
483
+ e=ste(raw,raw.floor()) if exact else raw*0.6931471805599453
484
+ if causal:
485
+ tq,tk=e.shape[-2:]
486
+
487
+ cm=torch.arange(tk,device=x.device)[None,:] <= (
488
+ torch.arange(tq,device=x.device)[:,None]+tk-tq)
489
+ e=e.masked_fill(~cm[None,None],-1e9)
490
+ if exact:
491
+ w=torch.exp2((e-e.amax(-1,keepdim=True)).clamp_min(-15))
492
+ w=w/w.sum(-1,keepdim=True)
493
+ else:
494
+ w=torch.softmax(e,-1)
495
+ y=(w.to(vr.dtype)@vr).transpose(1,2).reshape(x.shape[0],x.shape[1],NH*HD)
496
+ x=x+s.o(y*torch.sigmoid(s.g)); h=s.n2(x)
497
+ return x+s.dn(F.silu(s.gt(h))*s.up(h))
498
+
499
+ @staticmethod
500
+ def _popcount_bytes(x):
501
+ lut=torch.tensor([int(i).bit_count() for i in range(256)],
502
+ dtype=torch.int16,device=x.device)
503
+ return lut[x.long()].sum(-1,dtype=torch.int32)
504
+
505
+ def _archive_overflow(s,cache,overflow):
506
+ if overflow<=0 or not cache.get("cold_enabled") or s.kv_bits!=1:
507
+ return
508
+ ck=cache["k"][:,:,:overflow].detach()
509
+ cv=cache["v"][:,:,:overflow].detach()
510
+ start=int(cache["hot_start_position"])
511
+ positions=torch.arange(start,start+overflow,dtype=torch.int64,device=ck.device)
512
+ cache["cold_archive"].append(ck,cv,positions)
513
+ cache["hot_start_position"]=start+overflow
514
+
515
+ def _retrieve_cold(s,q,cache,dtype):
516
+ archive=cache.get("cold_archive")
517
+ if (not cache.get("cold_enabled") or archive is None or
518
+ len(archive)==0 or s.kv_bits!=1):
519
+ return None,None
520
+ if q.shape[2]!=1:
521
+ raise ValueError("cold KV retrieval requires single-token decode")
522
+ B,_,_,width=q.shape
523
+ groups=NH//NKV
524
+ q_group=q.reshape(B,NKV,groups,1,width).mean(2)
525
+ q_code=s.kcodec.pack(q_group)[:,:,0]
526
+ started=time.perf_counter()
527
+ n=len(archive)
528
+ final_k=min(int(cache["cold_topk"]),n)
529
+ wide_target=min(max(final_k*4,final_k),n)
530
+ wide_indices,wide_distances=archive.exact_hamming_topk(q_code,wide_target)
531
+ kp=archive.gather(wide_indices,"k")
532
+ vp=archive.gather(wide_indices,"v")
533
+ byte_width=archive.packed_width
534
+ kval=s.kcodec.unpack(kp,dtype)
535
+ q_score=pot(q.reshape(B,NKV,groups,width)).float()
536
+ k_score=pot(kval).float()
537
+ score=torch.einsum("bhgd,bhwd->bhgw",q_score,k_score).amax(2)
538
+ selected=torch.topk(score,final_k,-1).indices
539
+ sg=selected[...,None].expand(-1,-1,-1,byte_width)
540
+ kp=torch.gather(kp,2,sg); vp=torch.gather(vp,2,sg)
541
+ cache["cold_last_indices"]=torch.gather(wide_indices,2,selected)
542
+ if cache.get("retrieval_trace_enabled"):
543
+ selected_dist=torch.gather(wide_distances,2,selected)
544
+ cache["cold_last_trace"]={
545
+ "original_positions":archive.gather_positions(
546
+ cache["cold_last_indices"]).detach().cpu(),
547
+ "hamming_distances":selected_dist.detach().cpu(),
548
+ "shortlist_rank":selected.detach().cpu(),
549
+ "rerank_scores":torch.gather(score,2,selected).detach().cpu(),
550
+ "layer":s.layer_idx,
551
+ "heads":tuple(range(NKV)),
552
+ "cold_token_count":n,
553
+ "hot_token_count":int(cache["k"].shape[2]),
554
+ "latency_ms":(time.perf_counter()-started)*1000.0,
555
+ }
556
+ return s.kcodec.unpack(kp,dtype),s.vcodec.unpack(vp,dtype)
557
+
558
+ @torch.no_grad()
559
+ def prefill_cached(s,x,cos,sin,max_ctx=2048,exact=True,
560
+ archive_cold=False,cold_chunk=128,cold_topk=32,
561
+ retrieval_trace=False):
562
+ z=s.n1(x); q,k0,v0=s._qkv(z,cos,sin)
563
+ if s.kv_bits==1:
564
+ kp=s.kcodec.pack(k0); vp=s.vcodec.pack(v0)
565
+ k=s.kcodec.unpack(kp,z.dtype); v=s.vcodec.unpack(vp,z.dtype)
566
+ else:
567
+ kp,ks=kv2_pack(k0); vp,vs=kv2_pack(v0)
568
+ k=kv2_unpack(kp,ks,z.dtype); v=kv2_unpack(vp,vs,z.dtype)
569
+ out=s._finish_attention(x,q,k,v,causal=True,exact=exact)
570
+ cache={"k":kp[:,:,-max_ctx:],"v":vp[:,:,-max_ctx:],
571
+ "format":("random_walsh_ctv_1bit_v1" if s.kv_bits==1
572
+ else "walsh_hadamard_2bit_v1")}
573
+ if archive_cold and s.kv_bits==1:
574
+ archive=PagedKVArchive(
575
+ kp.shape[0],kp.shape[1],kp.shape[-1],
576
+ page_size=int(cold_chunk),device=kp.device)
577
+ cache.update({
578
+ "cold_enabled":True,"cold_archive":archive,
579
+ "cold_k":PagedKVView(archive,"k"),
580
+ "cold_v":PagedKVView(archive,"v"),
581
+ "chunk_keys":ExactChunkCountView(archive,int(cold_chunk)),
582
+ "cold_chunk":int(cold_chunk),"cold_topk":int(cold_topk),
583
+ "cold_last_indices":None,"cold_last_trace":None,
584
+ "retrieval_trace_enabled":bool(retrieval_trace),
585
+ "hot_start_position":max(0,int(x.shape[1])-int(max_ctx)),
586
+ })
587
+ else:
588
+ cache["cold_enabled"]=False
589
+ if s.kv_bits==2:
590
+ cache.update({"ks":ks[:,:,-max_ctx:],"vs":vs[:,:,-max_ctx:]})
591
+ return out,cache
592
+
593
+ @torch.no_grad()
594
+ def decode_cached(s,x,position,cache,max_ctx=2048,exact=True,
595
+ retrieve_cold=True):
596
+ expected=("random_walsh_ctv_1bit_v1" if s.kv_bits==1
597
+ else "walsh_hadamard_2bit_v1")
598
+ if cache.get("format")!=expected:
599
+ raise ValueError(f"legacy or unknown KV cache; expected {expected}")
600
+ cos,sin=cs_at(position,x.shape[1],x.device)
601
+ z=s.n1(x); q,k0,v0=s._qkv(z,cos,sin)
602
+ if s.kv_bits==1:
603
+ kp=s.kcodec.pack(k0); vp=s.vcodec.pack(v0)
604
+ else:
605
+ kp,ks=kv2_pack(k0); vp,vs=kv2_pack(v0)
606
+ joined_k=torch.cat((cache["k"],kp),2)
607
+ joined_v=torch.cat((cache["v"],vp),2)
608
+ overflow=max(0,joined_k.shape[2]-max_ctx)
609
+ cache["k"],cache["v"]=joined_k,joined_v
610
+ s._archive_overflow(cache,overflow)
611
+ cache["k"]=cache["k"][:,:,overflow:]
612
+ cache["v"]=cache["v"][:,:,overflow:]
613
+ if s.kv_bits==1:
614
+ k=s.kcodec.unpack(cache["k"],z.dtype)
615
+ v=s.vcodec.unpack(cache["v"],z.dtype)
616
+ cold_k,cold_v=(s._retrieve_cold(q,cache,z.dtype)
617
+ if retrieve_cold else (None,None))
618
+ if cold_k is not None:
619
+ k=torch.cat((cold_k,k),2); v=torch.cat((cold_v,v),2)
620
+ else:
621
+ cache["ks"]=torch.cat((cache["ks"],ks),2)[:,:,-max_ctx:]
622
+ cache["vs"]=torch.cat((cache["vs"],vs),2)[:,:,-max_ctx:]
623
+ k=kv2_unpack(cache["k"],cache["ks"],z.dtype)
624
+ v=kv2_unpack(cache["v"],cache["vs"],z.dtype)
625
+ return s._finish_attention(x,q,k,v,causal=x.shape[1]>1,exact=exact),cache
626
+
627
+ class StructStep(nn.Module):
628
+ def __init__(s):
629
+ super().__init__(); s.Wq=RVQ(D,D,8,2); s.cin=RVQ(2*D,FFNH,8,2); s.cout=RVQ(FFNH,D,8,2)
630
+ s.nf=RMS(D); s.verify=nn.Linear(D,1)
631
+ def forward(s,h):
632
+ Bs,Ts,_=h.shape; q=s.Wq(h)
633
+ att=(q@h.transpose(-1,-2))/math.sqrt(D)
634
+ cm=torch.ones(Ts,Ts,dtype=torch.bool,device=h.device).tril()
635
+ att=torch.softmax(att.masked_fill(~cm,-1e9),-1)
636
+ r=att@h
637
+ h=s.nf(h+s.cout(F.silu(s.cin(torch.cat([h,r],-1)))))
638
+ return h, torch.sigmoid(s.verify(h)).squeeze(-1)
639
+
640
+ @torch.no_grad()
641
+ def decode_cached(s,h_new,h_context):
642
+ q=s.Wq(h_new)
643
+ att=(q@h_context.transpose(-1,-2))/math.sqrt(D)
644
+ if h_new.shape[1]>1:
645
+ tq,tk=att.shape[-2:]
646
+ cm=torch.arange(tk,device=h_new.device)[None,:] <= (
647
+ torch.arange(tq,device=h_new.device)[:,None]+tk-tq)
648
+ att=att.masked_fill(~cm[None],-1e9)
649
+ w=torch.softmax(att,-1); r=w@h_context
650
+ h=s.nf(h_new+s.cout(F.silu(s.cin(torch.cat([h_new,r],-1)))))
651
+ return h,torch.sigmoid(s.verify(h)).squeeze(-1)
finetune/modeling/export_rvq.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys, numpy as np, torch
2
+
3
+ sys.path.insert(0, r"C:\Users\USER\Desktop\shadow_final\model_250m")
4
+ import common
5
+ from common import RVQ
6
+
7
+
8
+ def rvq_capture(m):
9
+ W = m.weight.detach().float()
10
+ sc = W.abs().mean(1, keepdim=True).clamp_min(1e-8)
11
+ r = (W / sc).reshape(-1, m.g).clone()
12
+ idxs, rmss = [], []
13
+ for t in range(m.st):
14
+ rms = r.pow(2).mean().sqrt().clamp_min(1e-8)
15
+ idx = torch.cdist(r / rms, m.cb[t]).argmin(1)
16
+ r = r - m.cb[t][idx] * rms
17
+ idxs.append(idx.cpu().numpy().astype(np.uint8)); rmss.append(float(rms))
18
+ return sc.squeeze(1).cpu().numpy(), idxs, rmss
19
+
20
+
21
+ def rvq_pack(m):
22
+ o, i, g, st = m.o, m.i, m.g, m.st
23
+ G = i // g
24
+ Npad = (o + 63) & ~63
25
+ nch = Npad // 64
26
+ sc, idxs, rmss = rvq_capture(m)
27
+
28
+ cbT = np.zeros((st, g, 16), dtype=np.float32)
29
+ for t in range(st):
30
+ cb = m.cb[t].detach().cpu().numpy()
31
+ cbT[t] = cb.T * rmss[t]
32
+
33
+ idx = np.zeros((st, nch, G, 32), dtype=np.uint8)
34
+ for t in range(st):
35
+ flat = idxs[t].reshape(o, G)
36
+ pad = np.zeros((Npad, G), dtype=np.uint8)
37
+ pad[:o] = flat
38
+ for c in range(nch):
39
+ lo = pad[c * 64: c * 64 + 32]
40
+ hi = pad[c * 64 + 32: c * 64 + 64]
41
+ idx[t, c] = (lo.T | (hi.T << 4))
42
+
43
+ scale = np.zeros(Npad, dtype=np.float32)
44
+ scale[:o] = sc
45
+ return cbT, idx, scale
46
+
47
+
48
+ def rvq_unpack(cbT, idx, scale, o, i, g, st):
49
+ G = i // g
50
+ Npad = scale.shape[0]; nch = Npad // 64
51
+ W = np.zeros((Npad, i), dtype=np.float32)
52
+ for t in range(st):
53
+ for c in range(nch):
54
+ blk = idx[t, c]
55
+ lo = (blk & 0x0F).T
56
+ hi = (blk >> 4).T
57
+ for sub, rows in ((lo, range(c * 64, c * 64 + 32)),
58
+ (hi, range(c * 64 + 32, c * 64 + 64))):
59
+ for bi, n in enumerate(rows):
60
+ codes = sub[bi]
61
+ W[n] += cbT[t][:, codes].T.reshape(-1)
62
+ return W[:o] * scale[:o, None]
63
+
64
+
65
+ if __name__ == "__main__":
66
+ torch.manual_seed(0)
67
+ print(f"{'module':>22s} {'shape':>14s} {'bits/w':>7s} {'max|err|':>10s} {'rel RMSE':>10s} verdict")
68
+ ok = True
69
+
70
+ for name, (i, o, g, st) in {
71
+ "proj 1-bit (g8,st2)": (256, 256, 8, 2),
72
+ "proj 1-bit rect ": (256, 128, 8, 2),
73
+ "FFN 0.125 (g32,st1)": (256, 1024, 32, 1),
74
+ "FFN 0.125 down ": (1024, 256, 32, 1),
75
+ "odd rows (pad test) ": (256, 100, 8, 2),
76
+ }.items():
77
+ m = RVQ(i, o, g, st)
78
+ m.weight.data = torch.randn(o, i) / (i ** 0.5)
79
+ m.enc()
80
+ ref = m._q.detach().cpu().numpy()
81
+
82
+ cbT, idxp, scale = rvq_pack(m)
83
+ got = rvq_unpack(cbT, idxp, scale, o, i, g, st)
84
+
85
+ err = np.abs(ref - got).max()
86
+ rel = np.sqrt(((ref - got) ** 2).mean()) / np.sqrt((ref ** 2).mean())
87
+ good = err < 1e-5
88
+ ok &= good
89
+ print(f"{name:>22s} {f'{o}x{i}':>14s} {m.bits():7.3f} {err:10.2e} {rel:10.2e} "
90
+ f"{'PASS' if good else 'FAIL'}")
91
+
92
+ print()
93
+ print("ROUNDTRIP_OK" if ok else "ROUNDTRIP_FAIL")
finetune/modeling/export_ternary.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, sys, struct
2
+ os.environ.setdefault("SHADOW_D", "1536"); os.environ.setdefault("SHADOW_NL", "10")
3
+ os.environ.setdefault("SHADOW_NH", "24"); os.environ.setdefault("SHADOW_NKV", "2")
4
+ os.environ.setdefault("SHADOW_HD", "64"); os.environ.setdefault("SHADOW_FFNH", "4224")
5
+ os.environ.setdefault("SHADOW_KV_TWO_TIER", "1")
6
+ import numpy as np, torch
7
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "model"))
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+ import common
10
+ from common import RVQ, requant
11
+ from model_250m import Shadow250M
12
+ from export_rvq import rvq_pack
13
+
14
+ CK, OUT = sys.argv[1], sys.argv[2]; COMPACT = "--compact" in sys.argv
15
+ ck = torch.load(CK, map_location="cpu", weights_only=False)
16
+ V, FPD = ck.get("cfg", {}).get("V", 131072), 512
17
+ model = Shadow250M(torch.zeros(V, FPD), torch.zeros(V, FPD), V)
18
+ model.load_state_dict(ck["model"], strict=False); model.eval(); requant(model)
19
+
20
+ def tern_pack(w):
21
+ wt = w.detach().float()
22
+ sc = 1.0 / wt.abs().mean(dim=1, keepdim=True).clamp_(min=1e-5)
23
+ t = (wt * sc).round().clamp(-1, 1).to(torch.int8).numpy()
24
+ rs = (1.0 / sc[:, 0]).numpy().astype(np.float32)
25
+ w = wt.numpy()
26
+ codes = (t + 1).astype(np.uint8)
27
+ o, i = w.shape; assert i % 4 == 0
28
+ c4 = codes.reshape(o, i // 4, 4)
29
+ packed = (c4[:, :, 0] | (c4[:, :, 1] << 2) | (c4[:, :, 2] << 4) | (c4[:, :, 3] << 6)).astype(np.uint8)
30
+ if COMPACT:
31
+ pad = (-i) % 5; c5 = np.concatenate([codes, np.ones((o, pad), np.uint8)], 1).reshape(o, -1, 5).astype(np.uint16)
32
+ packed = (c5[:, :, 0] + 3 * c5[:, :, 1] + 9 * c5[:, :, 2] + 27 * c5[:, :, 3] + 81 * c5[:, :, 4]).astype(np.uint8)
33
+ return packed, rs, t.astype(np.float32) * rs[:, None]
34
+
35
+ class Wrap(torch.nn.Module):
36
+ def __init__(s, m):
37
+ super().__init__(); s.emb = m.inp; s.b = m.b; s.step = m.struct; s.nf = m.nf; s.head = m.head; s.tb = m.tied_bias
38
+ wrap = Wrap(model)
39
+ recs = []; ntern = nrvq = 0; tern_bytes = rvq_bytes = dense_bytes = 0
40
+ rvq_ids = set()
41
+ for name, mod in wrap.named_modules():
42
+ if isinstance(mod, RVQ):
43
+ rvq_ids.add(id(mod))
44
+ if mod.g == 32:
45
+ packed, rs, deq = tern_pack(mod.weight)
46
+ recs.append((name, 4 if COMPACT else 3, (mod.o, mod.i, packed, rs))); ntern += 1; tern_bytes += packed.nbytes + rs.nbytes
47
+ else:
48
+ cbT, idx, scale = rvq_pack(mod)
49
+ recs.append((name, 1, (mod.o, mod.i, mod.g, mod.st, cbT, idx, scale))); nrvq += 1
50
+ rvq_bytes += cbT.nbytes + idx.nbytes + scale.nbytes
51
+ for name, p in wrap.named_parameters():
52
+ owner = name.rsplit(".", 1)[0]
53
+ mod = dict(wrap.named_modules()).get(owner)
54
+ if mod is not None and id(mod) in rvq_ids: continue
55
+ a = p.detach().float().numpy()
56
+ if COMPACT and a.ndim >= 1 and a.size >= 4096: recs.append((name, 5, a.astype(np.float16))); dense_bytes += a.size * 2
57
+ else: recs.append((name, 0, a)); dense_bytes += a.nbytes
58
+ for name, b in wrap.named_buffers():
59
+ if any(x in name for x in ("cent", "cb", "initialized", "sign", "mu", "ctv", "low", "high", "updates", "inv")): continue
60
+ a = b.detach().float().numpy(); recs.append((name, 0, a)); dense_bytes += a.nbytes
61
+
62
+ with open(OUT, "wb") as f:
63
+ f.write(b"SHDW"); f.write(struct.pack("<II", 1, len(recs)))
64
+ for name, kind, pay in recs:
65
+ nb = name.encode(); f.write(struct.pack("<I", len(nb))); f.write(nb); f.write(struct.pack("<I", kind))
66
+ if kind == 0:
67
+ a = np.ascontiguousarray(pay, np.float32); f.write(struct.pack("<I", a.ndim)); f.write(struct.pack("<" + "I" * a.ndim, *a.shape)); f.write(a.tobytes())
68
+ elif kind == 1:
69
+ o, i, g, st, cbT, idx, scale = pay
70
+ f.write(struct.pack("<IIII", o, i, g, st)); f.write(np.ascontiguousarray(cbT, np.float32).tobytes())
71
+ f.write(np.ascontiguousarray(idx, np.uint8).tobytes()); f.write(np.ascontiguousarray(scale, np.float32).tobytes())
72
+ elif kind == 5:
73
+ a = np.ascontiguousarray(pay, np.float16); f.write(struct.pack("<I", a.ndim)); f.write(struct.pack("<" + "I" * a.ndim, *a.shape)); f.write(a.tobytes())
74
+ else:
75
+ o, i, packed, rs = pay
76
+ f.write(struct.pack("<II", o, i)); f.write(np.ascontiguousarray(packed, np.uint8).tobytes()); f.write(rs.tobytes())
77
+ size = os.path.getsize(OUT)
78
+ print(f"loaded checkpoint ({ck.get('step','finetuned')})")
79
+ print(f"ternary FFN modules {ntern}: {tern_bytes/1e6:.2f} MB | RVQ 1-bit modules {nrvq}: {rvq_bytes/1e6:.2f} MB | dense fp32: {dense_bytes/1e6:.2f} MB")
80
+ print(f".shdw = {size/1e6:.2f} MB (+ 8.39 MB fp131072 table = {(size+8388736)/1e6:.2f} MB deploy)")
81
+
82
+ mod = model.b[0].up; w = mod.weight.detach().float()
83
+ sc = 1.0 / w.abs().mean(dim=1, keepdim=True).clamp_(min=1e-5); wq = (w * sc).round().clamp(-1, 1) / sc
84
+ _, _, deq = tern_pack(mod.weight); print("ternary round-trip max|err| vs trainer forward:", float((torch.tensor(deq) - wq).abs().max()))
finetune/modeling/model_250m.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, math, numpy as np, torch, torch.nn as nn, torch.nn.functional as F
2
+ import common
3
+ from common import D,NL,NH,NKV,HD,FFNH,FPD, RMS,RVQ,Block,StructStep, requant, cs
4
+
5
+
6
+ def load_vocab(fp_path,dev):
7
+ fp=np.unpackbits(np.load(fp_path),axis=1)[:,:FPD]
8
+ cent=torch.tensor(fp.astype(np.float32)*2-1,device=dev)
9
+ return cent, F.normalize(cent,dim=-1), cent.shape[0]
10
+
11
+ class Shadow250M(nn.Module):
12
+ def __init__(s,cent,cent_n,V,use_memory=False):
13
+ super().__init__()
14
+
15
+
16
+ s.register_buffer("cent",cent,persistent=False)
17
+ s.register_buffer("cent_n",cent_n,persistent=False); s.V=V
18
+ s.inp=nn.Linear(FPD,D,bias=False)
19
+ s.b=nn.ModuleList([Block(i) for i in range(NL)])
20
+ s.struct=StructStep()
21
+ s.nf=RMS(D); s.head=nn.Linear(D,FPD,bias=False)
22
+ s.tied_bias=nn.Parameter(torch.zeros(V))
23
+
24
+
25
+ s.default_use_memory=bool(use_memory)
26
+ inv=1.0/(10000**(torch.arange(0,HD,2).float()/HD)); s.register_buffer("inv",inv,persistent=False)
27
+ def trunk(s,idx):
28
+ cos,sin=cs(idx.shape[1],idx.device); x=s.inp(s.cent[idx])
29
+ for blk in s.b: x=blk(x,cos,sin)
30
+ x,conf=s.struct(x)
31
+ return s.nf(x), conf
32
+ def logits(s,x): return (s.head(x).float()@s.cent_n.T)+s.tied_bias
33
+ def forward(s,idx,ys=None):
34
+ x,conf=s.trunk(idx)
35
+ if ys is None: return s.logits(x)
36
+
37
+ ph=s.head(x).float().reshape(-1,FPD); yf=ys.reshape(-1); Nn=ph.shape[0]; CH=2048
38
+ return sum(F.cross_entropy(ph[i:i+CH]@s.cent_n.T+s.tied_bias,yf[i:i+CH],reduction="sum") for i in range(0,Nn,CH))/Nn
39
+
40
+ @torch.no_grad()
41
+ def prefill_cached(s,idx,max_ctx=2048,exact_shiftmax=True,
42
+ use_memory=None,memory_chunk=128,memory_capacity=256,
43
+ retrieval_trace=False,stream_block=256):
44
+ if use_memory is None: use_memory=s.default_use_memory
45
+ if idx.shape[1]>max_ctx:
46
+ if not use_memory:
47
+ idx=idx[:,-max_ctx:]
48
+ else:
49
+ logits,state=s.prefill_cached(
50
+ idx[:,:max_ctx],max_ctx=max_ctx,
51
+ exact_shiftmax=exact_shiftmax,use_memory=use_memory,
52
+ memory_chunk=memory_chunk,memory_capacity=memory_capacity,
53
+ retrieval_trace=retrieval_trace,stream_block=stream_block)
54
+ cursor=max_ctx
55
+ final=idx.shape[1]-1
56
+ while cursor<final:
57
+ stop=min(final,cursor+int(stream_block))
58
+ state=s.ingest_cached(idx[:,cursor:stop],state)
59
+ cursor=stop
60
+ return s.decode_cached(idx[:,final:final+1],state)
61
+ cos,sin=cs(idx.shape[1],idx.device); x=s.inp(s.cent[idx]); layers=[]
62
+ for blk in s.b:
63
+ x,cache=blk.prefill_cached(x,cos,sin,max_ctx=max_ctx,
64
+ exact=exact_shiftmax,
65
+ archive_cold=bool(use_memory),
66
+ cold_chunk=memory_chunk,
67
+ retrieval_trace=retrieval_trace); layers.append(cache)
68
+ trunk=x[:,-max_ctx:]; y,_=s.struct(x); ph=s.head(s.nf(y[:,-1])).float()
69
+ return ph@s.cent_n.T+s.tied_bias,{"layers":layers,"trunk":trunk,
70
+ "position":idx.shape[1],"max_ctx":max_ctx,
71
+ "exact_shiftmax":exact_shiftmax,
72
+ "retrieval_trace":bool(retrieval_trace),
73
+ "stream_block":int(stream_block),
74
+ "memory":common.make_memory_state(
75
+ idx.shape[0],D,idx.device,chunk_size=memory_chunk,
76
+ capacity=memory_capacity) if use_memory else None}
77
+
78
+ @torch.no_grad()
79
+ def ingest_cached(s,idx_block,state):
80
+ if idx_block.shape[1]==0:
81
+ return state
82
+ x=s.inp(s.cent[idx_block]); pos=int(state["position"])
83
+ for i,blk in enumerate(s.b):
84
+ x,state["layers"][i]=blk.decode_cached(
85
+ x,pos,state["layers"][i],max_ctx=state["max_ctx"],
86
+ exact=state["exact_shiftmax"],retrieve_cold=False)
87
+ combined=torch.cat((state["trunk"],x),1)
88
+ overflow=max(0,combined.shape[1]-state["max_ctx"])
89
+ common.memory_absorb_evicted(
90
+ state.get("memory"),combined[:,:overflow],s.head,s.nf)
91
+ state["trunk"]=combined[:,overflow:]
92
+ s.struct.decode_cached(x,state["trunk"])
93
+ state["position"]=pos+idx_block.shape[1]
94
+ return state
95
+
96
+ @torch.no_grad()
97
+ def decode_cached(s,idx_new,state):
98
+ if state.get("memory") is not None and idx_new.shape[1]!=1:
99
+ logits=None
100
+ for j in range(idx_new.shape[1]):
101
+ logits,state=s.decode_cached(idx_new[:,j:j+1],state)
102
+ return logits,state
103
+ x=s.inp(s.cent[idx_new]); pos=int(state["position"])
104
+ for i,blk in enumerate(s.b):
105
+ x,state["layers"][i]=blk.decode_cached(x,pos,state["layers"][i],
106
+ max_ctx=state["max_ctx"],
107
+ exact=state["exact_shiftmax"])
108
+ combined=torch.cat((state["trunk"],x),1)
109
+ overflow=max(0,combined.shape[1]-state["max_ctx"])
110
+ common.memory_absorb_evicted(
111
+ state.get("memory"),combined[:,:overflow],s.head,s.nf)
112
+ state["trunk"]=combined[:,overflow:]
113
+ context=common.memory_append_recall(
114
+ state.get("memory"),x[:,-1],state["trunk"],s.head,s.nf)
115
+ y,_=s.struct.decode_cached(x,context); state["position"]=pos+idx_new.shape[1]
116
+ ph=s.head(s.nf(y[:,-1])).float(); return ph@s.cent_n.T+s.tied_bias,state
117
+
118
+ if __name__=="__main__":
119
+ dev="cuda" if torch.cuda.is_available() else "cpu"
120
+ FP=os.path.join(os.path.dirname(__file__),"dolma3_fp_512b.npy")
121
+ if not os.path.exists(FP): FP=os.path.join(os.path.dirname(__file__),"..","..","shadow-o","phase0_tokenizer","dolma3_fp_512b.npy")
122
+ cent,cent_n,V=load_vocab(FP,dev); m=Shadow250M(cent,cent_n,V).to(dev)
123
+ body=sum(p.numel() for n,p in m.named_parameters() if "cent" not in n and "tied_bias" not in n)
124
+ print(f"SHADOW-O 250M | body {body/1e6:.1f}M params | vocab {V} (0 params) | config d{D} L{NL} GQA{NH}/{NKV}")
125
+ b=m.b[0]; print(f" proj {b.q.bits():.3f} bit/w | FFN {b.up.bits():.3f} bit/w | struct step: 1 | Q4/loop: none")
126
+ x=torch.randint(0,V,(1,64),device=dev); requant(m)
127
+ with torch.autocast("cuda",dtype=torch.bfloat16) if dev=="cuda" else torch.no_grad():
128
+ lg=m(x); print(f" forward OK: logits {tuple(lg.shape)} finite={bool(torch.isfinite(lg).all())}")
finetune/modeling/paged_kv.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Iterable
6
+
7
+ import torch
8
+
9
+
10
+ _POPCOUNT = tuple(int(i).bit_count() for i in range(256))
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ArchiveAccounting:
15
+ logical_tokens: int
16
+ page_count: int
17
+ payload_bytes: int
18
+ allocated_payload_bytes: int
19
+ position_index_bytes: int
20
+ bytes_copied_on_append: int
21
+
22
+ @property
23
+ def allocated_total_bytes(self) -> int:
24
+ return self.allocated_payload_bytes + self.position_index_bytes
25
+
26
+
27
+ class PagedKVArchive:
28
+
29
+ format = "paged_random_walsh_ctv_1bit_v1"
30
+ state_version = 1
31
+
32
+ def __init__(
33
+ self,
34
+ batch: int,
35
+ heads: int,
36
+ packed_width: int,
37
+ *,
38
+ page_size: int = 256,
39
+ device=None,
40
+ ):
41
+ if min(batch, heads, packed_width, page_size) < 1:
42
+ raise ValueError("batch, heads, packed_width, and page_size must be positive")
43
+ self.batch = int(batch)
44
+ self.heads = int(heads)
45
+ self.packed_width = int(packed_width)
46
+ self.page_size = int(page_size)
47
+ self.device = torch.device(device or "cpu")
48
+ self.k_pages: list[torch.Tensor] = []
49
+ self.v_pages: list[torch.Tensor] = []
50
+ self.position_pages: list[torch.Tensor] = []
51
+ self.length = 0
52
+ self.bytes_copied_on_append = 0
53
+
54
+ @property
55
+ def shape(self) -> tuple[int, int, int, int]:
56
+ return self.batch, self.heads, self.length, self.packed_width
57
+
58
+ def __len__(self) -> int:
59
+ return self.length
60
+
61
+ def _allocate_page(self):
62
+ shape = (self.batch, self.heads, self.page_size, self.packed_width)
63
+ self.k_pages.append(torch.empty(shape, dtype=torch.uint8, device=self.device))
64
+ self.v_pages.append(torch.empty(shape, dtype=torch.uint8, device=self.device))
65
+ self.position_pages.append(
66
+ torch.empty(self.page_size, dtype=torch.int64, device=self.device)
67
+ )
68
+
69
+ def append(
70
+ self,
71
+ packed_k: torch.Tensor,
72
+ packed_v: torch.Tensor,
73
+ positions: torch.Tensor | Iterable[int] | None = None,
74
+ ) -> None:
75
+ expected_prefix = (self.batch, self.heads)
76
+ if packed_k.dtype != torch.uint8 or packed_v.dtype != torch.uint8:
77
+ raise TypeError("paged one-bit K/V payloads must be torch.uint8")
78
+ if packed_k.shape != packed_v.shape:
79
+ raise ValueError("packed K and V shapes differ")
80
+ if (
81
+ packed_k.ndim != 4
82
+ or tuple(packed_k.shape[:2]) != expected_prefix
83
+ or packed_k.shape[-1] != self.packed_width
84
+ ):
85
+ raise ValueError(
86
+ "expected packed K/V shape "
87
+ f"({self.batch},{self.heads},T,{self.packed_width}), "
88
+ f"got {tuple(packed_k.shape)}"
89
+ )
90
+ tokens = int(packed_k.shape[2])
91
+ if tokens == 0:
92
+ return
93
+ if packed_k.device != self.device or packed_v.device != self.device:
94
+ raise ValueError(
95
+ f"archive is on {self.device}, payload is on "
96
+ f"{packed_k.device}/{packed_v.device}"
97
+ )
98
+ if positions is None:
99
+ pos = torch.arange(
100
+ self.length, self.length + tokens, dtype=torch.int64, device=self.device
101
+ )
102
+ else:
103
+ pos = torch.as_tensor(positions, dtype=torch.int64, device=self.device)
104
+ if pos.ndim != 1 or pos.numel() != tokens:
105
+ raise ValueError(f"positions must contain exactly {tokens} entries")
106
+
107
+ source = 0
108
+ while source < tokens:
109
+ page_index = self.length // self.page_size
110
+ page_offset = self.length % self.page_size
111
+ if page_index == len(self.k_pages):
112
+ self._allocate_page()
113
+ take = min(tokens - source, self.page_size - page_offset)
114
+ dst = slice(page_offset, page_offset + take)
115
+ src = slice(source, source + take)
116
+ self.k_pages[page_index][:, :, dst].copy_(packed_k[:, :, src])
117
+ self.v_pages[page_index][:, :, dst].copy_(packed_v[:, :, src])
118
+ self.position_pages[page_index][dst].copy_(pos[src])
119
+ copied = 2 * self.batch * self.heads * take * self.packed_width
120
+ self.bytes_copied_on_append += copied
121
+ self.length += take
122
+ source += take
123
+
124
+ def _validate_indices(self, indices: torch.Tensor) -> torch.Tensor:
125
+ indices = torch.as_tensor(indices, dtype=torch.long, device=self.device)
126
+ if indices.ndim != 3 or tuple(indices.shape[:2]) != (self.batch, self.heads):
127
+ raise ValueError(
128
+ f"indices must have shape ({self.batch},{self.heads},K), "
129
+ f"got {tuple(indices.shape)}"
130
+ )
131
+ if indices.numel() and (
132
+ int(indices.min().item()) < 0 or int(indices.max().item()) >= self.length
133
+ ):
134
+ raise IndexError(f"archive index outside [0,{self.length})")
135
+ return indices
136
+
137
+ def gather(self, indices: torch.Tensor, kind: str) -> torch.Tensor:
138
+ indices = self._validate_indices(indices)
139
+ if kind not in {"k", "v"}:
140
+ raise ValueError("kind must be 'k' or 'v'")
141
+ pages = self.k_pages if kind == "k" else self.v_pages
142
+ out = torch.empty(
143
+ (*indices.shape, self.packed_width),
144
+ dtype=torch.uint8,
145
+ device=self.device,
146
+ )
147
+ page_ids = torch.div(indices, self.page_size, rounding_mode="floor")
148
+ offsets = indices.remainder(self.page_size)
149
+ for page_id in torch.unique(page_ids).tolist():
150
+ mask = page_ids == int(page_id)
151
+ b, h, slot = mask.nonzero(as_tuple=True)
152
+ out[b, h, slot] = pages[int(page_id)][b, h, offsets[b, h, slot]]
153
+ return out
154
+
155
+ def gather_positions(self, indices: torch.Tensor) -> torch.Tensor:
156
+ indices = self._validate_indices(indices)
157
+ out = torch.empty(indices.shape, dtype=torch.int64, device=self.device)
158
+ page_ids = torch.div(indices, self.page_size, rounding_mode="floor")
159
+ offsets = indices.remainder(self.page_size)
160
+ for page_id in torch.unique(page_ids).tolist():
161
+ mask = page_ids == int(page_id)
162
+ b, h, slot = mask.nonzero(as_tuple=True)
163
+ out[b, h, slot] = self.position_pages[int(page_id)][offsets[b, h, slot]]
164
+ return out
165
+
166
+ @staticmethod
167
+ def _popcount_bytes(value: torch.Tensor) -> torch.Tensor:
168
+ lut = torch.tensor(_POPCOUNT, dtype=torch.int16, device=value.device)
169
+ return lut[value.long()].sum(-1, dtype=torch.int32)
170
+
171
+ def exact_hamming_topk(
172
+ self, query: torch.Tensor, k: int
173
+ ) -> tuple[torch.Tensor, torch.Tensor]:
174
+ if self.length == 0:
175
+ raise ValueError("cannot search an empty archive")
176
+ if (
177
+ query.dtype != torch.uint8
178
+ or query.ndim != 3
179
+ or tuple(query.shape) != (self.batch, self.heads, self.packed_width)
180
+ ):
181
+ raise ValueError(
182
+ "query must be uint8 with shape "
183
+ f"({self.batch},{self.heads},{self.packed_width})"
184
+ )
185
+ take = min(max(1, int(k)), self.length)
186
+ best_indices = torch.empty(
187
+ (self.batch, self.heads, 0), dtype=torch.long, device=self.device
188
+ )
189
+ best_distances = torch.empty(
190
+ (self.batch, self.heads, 0), dtype=torch.int32, device=self.device
191
+ )
192
+ absolute = 0
193
+ tie_base = self.length + 1
194
+ for page_id, page in enumerate(self.k_pages):
195
+ used = min(self.page_size, self.length - absolute)
196
+ if used <= 0:
197
+ break
198
+ distance = self._popcount_bytes(page[:, :, :used] ^ query[:, :, None])
199
+ indices = torch.arange(
200
+ absolute, absolute + used, dtype=torch.long, device=self.device
201
+ ).view(1, 1, used).expand(self.batch, self.heads, used)
202
+ candidate_indices = torch.cat((best_indices, indices), dim=-1)
203
+ candidate_distances = torch.cat((best_distances, distance), dim=-1)
204
+ rank_key = candidate_distances.to(torch.int64) * tie_base + candidate_indices
205
+ selected = torch.topk(
206
+ rank_key, min(take, rank_key.shape[-1]), largest=False, sorted=True
207
+ ).indices
208
+ best_indices = torch.gather(candidate_indices, -1, selected)
209
+ best_distances = torch.gather(candidate_distances, -1, selected)
210
+ absolute += used
211
+ return best_indices, best_distances
212
+
213
+ def materialize(self, kind: str) -> torch.Tensor:
214
+ if kind not in {"k", "v"}:
215
+ raise ValueError("kind must be 'k' or 'v'")
216
+ pages = self.k_pages if kind == "k" else self.v_pages
217
+ if not pages:
218
+ return torch.empty(self.shape, dtype=torch.uint8, device=self.device)
219
+ parts = []
220
+ remaining = self.length
221
+ for page in pages:
222
+ used = min(self.page_size, remaining)
223
+ parts.append(page[:, :, :used])
224
+ remaining -= used
225
+ return torch.cat(parts, dim=2)
226
+
227
+ def accounting(self) -> ArchiveAccounting:
228
+ payload = 2 * self.batch * self.heads * self.length * self.packed_width
229
+ allocated = (
230
+ 2
231
+ * self.batch
232
+ * self.heads
233
+ * len(self.k_pages)
234
+ * self.page_size
235
+ * self.packed_width
236
+ )
237
+ positions = len(self.position_pages) * self.page_size * 8
238
+ return ArchiveAccounting(
239
+ logical_tokens=self.length,
240
+ page_count=len(self.k_pages),
241
+ payload_bytes=payload,
242
+ allocated_payload_bytes=allocated,
243
+ position_index_bytes=positions,
244
+ bytes_copied_on_append=self.bytes_copied_on_append,
245
+ )
246
+
247
+ def state(self) -> dict:
248
+ return {
249
+ "format": self.format,
250
+ "version": self.state_version,
251
+ "batch": self.batch,
252
+ "heads": self.heads,
253
+ "packed_width": self.packed_width,
254
+ "page_size": self.page_size,
255
+ "length": self.length,
256
+ "bytes_copied_on_append": self.bytes_copied_on_append,
257
+ "k_pages": [page.clone() for page in self.k_pages],
258
+ "v_pages": [page.clone() for page in self.v_pages],
259
+ "position_pages": [page.clone() for page in self.position_pages],
260
+ }
261
+
262
+ @classmethod
263
+ def from_state(cls, state: dict, device=None) -> "PagedKVArchive":
264
+ if state.get("format") != cls.format or int(state.get("version", -1)) != 1:
265
+ raise ValueError("unknown paged KV archive state")
266
+ inferred = state["k_pages"][0].device if state["k_pages"] else "cpu"
267
+ target = torch.device(device or inferred)
268
+ archive = cls(
269
+ state["batch"],
270
+ state["heads"],
271
+ state["packed_width"],
272
+ page_size=state["page_size"],
273
+ device=target,
274
+ )
275
+ archive.k_pages = [page.to(target) for page in state["k_pages"]]
276
+ archive.v_pages = [page.to(target) for page in state["v_pages"]]
277
+ archive.position_pages = [page.to(target) for page in state["position_pages"]]
278
+ archive.length = int(state["length"])
279
+ archive.bytes_copied_on_append = int(state["bytes_copied_on_append"])
280
+ return archive
281
+
282
+
283
+ class PagedKVView:
284
+
285
+ def __init__(self, archive: PagedKVArchive, kind: str):
286
+ self.archive = archive
287
+ self.kind = kind
288
+
289
+ @property
290
+ def shape(self):
291
+ return self.archive.shape
292
+
293
+ @property
294
+ def dtype(self):
295
+ return torch.uint8
296
+
297
+ @property
298
+ def device(self):
299
+ return self.archive.device
300
+
301
+ def materialize(self):
302
+ return self.archive.materialize(self.kind)
303
+
304
+
305
+ class ExactChunkCountView:
306
+
307
+ def __init__(self, archive: PagedKVArchive, chunk_size: int):
308
+ self.archive = archive
309
+ self.chunk_size = int(chunk_size)
310
+
311
+ @property
312
+ def shape(self):
313
+ complete = self.archive.length // self.chunk_size
314
+ return (
315
+ self.archive.batch,
316
+ self.archive.heads,
317
+ complete,
318
+ self.archive.packed_width,
319
+ )
finetune/modeling/repack_shdw.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys, struct, numpy as np
2
+ src, dst = sys.argv[1], sys.argv[2]; FP16 = "--fp16" in sys.argv
3
+ f = open(src, "rb"); assert f.read(4) == b"SHDW"; ver, n = struct.unpack("<II", f.read(8))
4
+ out = open(dst, "wb"); out.write(b"SHDW"); out.write(struct.pack("<II", 1, n))
5
+ tern_in = tern_out = dense_in = dense_out = 0
6
+ for _ in range(n):
7
+ nl, = struct.unpack("<I", f.read(4)); name = f.read(nl); kind, = struct.unpack("<I", f.read(4))
8
+ out.write(struct.pack("<I", nl)); out.write(name)
9
+ if kind == 0:
10
+ nd, = struct.unpack("<I", f.read(4)); dims = struct.unpack("<" + "I" * nd, f.read(4 * nd)); c = int(np.prod(dims))
11
+ a = np.frombuffer(f.read(4 * c), np.float32); dense_in += 4 * c
12
+ if FP16 and c >= 4096: out.write(struct.pack("<I", 5)); out.write(struct.pack("<I", nd)); out.write(struct.pack("<" + "I" * nd, *dims)); out.write(a.astype(np.float16).tobytes()); dense_out += 2 * c
13
+ else: out.write(struct.pack("<I", 0)); out.write(struct.pack("<I", nd)); out.write(struct.pack("<" + "I" * nd, *dims)); out.write(a.tobytes()); dense_out += 4 * c
14
+ elif kind == 1:
15
+ o, i, g, st = struct.unpack("<IIII", f.read(16)); G = i // g; Npad = (o + 63) & ~63; nch = Npad // 64
16
+ cb = f.read(st * g * 16 * 4); idx = f.read(st * nch * G * 32); sc = f.read(Npad * 4)
17
+ out.write(struct.pack("<I", 1)); out.write(struct.pack("<IIII", o, i, g, st)); out.write(cb); out.write(idx); out.write(sc)
18
+ elif kind == 3:
19
+ o, i = struct.unpack("<II", f.read(8)); packed = np.frombuffer(f.read(o * i // 4), np.uint8).reshape(o, i // 4); rs = f.read(o * 4)
20
+ tern_in += packed.nbytes
21
+ c4 = np.stack([(packed >> (2 * j)) & 3 for j in range(4)], -1).reshape(o, i)
22
+ pad = (-i) % 5; c5 = np.concatenate([c4, np.ones((o, pad), np.uint8)], 1).reshape(o, -1, 5).astype(np.uint16)
23
+ p5 = (c5[:, :, 0] + 3 * c5[:, :, 1] + 9 * c5[:, :, 2] + 27 * c5[:, :, 3] + 81 * c5[:, :, 4]).astype(np.uint8)
24
+ out.write(struct.pack("<I", 4)); out.write(struct.pack("<II", o, i)); out.write(p5.tobytes()); out.write(rs); tern_out += p5.nbytes
25
+ else: raise SystemExit(f"unknown kind {kind}")
26
+ out.close()
27
+ import os
28
+ print(f"ternary {tern_in/1e6:.1f} -> {tern_out/1e6:.1f} MB | dense {dense_in/1e6:.1f} -> {dense_out/1e6:.1f} MB | file {os.path.getsize(src)/1e6:.1f} -> {os.path.getsize(dst)/1e6:.1f} MB")
finetune/pirate.shdw ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3308a5706c4a56f404e117a147c1c5c7b469df2dd890336f88df9bce294388bf
3
+ size 52342802
finetune/shadow250m_instruct.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c23bde9d8669f38842328a6e7fc940353342750aa264056530ea6d0da11151c0
3
+ size 538876067
framework.png ADDED

Git LFS Details

  • SHA256: 7632aa459ccb728398aacd77cb72468b730c4504921e5a1277ec5eedd67fb233
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
shadow_chat.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Start chatting with SHADOW. Picks the right binary for your system automatically.
2
+ python shadow_chat.py
3
+ """
4
+ import os, sys, platform, subprocess, pathlib
5
+ HERE = pathlib.Path(__file__).resolve().parent
6
+ osname = platform.system()
7
+ if osname == "Windows": k = HERE / "deployment" / "bin" / "windows" / "shadow.exe"
8
+ elif osname == "Linux": k = HERE / "deployment" / "bin" / "linux" / "shadow"
9
+ else: sys.exit("macOS build available on request: saikiranbathula1@gmail.com")
10
+ if osname != "Windows": os.chmod(k, 0o755)
11
+ sys.path.insert(0, str(HERE))
12
+ from shadow_runtime import Engine
13
+ eng = Engine(str(HERE / "deployment" / "shadow250m_instruct.shdw"), str(HERE / "deployment" / "fp131072.npy"), kernel=str(k))
14
+ print("SHADOW 250M. Type your message, 'quit' to stop.")
15
+ while True:
16
+ try: q = input("you> ").strip()
17
+ except EOFError: break
18
+ if q in ("quit", "exit"): break
19
+ if q: print("shadow>", eng.chat(q))
shadow_runtime/__init__.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SHADOW 250M runtime: chat through the bundled CPU kernel, and question answering over an on-disk archive.
2
+
3
+ from shadow_runtime import Engine
4
+ eng = Engine("shadow250m_instruct.shdw", "fp131072.npy", archive="path/to/archive_dir")
5
+ print(eng.answer("your question"))
6
+
7
+ An archive directory holds tokens.u32 (uint32 token stream). The lexical index is built once on first use
8
+ and cached next to it. Chat without an archive:
9
+
10
+ eng = Engine("shadow250m_instruct.shdw", "fp131072.npy")
11
+ print(eng.chat("Explain photosynthesis in two sentences."))
12
+
13
+ CLI: python -m shadow_runtime --model shadow250m_instruct.shdw --table fp131072.npy \
14
+ --archive path/to/archive --ask "your question"
15
+ """
16
+ import os, sys, pathlib, subprocess
17
+ import numpy as np
18
+ HERE = pathlib.Path(__file__).resolve().parent
19
+ sys.path.insert(0, str(HERE))
20
+ from retriever import Inverted, load_archive, enc, _dec
21
+ from answer_engine import Engine as _Extractor, ABSTAIN
22
+ from prompt import EOT
23
+
24
+ BOS, SOT, EOS = 2, 8, 1
25
+
26
+ class Engine:
27
+ def __init__(s, model, table, archive=None, kernel=None, threads=None):
28
+ s.model = str(pathlib.Path(model).resolve()); s.table = str(pathlib.Path(table).resolve())
29
+ s.kernel = str(pathlib.Path(kernel).resolve()) if kernel else str((HERE.parent / "deployment" / "bin" / ("windows/shadow.exe" if os.name == "nt" else "linux/shadow")).resolve())
30
+ s.env = dict(os.environ)
31
+ if threads: s.env["SHADOW_THREADS"] = str(threads)
32
+ s.ext = None
33
+ if archive:
34
+ tok, meta, _bank = load_archive(str(archive))
35
+ inv = Inverted(tok)
36
+ s.ext = _Extractor(tok, inv, model_ask=s.chat)
37
+ def _gen(s, ids, n=140, extra=()):
38
+ r = subprocess.run([s.kernel, s.model, s.table, " ".join(map(str, ids)), str(n), *extra],
39
+ capture_output=True, text=True, env=s.env)
40
+ out = [int(x) for x in r.stdout.split()]
41
+ for stop in (EOT, EOS):
42
+ if stop in out: out = out[:out.index(stop)]
43
+ return _dec(out).strip()
44
+ def chat(s, message, n=160, greedy=True, temp=0.25, topk=30, rep=1.15, seed=0):
45
+ ids = [BOS, SOT] + enc("user\n") + enc(message) + [EOT] + enc("\n") + [SOT] + enc("model\n")
46
+ extra = () if greedy else ("--temp", str(temp), "--topk", str(topk), "--rep", str(rep), "--seed", str(seed))
47
+ return s._gen(ids, n, extra)
48
+ def answer(s, question):
49
+ if s.ext is None: return s.chat(question)
50
+ a, how = s.ext.answer(question)
51
+ return a
shadow_runtime/__main__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, sys
2
+ from . import Engine
3
+ ap = argparse.ArgumentParser()
4
+ ap.add_argument("--model", required=True); ap.add_argument("--table", required=True)
5
+ ap.add_argument("--archive"); ap.add_argument("--ask"); ap.add_argument("--chat", action="store_true")
6
+ a = ap.parse_args()
7
+ eng = Engine(a.model, a.table, archive=a.archive)
8
+ if a.ask: print(eng.answer(a.ask))
9
+ elif a.chat:
10
+ while True:
11
+ try: q = input("you> ")
12
+ except EOFError: break
13
+ if not q.strip(): continue
14
+ print("shadow>", eng.chat(q))
shadow_runtime/answer_engine.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic-extraction answer engine over the SHADOW archive pipeline.
2
+
3
+ Pipeline (each step deterministic or measured ~100%):
4
+ 1. retrieve 16 chunks (lexical inverted index, 2 hops) -- measured 0.98-1.00 recall
5
+ 2. locate the question's KEY inside the retrieved text by string match -- deterministic
6
+ (key = identifier-shaped tokens in the question; fallback = rarest word n-gram)
7
+ 3. no match -> "NOT IN CONTEXT" -- deterministic abstain
8
+ 4. extract the VALUE from the matched sentence: the value is the maximal
9
+ content span of the sentence that does NOT appear in the question,
10
+ type-filtered by the question word (how many->number, who->name, ...) -- deterministic
11
+ 5. recency: several matches -> highest archive position wins -- deterministic
12
+ 2-hop: matched sentence names another identifier and holds no value ->
13
+ re-query with that identifier and extract there -- deterministic + retrieval
14
+ count ("how many ... listed"): count distinct matches -- deterministic
15
+ 6. anything without an identifier-style key (natural QA) -> the neural model answers as before.
16
+
17
+ The network still does chat and natural QA; archive fact lookup is a verified path. Disclosed as
18
+ "hybrid (neural + deterministic extraction)" wherever results are published.
19
+ """
20
+ import re, sys, pathlib
21
+ import numpy as np
22
+ HERE = pathlib.Path(__file__).resolve().parent
23
+ sys.path.insert(0, str(HERE))
24
+ from retriever import enc, _dec, BLK, stop_ids
25
+
26
+ ABSTAIN = "NOT IN CONTEXT"
27
+ ID_RE = re.compile(r"\b((?=[A-Za-z0-9_-]*\d)[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)+|[A-Z][a-z]+[A-Z][a-z]+[A-Za-z]*)\b")
28
+ SENT_SPLIT = re.compile(r"(?<=[.!?])\s+|\n+")
29
+ STOP_WORDS = set("""a an the is are was were be been being of in on at to from by for with under over into as and or if
30
+ that this these those it its his her their there here does do did has have had holds hold most recent statement if
31
+ several use please what which who whom whose when where how many much give state say tell answer question retrieved
32
+ passages above only reply exactly not them im""".split())
33
+
34
+ def question_keys(q):
35
+ ids = ID_RE.findall(q)
36
+ seen = set(); out = []
37
+ for x in ids:
38
+ if x not in seen: seen.add(x); out.append(x)
39
+ return out
40
+
41
+ def sentences_with(text, key):
42
+ """(sentence, char_start) for every sentence in text containing key (word-boundary)."""
43
+ out = []
44
+ kre = re.compile(re.escape(key) + r"(?![A-Za-z0-9])")
45
+ # manual split with offsets
46
+ bounds = [0] + [mm.end() for mm in re.finditer(r"[.!?\n]", text)] + [len(text)]
47
+ for i in range(len(bounds) - 1):
48
+ s = text[bounds[i]:bounds[i + 1] + 1]
49
+ if kre.search(s): out.append((s.strip(), bounds[i]))
50
+ return out
51
+
52
+ def q_type(q):
53
+ ql = q.lower()
54
+ if "how many" in ql or "at how many" in ql: return "number"
55
+ if ql.startswith("who ") or " who " in ql or "assigned to" in ql or "sealed by" in ql or "signed by" in ql or "came from" in ql or "maintain" in ql or "bid" in ql: return "name"
56
+ if ql.startswith("when ") or "expire" in ql or "accessed" in ql or "date" in ql: return "date"
57
+ return "any"
58
+
59
+ def candidate_value(sent, q, want):
60
+ """maximal non-question content spans of the sentence; choose by type then by position (later wins)."""
61
+ qw0 = {w.lower().strip(".,?!'\"()") for w in q.split()}
62
+ qwords = set(qw0)
63
+ for w in qw0: # morphological variants: weigh/weighs, expire/expires
64
+ qwords.add(w + "s"); qwords.add(w + "es"); qwords.add(w + "d"); qwords.add(w + "ed")
65
+ if w.endswith("s"): qwords.add(w[:-1])
66
+ toks = re.findall(r"0x[0-9a-f]+|[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+|\d+\.\d+|[A-Za-z0-9][A-Za-z0-9']*|'[^']*'", sent)
67
+ spans = []; cur = []
68
+ for w in toks:
69
+ wl = w.lower().strip("'")
70
+ skip = (wl in qwords) or (wl in STOP_WORDS)
71
+ if skip:
72
+ if cur: spans.append(" ".join(cur)); cur = []
73
+ else: cur.append(w.strip("'"))
74
+ if cur: spans.append(" ".join(cur))
75
+ if not spans: return None
76
+ def is_num(s): return bool(re.fullmatch(r"\d+(?:\.\d+)?", s.replace(" ", "")))
77
+ def is_date(s): return bool(re.search(r"(January|February|March|April|May|June|July|August|September|October|November|December)", s))
78
+ def is_name(s): return bool(re.fullmatch(r"[A-Z][a-z]+ [A-Z][a-z]+", s))
79
+ if want == "number":
80
+ cs = [s for s in spans if is_num(s)]
81
+ if cs: return cs[-1]
82
+ if want == "date":
83
+ cs = [s for s in spans if is_date(s)]
84
+ if cs: return cs[-1]
85
+ if want == "name":
86
+ cs = [s for s in spans if is_name(s)]
87
+ if cs: return cs[-1]
88
+ return spans[-1]
89
+
90
+ class Engine:
91
+ def __init__(s, tok_or_ov, inv, model_ask=None, hops=2, k=16):
92
+ s.ov = tok_or_ov; s.inv = inv; s.model_ask = model_ask; s.hops = hops; s.k = k
93
+ s.is_overlay = hasattr(tok_or_ov, "block")
94
+ def _chunk_text(s, blocks):
95
+ """decode retrieved (b,b+1) chunks; return [(pos_block, text)] ordered by pos."""
96
+ got = sorted({int(b) for b in blocks})
97
+ out = []
98
+ for b in got:
99
+ if s.is_overlay: t = _dec(s.ov.block(b) + s.ov.block(min(b + 1, s.ov.nb - 1)))
100
+ else: t = _dec(np.asarray(s.ov[b * BLK:(b + 2) * BLK], np.int64))
101
+ out.append((b, t))
102
+ return out
103
+ def _retrieve(s, q):
104
+ from retriever import Inverted
105
+ qi = enc(q)
106
+ idx = s.inv.topk_hops(qi, s.k, rounds=s.hops)[0] if not s.is_overlay else None
107
+ if s.is_overlay:
108
+ import bench_longctx as B
109
+ idx = B.ov_topk_hops(s.inv, s.ov, qi, s.k)
110
+ return idx
111
+ def answer(s, q):
112
+ keys = question_keys(q)
113
+ if not keys:
114
+ return (s.model_ask(q) if s.model_ask else ABSTAIN), "neural"
115
+ idx = s._retrieve(q); chunks = s._chunk_text(idx)
116
+ want = q_type(q)
117
+ key = keys[0]
118
+ # count task: count distinct "Member i of KEY" indices; members are numbered 1..n, so the answer is
119
+ # max(index) (robust to a missed block) cross-checked with the distinct count; wider retrieval (k=32).
120
+ if re.search(r"how many .* (listed|are there|in the archive)", q.lower()):
121
+ k_save = s.k; s.k = 32
122
+ idx = s._retrieve(q); chunks = s._chunk_text(idx); s.k = k_save
123
+ seen = set()
124
+ for _, t in chunks:
125
+ for sent, _o in sentences_with(t, key):
126
+ m = re.search(r"Member (\d+) of", sent)
127
+ if m: seen.add(int(m.group(1)))
128
+ if not seen: return ABSTAIN, "count"
129
+ return str(max(max(seen), len(seen))), "count"
130
+ matches = []
131
+ for pos, t in chunks:
132
+ for sent, off in sentences_with(t, key): matches.append((pos, off, sent))
133
+ if not matches: return ABSTAIN, "abstain"
134
+ matches.sort(key=lambda m: (m[0], m[1]))
135
+ # try direct extraction from the LATEST match backwards (recency). A candidate is a POINTER (alias
136
+ # record, e.g. "stored under reference K2") -- not a value -- only when the sentence carries an explicit
137
+ # reference cue; plain identifier-shaped values (serials "SN-...", CamelCase names) are legitimate answers.
138
+ PTR_CUE = re.compile(r"stored under|see reference|filed under|under reference", re.I)
139
+ for pos, off, sent in reversed(matches):
140
+ v = candidate_value(sent, q, want)
141
+ if not v: continue
142
+ if PTR_CUE.search(sent) and " " not in v and ID_RE.fullmatch(v): continue # alias pointer -> 2-hop
143
+ if " " in v and ID_RE.search(v): continue # multi-word span containing an identifier = junk -> 2-hop
144
+ return v, "extract"
145
+ # 2-hop: the matched sentence points at another identifier
146
+ for pos, off, sent in reversed(matches):
147
+ others = [x for x in ID_RE.findall(sent) if x != key and x not in q]
148
+ for k2 in others:
149
+ q2 = q.replace(key, k2)
150
+ idx2 = s._retrieve(q2); chunks2 = s._chunk_text(idx2)
151
+ m2 = []
152
+ for p2, t2 in chunks2:
153
+ for s2, o2 in sentences_with(t2, k2): m2.append((p2, o2, s2))
154
+ for p2, o2, s2 in reversed(sorted(m2)):
155
+ v = candidate_value(s2, q, want)
156
+ if not v or v == key: continue # circular: back to the original key
157
+ if re.search(r"stored under|see reference|filed under|under reference", s2, re.I) and " " not in v and ID_RE.fullmatch(v): continue
158
+ if " " in v and ID_RE.search(v): continue
159
+ return v, "2hop"
160
+ return ABSTAIN, "abstain"
shadow_runtime/prompt.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prompt assembly for retrieval QA. Retrieved chunks are framed inside the user turn:
2
+
3
+ <bos><start_of_turn>user\n [ <unused0> pos=N \n <chunk tokens> <unused1> \n ] x K
4
+ \nUsing only the retrieved passages above, answer the question. If the answer is not in them, reply exactly "NOT IN CONTEXT".
5
+ Question: {q}<end_of_turn>\n<start_of_turn>model\n
6
+ cold mask = True on every token inside a chunk frame (open..close), False elsewhere (hot window).
7
+ Chunks = retrieved 64-token blocks expanded to (b, b+1) and merged when adjacent, ordered by archive position.
8
+ """
9
+ import sys, json, pathlib, re, string
10
+ import numpy as np
11
+ HERE = pathlib.Path(__file__).resolve().parent; PKG = HERE.parent
12
+ sys.path.insert(0, str(HERE))
13
+ from retriever import enc, BLK
14
+ BOS, EOS, SOT, EOT = 2, 1, 8, 9; COPEN, CCLOSE = 6, 7
15
+ ABSTAIN = "NOT IN CONTEXT"
16
+ INSTR = f"\nUsing only the retrieved passages above, answer the question. If the answer is not in them, reply exactly \"{ABSTAIN}\".\nQuestion: "
17
+ _NL = None
18
+ def nl():
19
+ global _NL
20
+ if _NL is None: _NL = enc("\n")
21
+ return _NL
22
+
23
+ def chunks_from_blocks(tok, blocks, expand=1, max_chunks=16):
24
+ """retrieved block ids -> list of (start_block, token list): (b, b+expand) spans, merged when overlapping, by position."""
25
+ spans = sorted({(int(b), int(b) + expand + 1) for b in blocks[:max_chunks]})
26
+ merged = []
27
+ for a, e in spans:
28
+ if merged and a <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], e)
29
+ else: merged.append([a, e])
30
+ nb = len(tok) // BLK
31
+ return [(a, [int(t) for t in tok[a * BLK:min(e, nb) * BLK]]) for a, e in merged if a < nb]
32
+
33
+ def build_prompt(chunks, question):
34
+ """chunks: [(pos_block, ids)] -> (ids, cold_mask) for the user turn + model turn opener."""
35
+ ids = [BOS, SOT] + enc("user\n"); cold = [False] * len(ids)
36
+ for pos, ct in chunks:
37
+ fr = [COPEN] + enc(f"pos={pos}\n"); body = ct; close = [CCLOSE] + nl()
38
+ seg = fr + body + close; ids += seg; cold += [True] * len(seg)
39
+ tail = enc(INSTR + question) + [EOT] + nl() + [SOT] + enc("model\n")
40
+ ids += tail; cold += [False] * len(tail)
41
+ return ids, cold
42
+
43
+ def normalize(s):
44
+ s = s.lower().strip(); s = "".join(ch for ch in s if ch not in set(string.punctuation)); s = re.sub(r"\b(a|an|the)\b", " ", s)
45
+ return " ".join(s.split())
46
+ def final_answer(text):
47
+ """trace-aware: the answer is the text after the last '->' (quote-then-answer format), else the whole text."""
48
+ return text.rsplit("->", 1)[-1] if "->" in text else text
49
+ def reward(answer_text, gold, task):
50
+ """verifiable reward: 1 if exact match after normalisation (numbers: exact digits), else 0. T5: must be the abstain marker."""
51
+ a = normalize(final_answer(answer_text)); g = normalize(str(gold))
52
+ if task == "T5" or g == normalize(ABSTAIN): return 1.0 if a == normalize(ABSTAIN) else 0.0
53
+ if a == normalize(ABSTAIN): return 0.0
54
+ if a == g: return 1.0
55
+ # short free-form gold (hotpot/squad): token-F1 >= 0.8 counts
56
+ at, gt = a.split(), g.split()
57
+ if not at or not gt: return 0.0
58
+ common_ = sum(min(at.count(w), gt.count(w)) for w in set(at));
59
+ if common_ == 0: return 0.0
60
+ p, r = common_ / len(at), common_ / len(gt); f1 = 2 * p * r / (p + r)
61
+ return 1.0 if f1 >= 0.8 else 0.0
shadow_runtime/retriever.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Archive retriever: exact lexical inverted index over 64-token blocks, with 2-hop follow-up.
2
+
3
+ An archive is a directory with tokens.u32 (uint32 token stream) and meta.json. Blocks of 64 tokens
4
+ are indexed by their content features (stop-filtered unigrams plus hashed bigrams and trigrams,
5
+ idf-weighted). A query returns the top-k blocks; a second hop follows identifiers named near the
6
+ match, so alias chains ("the record for K is stored under reference K2") resolve.
7
+
8
+ Also holds the tokenizer used by the runtime (sentencepiece + id remap).
9
+ """
10
+ import os, sys, json, pathlib, hashlib, re
11
+ import numpy as np
12
+ HERE = pathlib.Path(__file__).resolve().parent; PKG = HERE.parent
13
+ BLK = 64
14
+
15
+ def load_archive(path):
16
+ tok = np.memmap(os.path.join(path, "tokens.u32"), np.uint32, "r")
17
+ meta = json.load(open(os.path.join(path, "meta.json")))
18
+ bp = os.path.join(path, "bank_valid.jsonl"); bp = bp if os.path.exists(bp) else os.path.join(path, "bank.jsonl") # validated bank if present
19
+ bank = [json.loads(l) for l in open(bp, encoding="utf-8")]
20
+ return tok, meta, bank
21
+
22
+ _STOP = None
23
+ def stop_ids(cache=None):
24
+ """500 most frequent ids, fixed once from a held-out fineweb shard (deploy/stop500.npy)."""
25
+ global _STOP
26
+ if _STOP is None:
27
+ _STOP = np.load(PKG / "tokenizer" / "stop500.npy")
28
+ return _STOP
29
+
30
+ NBG = 1 << 20 # hashed bigram feature space
31
+
32
+ def _feats(ids):
33
+ """content features of a token sequence: unique unigrams (stop-filtered) + hashed bigrams of ALL adjacent
34
+ tokens (digits/punctuation are frequent as unigrams but their sequences -- 'Vega-713' -- are the signal)."""
35
+ ids = np.asarray(ids, np.int64); st = stop_ids()
36
+ uni = np.unique(ids); uni = uni[(~np.isin(uni, st)) & (uni > 9)]
37
+ if len(ids) >= 2:
38
+ bg = (ids[:-1] * np.int64(1000003) + ids[1:] * np.int64(7919)) % NBG
39
+ bg = bg[(ids[:-1] > 9) & (ids[1:] > 9)]
40
+ if len(ids) >= 3: # trigrams too: 'Cygnus','-','9' / '9','2','2' make the key number discriminative
41
+ tg = (ids[:-2] * np.int64(1000003) + ids[1:-1] * np.int64(7919) + ids[2:] * np.int64(104729) + np.int64(17)) % NBG
42
+ bg = np.concatenate([bg, tg[(ids[:-2] > 9) & (ids[1:-1] > 9) & (ids[2:] > 9)]])
43
+ bg = np.unique(bg) + (1 << 17)
44
+ else: bg = np.zeros(0, np.int64)
45
+ return uni, bg
46
+
47
+ def _content(ids):
48
+ u, g = _feats(ids); return np.concatenate([u, g])
49
+
50
+
51
+ # ---------------- exact lexical inverted index (BM25-style, unigram + bigram features) ----------------
52
+ # The sketch retrievers above lose the rare-key signal inside 64-token blocks; an inverted index keeps it exactly.
53
+ # Deploy cost at 100M tokens: ~60M postings (~250 MB) next to the 32 GB 1-bit KV archive; query = 3-10 posting lists.
54
+ class Inverted:
55
+ def __init__(s, tok, k1=1.2, b=0.75):
56
+ import scipy.sparse as sp
57
+ nb = len(tok) // BLK; rows = []; cols = []
58
+ for bi in range(nb):
59
+ u, g = _feats(tok[bi * BLK:(bi + 1) * BLK]); f = np.concatenate([u, g]); rows.append(f); cols.append(np.full(len(f), bi, np.int64))
60
+ r = np.concatenate(rows); c = np.concatenate(cols)
61
+ s.M = sp.csr_matrix((np.ones(len(r), np.float32), (r, c)), shape=((1 << 17) + NBG, nb)) # feature x block
62
+ df = np.asarray(s.M.sum(1)).ravel(); s.idf = np.log(1 + (nb - df + 0.5) / (df + 0.5)).astype(np.float32)
63
+ s.nb = nb; s.tok = tok
64
+ def topk(s, ids, k):
65
+ f = _content(ids)
66
+ if len(f) == 0: return np.arange(min(k, s.nb)), np.zeros(min(k, s.nb))
67
+ sc = np.asarray((s.idf[f][None, :] @ s.M[f]).todense()).ravel() if False else s.idf[f] @ s.M[f]
68
+ sc = np.asarray(sc).ravel()
69
+ idx = np.argpartition(-sc, min(k, s.nb - 1))[:k]; idx = idx[np.argsort(-sc[idx], kind="stable")]
70
+ return idx, sc[idx]
71
+ def topk_hops(s, ids, k, rounds=2, k1=8, idf_min=5.0, win=14):
72
+ """multi-hop: round 1 = top-k1 direct hits (kept). Then EACH round-1 block gets its own follow-up query made of
73
+ the rare features within +-win tokens (over the stream) of where the question's tokens matched inside it --
74
+ 'The record for K is stored under reference K2.' -> K2's n-grams -- and contributes its best new block.
75
+ Per-source queries (not a union) so one alias chain cannot be drowned by the others' noise."""
76
+ idx1, sc1 = s.topk(ids, k1); out = [int(i) for i in idx1]; qset = set(_content(ids).tolist()); qtext = _dec(np.asarray(ids, np.int64))
77
+ qtok = set(np.asarray(ids, np.int64).tolist()) - set(stop_ids().tolist()); src = out[:]
78
+ # template features = present in >=2 of the round-1 blocks ('stored under reference', 'access code for vault'):
79
+ # they would pull in every block of the same template; only features UNIQUE to a source block are followed.
80
+ cnt = {}
81
+ for bi in src:
82
+ for f in set(_content(np.asarray(s.tok[bi * BLK:(bi + 1) * BLK], np.int64)).tolist()): cnt[f] = cnt.get(f, 0) + 1
83
+ template = {f for f, c in cnt.items() if c >= 2}
84
+ for r in range(rounds - 1):
85
+ new = []
86
+ for bi in src:
87
+ if len(out) + len(new) >= k: break
88
+ blk = np.asarray(s.tok[bi * BLK:(bi + 1) * BLK], np.int64)
89
+ pos = [i for i, t in enumerate(blk) if t in qtok]
90
+ if not pos: continue
91
+ lo = max(0, bi * BLK + min(pos) - 8); hi = min(len(s.tok), bi * BLK + max(pos) + win + 1)
92
+ w = np.asarray(s.tok[lo:hi], np.int64)
93
+ # follow-up query: entity-like identifiers mentioned near the match (Name-123 style, the shape of archive
94
+ # keys) other than the question's own; fall back to the rare non-template features of the window.
95
+ ents = [e for e in set(re.findall(r"[A-Z][a-z]+-\d+", _dec(w))) if e not in qtext]
96
+ if ents: fb = np.unique(np.concatenate([_content(enc(" " + e)) for e in ents]))
97
+ else:
98
+ fb = _content(w); fb = fb[(s.idf[fb] >= idf_min) & ~np.isin(fb, list(qset | template))]
99
+ if len(fb) == 0: continue
100
+ sc = np.asarray(s.idf[fb] @ s.M[fb]).ravel(); sc[out + new] = -1e9
101
+ for j in np.argsort(-sc)[:2]: # best new block of this chain (skip the source's own neighbourhood)
102
+ j = int(j)
103
+ if abs(j - bi) > 1 and j not in new: new.append(j); break
104
+ out += new; src = new
105
+ if not new: break
106
+ return np.array(out[:k]), np.zeros(min(k, len(out)))
107
+
108
+
109
+ # ---------------- tokenizer (sentencepiece + id remap; expansion for out-of-subset ids computed lazily) ----
110
+ _sp = None; _o2n = None; _n2o = None; _exp_cache = {}
111
+ def _load_tok():
112
+ global _sp, _o2n, _n2o
113
+ if _sp is None:
114
+ import sentencepiece as spm
115
+ _sp = spm.SentencePieceProcessor(model_file=str(PKG / "tokenizer" / "tokenizer.model"))
116
+ _n2o = np.fromfile(PKG / "tokenizer" / "new2old.u32", np.uint32).astype(np.int64)
117
+ _o2n = np.full(262144, -1, np.int64); _o2n[_n2o] = np.arange(len(_n2o))
118
+ def _expand(old_id):
119
+ """out-of-subset token -> its byte-fallback sequence in the 131k id space (lossless)."""
120
+ seq = _exp_cache.get(old_id)
121
+ if seq is None:
122
+ piece = _sp.id_to_piece(int(old_id)).replace("▁", " ")
123
+ raw = _sp.decode([int(old_id)]).encode("utf-8") if not piece.startswith("<0x") else bytes([int(piece[3:5], 16)])
124
+ seq = [int(_o2n[_sp.piece_to_id(f"<0x{b:02X}>")]) for b in raw]
125
+ _exp_cache[old_id] = seq
126
+ return seq
127
+ def enc(text):
128
+ _load_tok()
129
+ out = []
130
+ for i in _sp.encode(text):
131
+ n = _o2n[i]
132
+ if n >= 0: out.append(int(n))
133
+ else: out.extend(_expand(i))
134
+ return out
135
+ def _dec(ids):
136
+ _load_tok()
137
+ return _sp.decode(_n2o[np.asarray(ids, np.int64)].tolist())
tokenizer/new2old.u32 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d16fc82774e38698e919ebae21aa3969c52f3d846bd949dd3169452273b04635
3
+ size 524288
tokenizer/stop500.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:983d343784453f07b3f0f4cd2c16a685aeece4b4a5f6639e700a81aeafff2e38
3
+ size 4128
tokenizer/tokenizer.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1299c11d7cf632ef3b4e11937501358ada021bbdf7c47638d13c0ee982f2e79c
3
+ size 4689074