mukunds commited on
Commit
e29f88c
·
verified ·
1 Parent(s): 5156998

Publish frozen LoopFormer pointer checkpoint and inference code

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ base_model: Qwen/Qwen2.5-0.5B-Instruct
4
+ tags:
5
+ - loopformer
6
+ - recurrent-transformer
7
+ - pointer-chasing
8
+ - custom-code
9
+ ---
10
+
11
+ # LoopFormer: recurrent pointer execution
12
+
13
+ Frozen final checkpoint: `controller-shared-number-precision-seed83`.
14
+ This is a custom recurrent Qwen model for following a supplied A–Z rule table.
15
+ It requires the included Python inference code; ordinary `AutoModelForCausalLM`
16
+ generation does not implement its recurrence.
17
+
18
+ ## Download and run
19
+
20
+ Use Python 3.11 or 3.12. Install a PyTorch build suitable for your device.
21
+ The requirements record the validated research dependency versions; the Space
22
+ adds Gradio and `spaces` for hosting. Export a graph request from the playground,
23
+ or create JSON with `mapping` (26 ordered `[source, destination]` pairs), `start`
24
+ (one A–Z letter), and `steps` (a positive integer).
25
+
26
+ ```bash
27
+ hf download mukunds/loopformer --local-dir loopformer-model
28
+ cd loopformer-model
29
+ python -m pip install -r requirements.txt
30
+ python -m scripts.demo --checkpoint . --request graph.json --device cuda --download
31
+ ```
32
+
33
+ The explicit `--download` option permits downloading the pinned Qwen base
34
+ revision. It is needed on the first run. `--device cpu` and `--device mps` are
35
+ also available, but their latency and final-checkpoint fidelity must be measured
36
+ on the selected device. Each output line is a cumulative JSON snapshot of actual
37
+ model predictions; the final line includes separate correctness/stopping metrics.
38
+
39
+ The default requested-step ceiling is 256 and the safety cap is 272. To explore
40
+ longer rollouts, explicitly increase both `--max-steps` and `--loop-cap`. This
41
+ does not establish graph-execution accuracy beyond the evaluated 256 steps.
42
+ The symbol vocabulary remains exactly A–Z; 256 steps does not mean 256 nodes.
43
+
44
+ ## Architecture and inference
45
+
46
+ The base is `Qwen/Qwen2.5-0.5B-Instruct`, revision
47
+ `7ae557604adf67be50417f59c2c2f167def9a775`. Six prelude layers prepare the prompt;
48
+ twelve shared middle layers update a recurrent working state; six coda layers
49
+ read out a distribution over the 26 letters after every loop. A trained bridge
50
+ re-enters the shared middle block. Fixed prefix memory is reused within a run.
51
+ The final executor adapts all R weights and freezes P/C.
52
+
53
+ The Steps field is routed away from the executor. A shared learned number reader
54
+ initializes a separate scalar timer, whose learned affine recurrence and stop
55
+ readout determine completion. Stop probability is a timing signal, not a
56
+ correctness estimate. Decoded predictions and reference states are never fed
57
+ back into recurrence. Default inference is FP32 with SDPA and threshold 0.5.
58
+
59
+ `adapter_model.pt` contains all adapted executor/controller tensors, not merely
60
+ LoRA weights. SHA-256:
61
+ `0f8a3f2572ec2caf4fafbcdd074b5283bd8c610602502763b3c04487ce1f123d`.
62
+ Optimizer state and training data are excluded from this inference release.
63
+
64
+ ## Evidence and limitations
65
+
66
+ On the frozen final panel of 1,350 new graphs and requests 1–256:
67
+
68
+ - Final letter plus exact stopping: 97.81% over 345,600 graph/count queries.
69
+ - Complete trajectories through depth 256: 96.89% (1,308/1,350 graphs).
70
+ - Exact stopping: 100% across that finite requested-count panel.
71
+
72
+ Executor supervision used depths 1–6, 8, 10, 12. Controller fitting used
73
+ counts 1–63 excluding 9, 17, 29, 41, 53, with supervised unrolls capped at 12.
74
+ Controller-only numerical results at longer horizons are distinct from executing
75
+ graphs correctly. This is one task family, not general reasoning or transfer
76
+ evidence. Arbitrary hand-edited graphs can fail. Changes to precision, runtime,
77
+ hardware or kernels need validation against native intermediate readouts.
78
+
79
+ See the [source and full evidence](https://github.com/MukundShankar-dev/loopformer)
80
+ and the [research journal](https://mukundshankar-dev.github.io/projects/loopformer/).
adapter_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f8a3f2572ec2caf4fafbcdd074b5283bd8c610602502763b3c04487ce1f123d
3
+ size 719156751
chat_template.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0]['role'] == 'system' %}
4
+ {{- messages[0]['content'] }}
5
+ {%- else %}
6
+ {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
7
+ {%- endif %}
8
+ {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
9
+ {%- for tool in tools %}
10
+ {{- "\n" }}
11
+ {{- tool | tojson }}
12
+ {%- endfor %}
13
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
14
+ {%- else %}
15
+ {%- if messages[0]['role'] == 'system' %}
16
+ {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
17
+ {%- else %}
18
+ {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
19
+ {%- endif %}
20
+ {%- endif %}
21
+ {%- for message in messages %}
22
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
23
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
24
+ {%- elif message.role == "assistant" %}
25
+ {{- '<|im_start|>' + message.role }}
26
+ {%- if message.content %}
27
+ {{- '\n' + message.content }}
28
+ {%- endif %}
29
+ {%- for tool_call in message.tool_calls %}
30
+ {%- if tool_call.function is defined %}
31
+ {%- set tool_call = tool_call.function %}
32
+ {%- endif %}
33
+ {{- '\n<tool_call>\n{"name": "' }}
34
+ {{- tool_call.name }}
35
+ {{- '", "arguments": ' }}
36
+ {{- tool_call.arguments | tojson }}
37
+ {{- '}\n</tool_call>' }}
38
+ {%- endfor %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- elif message.role == "tool" %}
41
+ {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
42
+ {{- '<|im_start|>user' }}
43
+ {%- endif %}
44
+ {{- '\n<tool_response>\n' }}
45
+ {{- message.content }}
46
+ {{- '\n</tool_response>' }}
47
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
48
+ {{- '<|im_end|>\n' }}
49
+ {%- endif %}
50
+ {%- endif %}
51
+ {%- endfor %}
52
+ {%- if add_generation_prompt %}
53
+ {{- '<|im_start|>assistant\n' }}
54
+ {%- endif %}
recurrent_config.json ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
3
+ "revision": "7ae557604adf67be50417f59c2c2f167def9a775",
4
+ "recurrent_start": 6,
5
+ "recurrent_end": 18,
6
+ "lora_rank": 8,
7
+ "lora_alpha": 16,
8
+ "symbols": [
9
+ "A",
10
+ "B",
11
+ "C",
12
+ "D",
13
+ "E",
14
+ "F",
15
+ "G",
16
+ "H",
17
+ "I",
18
+ "J",
19
+ "K",
20
+ "L",
21
+ "M",
22
+ "N",
23
+ "O",
24
+ "P",
25
+ "Q",
26
+ "R",
27
+ "S",
28
+ "T",
29
+ "U",
30
+ "V",
31
+ "W",
32
+ "X",
33
+ "Y",
34
+ "Z"
35
+ ],
36
+ "token_ids": [
37
+ 362,
38
+ 425,
39
+ 356,
40
+ 422,
41
+ 468,
42
+ 434,
43
+ 479,
44
+ 472,
45
+ 358,
46
+ 619,
47
+ 730,
48
+ 444,
49
+ 386,
50
+ 451,
51
+ 506,
52
+ 393,
53
+ 1207,
54
+ 431,
55
+ 328,
56
+ 350,
57
+ 547,
58
+ 647,
59
+ 467,
60
+ 1599,
61
+ 809,
62
+ 1863
63
+ ],
64
+ "prompt_format": "dataset_raw",
65
+ "loss_vocabulary": "symbols",
66
+ "train_max_depth": 12,
67
+ "train_depths": [
68
+ 1,
69
+ 2,
70
+ 3,
71
+ 4,
72
+ 5,
73
+ 6,
74
+ 8,
75
+ 10,
76
+ 12
77
+ ],
78
+ "recurrence_mode": "fixed_prompt",
79
+ "completion_head": {
80
+ "intermediate": 1
81
+ },
82
+ "completion_loss_weight": 0.1,
83
+ "training_compute": {
84
+ "precision": "bf16",
85
+ "attention": "sdpa"
86
+ },
87
+ "executor": {
88
+ "train_scope": "recurrent",
89
+ "isolated": true,
90
+ "bridge": true,
91
+ "direct_readout": true,
92
+ "controller_size": 1,
93
+ "prefix_reuse": true,
94
+ "gradient_checkpointing": true,
95
+ "controller_kind": "shared_number"
96
+ },
97
+ "format": "loopformer-executor-v2",
98
+ "controller_training": {
99
+ "source": "models/stage1_pointer/controller-shared-number-seed83/best",
100
+ "source_checkpoint_sha256": "42059fd6fd4cc1ffe05a705a99a7aba76d4e7e0d87933bad18c7adc4cdee1110",
101
+ "trained_depths": [
102
+ 1,
103
+ 2,
104
+ 3,
105
+ 4,
106
+ 5,
107
+ 6,
108
+ 7,
109
+ 8,
110
+ 10,
111
+ 11,
112
+ 12,
113
+ 13,
114
+ 14,
115
+ 15,
116
+ 16,
117
+ 18,
118
+ 19,
119
+ 20,
120
+ 21,
121
+ 22,
122
+ 23,
123
+ 24,
124
+ 25,
125
+ 26,
126
+ 27,
127
+ 28,
128
+ 30,
129
+ 31,
130
+ 32,
131
+ 33,
132
+ 34,
133
+ 35,
134
+ 36,
135
+ 37,
136
+ 38,
137
+ 39,
138
+ 40,
139
+ 42,
140
+ 43,
141
+ 44,
142
+ 45,
143
+ 46,
144
+ 47,
145
+ 48,
146
+ 49,
147
+ 50,
148
+ 51,
149
+ 52,
150
+ 54,
151
+ 55,
152
+ 56,
153
+ 57,
154
+ 58,
155
+ 59,
156
+ 60,
157
+ 61,
158
+ 62,
159
+ 63
160
+ ],
161
+ "training_loops": 12,
162
+ "optimization_scope": "countdown_cell_only",
163
+ "config": {
164
+ "source": "models/stage1_pointer/controller-shared-number-seed83/best",
165
+ "training_config": "configs/controller_affine.json",
166
+ "output": "models/stage1_pointer/controller-shared-number-precision-seed83"
167
+ },
168
+ "fit": {
169
+ "method": "float64 L-BFGS on original free-running numerical prefix labels; float32 deployment",
170
+ "training_counts": [
171
+ 1,
172
+ 2,
173
+ 3,
174
+ 4,
175
+ 5,
176
+ 6,
177
+ 7,
178
+ 8,
179
+ 10,
180
+ 11,
181
+ 12,
182
+ 13,
183
+ 14,
184
+ 15,
185
+ 16,
186
+ 18,
187
+ 19,
188
+ 20,
189
+ 21,
190
+ 22,
191
+ 23,
192
+ 24,
193
+ 25,
194
+ 26,
195
+ 27,
196
+ 28,
197
+ 30,
198
+ 31,
199
+ 32,
200
+ 33,
201
+ 34,
202
+ 35,
203
+ 36,
204
+ 37,
205
+ 38,
206
+ 39,
207
+ 40,
208
+ 42,
209
+ 43,
210
+ 44,
211
+ 45,
212
+ 46,
213
+ 47,
214
+ 48,
215
+ 49,
216
+ 50,
217
+ 51,
218
+ 52,
219
+ 54,
220
+ 55,
221
+ 56,
222
+ 57,
223
+ 58,
224
+ 59,
225
+ 60,
226
+ 61,
227
+ 62,
228
+ 63
229
+ ],
230
+ "training_loops": 12,
231
+ "objective_evaluations": 10,
232
+ "before_mse": 4.1626904266916364e-09,
233
+ "fitted_mse": 2.002921115782509e-12,
234
+ "deployed_mse": 6.307695082326698e-12,
235
+ "learned_gain": 1.0,
236
+ "learned_offset": -0.9999999403953552,
237
+ "numeric_targets_are_inputs": false
238
+ },
239
+ "selection": "One predetermined training-label fit; no stress/graph benchmark selection"
240
+ }
241
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch==2.14.0
2
+ transformers==5.17.0
3
+ tokenizers==0.23.2
4
+ huggingface-hub==1.31.0
5
+ peft==0.20.0
6
+ numpy==2.4.6
scripts/__init__.py ADDED
File without changes
scripts/dataset/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Synthetic tasks with exact reference execution; no model loading."""
scripts/dataset/__main__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Run the dataset CLI with python -m scripts.dataset."""
2
+
3
+ from scripts.dataset.cli import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ main()
scripts/dataset/benchmark.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Independent, seed-replayable graph panels for frozen pointer benchmarks."""
2
+ from dataclasses import replace
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from .pointer import (PointerExample, example_seed, execute, generate_unconditioned_example,
7
+ orbit_structure, render_prompt, validate_example)
8
+
9
+
10
+ def count_variant(task: PointerExample, count: int) -> PointerExample:
11
+ """Change requested horizon/labels only; never condition graph sampling on it."""
12
+ states = execute(dict(task.mapping), task.initial_state, count)
13
+ result = replace(task, example_id=f'{task.example_id}-steps-{count}', task_depth=count,
14
+ intermediate_states=states, final_state=states[-1],
15
+ prompt=render_prompt(task.mapping, task.initial_state, count))
16
+ validate_example(result)
17
+ return result
18
+
19
+
20
+ def independent_graphs(config: dict, excluded: set[str]) -> tuple[list[PointerExample], list[dict]]:
21
+ """Balanced seed/mode strata; reject only graph identity overlap, never difficulty."""
22
+ if config['graphs_per_seed_mode'] < 1 or not 1 <= config['max_depth'] <= 256:
23
+ raise ValueError('Need positive graph count and a maximum depth in 1..256')
24
+ if len(set(config['seeds'])) != len(config['seeds']) or len(set(config['graph_modes'])) != len(config['graph_modes']):
25
+ raise ValueError('Repeated seeds or graph modes')
26
+ used = set(excluded)
27
+ tasks, metadata = [], []
28
+ for seed in config['seeds']:
29
+ for mode in config['graph_modes']:
30
+ for index in range(config['graphs_per_seed_mode']):
31
+ for attempt in range(1000):
32
+ task = generate_unconditioned_example(example_seed(seed, f'benchmark-{mode}', index, attempt),
33
+ config['max_depth'], 'benchmark', index, mode)
34
+ if task.mapping_sha256 not in used:
35
+ break
36
+ else:
37
+ raise ValueError('Unable to sample a new rule table after 1000 attempts')
38
+ used.add(task.mapping_sha256)
39
+ task = replace(task, example_id=f'benchmark-{seed}-{mode}-{index:06d}-{task.seed:016x}')
40
+ validate_example(task)
41
+ tasks.append(task)
42
+ metadata.append({'graph_index': len(tasks) - 1, 'example_id': task.example_id,
43
+ 'mapping_sha256': task.mapping_sha256, 'dataset_seed': seed, 'graph_mode': mode,
44
+ **orbit_structure(dict(task.mapping), task.initial_state)})
45
+ return tasks, metadata
46
+
47
+
48
+ def exclusion_hashes(directory: Path) -> set[str]:
49
+ """Read graph fingerprints, including reserved splits, solely to prevent overlap."""
50
+ hashes = set()
51
+ for path in sorted(directory.glob('*.jsonl')):
52
+ for line in path.open():
53
+ record = json.loads(line)
54
+ hashes.add(record['mapping_sha256'])
55
+ if not hashes:
56
+ raise ValueError('No old dataset graph fingerprints found')
57
+ return hashes
scripts/dataset/cli.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate or verify reproducible Stage 1 pointer data (no model training).
2
+
3
+ Run from the repository root: python -m scripts.dataset --help
4
+ """
5
+
6
+ import argparse
7
+ from dataclasses import asdict
8
+ import hashlib
9
+ from importlib.metadata import version
10
+ from pathlib import Path
11
+ import platform
12
+ import subprocess
13
+ import sys
14
+
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+ from scripts.dataset.dataset import (
21
+ DatasetConfig, SPLITS, generate_dataset, make_record, preview_indices,
22
+ verify_dataset, write_dataset,
23
+ )
24
+ from scripts.dataset.symbols import MODEL_ID, MODEL_REVISION, validate_symbols
25
+
26
+
27
+ def show_summary(console: Console, manifest: dict) -> None:
28
+ table = Table(title="Validated pointer dataset")
29
+ for column in ("Split", "Examples", "Depths (count)", "Prompt tokens"):
30
+ table.add_column(column)
31
+ for split in SPLITS:
32
+ summary = manifest["splits"][split]
33
+ depths = ", ".join(f"{depth}: {count}" for depth, count in sorted(summary["depth_counts"].items(), key=lambda item: int(item[0])))
34
+ table.add_row(split, f"{summary['count']:,}", depths, f"{summary['prompt_tokens_min']}–{summary['prompt_tokens_max']}")
35
+ console.print(table)
36
+ console.print("[green]PASS[/green] Exact targets, tokenizer contexts, declared graph sharing, disjoint split boundaries, and seed replay")
37
+
38
+
39
+ def main() -> None:
40
+ parser = argparse.ArgumentParser(description=__doc__)
41
+ parser.add_argument("--graph-mode", choices=("unique_path", "mixture", "random_function", "permutation", "full_cycle"), default="unique_path")
42
+ parser.add_argument("--paired-horizons", action="store_true", help="Match each evaluation graph/start across depths; training graphs stay independent")
43
+ parser.add_argument("--seed", type=int, default=17)
44
+ parser.add_argument("--train-count", type=int, default=10_000)
45
+ parser.add_argument("--validation-count", type=int, default=1_000)
46
+ parser.add_argument("--test-count", type=int, default=1_000)
47
+ parser.add_argument("--depth-test-count", type=int, default=1_000)
48
+ parser.add_argument("--min-depth", type=int, default=1)
49
+ parser.add_argument("--max-train-depth", type=int, default=8)
50
+ parser.add_argument("--max-eval-depth", type=int, default=16)
51
+ parser.add_argument("--train-depths", type=int, nargs="+", help="Explicit training depths; validation/test retain every depth through max-train-depth")
52
+ parser.add_argument("--output", type=Path, help="New directory; default data/pointer/seed-<seed>")
53
+ mode = parser.add_mutually_exclusive_group()
54
+ mode.add_argument("--dry-run", nargs="?", const=5, type=int, choices=(5, 10), help="Preview up to 5 (default) or 10 examples spanning depths; write no dataset files")
55
+ mode.add_argument("--verify", type=Path, metavar="DIRECTORY", help="Read and independently verify an existing dataset")
56
+ args = parser.parse_args()
57
+ config = DatasetConfig(**{key: getattr(args, key) for key in asdict(DatasetConfig())})
58
+ try:
59
+ config.validate()
60
+ except ValueError as error:
61
+ parser.error(str(error))
62
+ output = args.output or Path("data/pointer") / f"seed-{args.seed}"
63
+ if args.verify is None and args.dry_run is None and output.exists():
64
+ parser.error(f"Output already exists: {output}; choose a new --output (never overwritten)")
65
+
66
+ from transformers import AutoTokenizer
67
+
68
+ console = Console()
69
+ console.print("[bold cyan]Stage 1 · pointer data[/bold cyan]")
70
+ console.print(f"Cached tokenizer only: {MODEL_ID} @ {MODEL_REVISION}")
71
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, revision=MODEL_REVISION, local_files_only=True)
72
+ if args.verify is not None:
73
+ with console.status("Verifying saved files and replaying seeds…"):
74
+ manifest = verify_dataset(args.verify, tokenizer)
75
+ show_summary(console, manifest)
76
+ console.print(f"Verified: {args.verify.resolve()}")
77
+ return
78
+
79
+ token_ids = validate_symbols(tokenizer)
80
+ console.print(f"[green]PASS[/green] 26 unique single-token symbols in rule, start, and answer contexts; seed {config.seed}")
81
+ console.print(f"Training depths: {list(config.depths('train'))}; validation/test: {config.min_depth}–{config.max_train_depth}; extrapolation depths: {config.max_train_depth + 1}–{config.max_eval_depth}")
82
+ if args.dry_run is not None:
83
+ distribution = Table(title="Configured dataset distribution (not preview frequencies)")
84
+ for column in ("Split", "Total", "Steps: example count"):
85
+ distribution.add_column(column)
86
+ for split in SPLITS:
87
+ depths = config.depths(split)
88
+ quotient, remainder = divmod(config.count(split), len(depths))
89
+ counts = ", ".join(f"{depth}: {quotient + (index < remainder)}" for index, depth in enumerate(depths))
90
+ distribution.add_row(split, f"{config.count(split):,}", counts)
91
+ console.print(distribution)
92
+ selected = preview_indices(config, args.dry_run)
93
+ console.print("[cyan]Preview spans shallow through deep tasks; it is not a random frequency sample.[/cyan]")
94
+ for split, index in selected:
95
+ record = make_record(config, split, index, tokenizer, token_ids)
96
+ console.print(Panel(Text(record["prompt"]), title=f"{record['example_id']} · depth {record['task_depth']}"))
97
+ table = Table(title=f"Start {record['initial_state']} · seed {record['seed']}")
98
+ for column in ("Step / loop", "Expected symbol", "Answer token ID"):
99
+ table.add_column(column)
100
+ for step, (symbol, token_id) in enumerate(zip(record["intermediate_states"], record["intermediate_token_ids"]), 1):
101
+ table.add_row(str(step), symbol, str(token_id))
102
+ console.print(table)
103
+ console.print(f"[bold green]Final: {record['final_state']}[/bold green] · answer position {record['answer_position']}")
104
+ console.print(f"[yellow]Dry run complete: {len(selected)} examples; no dataset files written.[/yellow]")
105
+ return
106
+
107
+ root = Path(__file__).resolve().parents[2]
108
+ sources = sorted((root / "scripts/dataset").glob("*.py"))
109
+ provenance = {
110
+ "command": [sys.executable, "-m", "scripts.dataset", *sys.argv[1:]],
111
+ "python": platform.python_version(),
112
+ "packages": {name: version(name) for name in ("transformers", "tokenizers", "huggingface-hub", "rich")},
113
+ "git_head": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip(),
114
+ "git_status": subprocess.check_output(["git", "status", "--short"], cwd=root, text=True),
115
+ "source_sha256": {str(path.relative_to(root)): hashlib.sha256(path.read_bytes()).hexdigest() for path in sources},
116
+ }
117
+ with console.status("Generating and checking every example…"):
118
+ dataset = generate_dataset(config, tokenizer, token_ids)
119
+ write_dataset(output, dataset, config, token_ids, provenance)
120
+ with console.status("Reading files back and independently replaying seeds…"):
121
+ manifest = verify_dataset(output, tokenizer)
122
+ show_summary(console, manifest)
123
+ console.print(f"Dataset and manifest: [bold]{output.resolve()}[/bold]")
124
+ console.print("Data milestone complete. No training performed.")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
scripts/dataset/dataset.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Seeded splits, JSONL persistence, and independent pointer-data validation."""
2
+
3
+ from collections import Counter
4
+ from dataclasses import asdict, dataclass
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from scripts.dataset.pointer import (PointerExample, example_seed, generate_example, validate_example,
11
+ generate_unconditioned_example, orbit_structure)
12
+ from scripts.dataset.symbols import MODEL_ID, MODEL_REVISION, validate_prompt_tokens, validate_symbols
13
+
14
+
15
+ SPLITS = ("train", "validation", "test", "depth_test")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DatasetConfig:
20
+ graph_mode: str = "unique_path"
21
+ paired_horizons: bool = False
22
+ seed: int = 17
23
+ train_count: int = 10_000
24
+ validation_count: int = 1_000
25
+ test_count: int = 1_000
26
+ depth_test_count: int = 1_000
27
+ min_depth: int = 1
28
+ max_train_depth: int = 8
29
+ max_eval_depth: int = 16
30
+ train_depths: list[int] | None = None
31
+
32
+ def validate(self) -> None:
33
+ if any(type(value) is not int for name, value in asdict(self).items() if name not in ("train_depths", "graph_mode", "paired_horizons")):
34
+ raise ValueError("Dataset configuration values must be integers")
35
+ if self.graph_mode not in ("unique_path", "mixture", "random_function", "permutation", "full_cycle"):
36
+ raise ValueError("Unknown graph_mode")
37
+ if type(self.paired_horizons) is not bool or (self.paired_horizons and self.graph_mode == "unique_path"):
38
+ raise ValueError("paired_horizons requires depth-independent graph sampling")
39
+ if self.seed < 0 or min(self.count(split) for split in SPLITS) < 1:
40
+ raise ValueError("seed must be nonnegative and all split counts must be positive")
41
+ if not 1 <= self.min_depth <= self.max_train_depth < self.max_eval_depth <= (25 if self.graph_mode == "unique_path" else 256):
42
+ raise ValueError("Require ordered positive train/eval depths within the graph-mode limit")
43
+
44
+ if self.train_depths is not None:
45
+ if (not self.train_depths or any(type(d) is not int for d in self.train_depths)
46
+ or sorted(set(self.train_depths)) != self.train_depths
47
+ or min(self.train_depths) < self.min_depth or max(self.train_depths) != self.max_train_depth):
48
+ raise ValueError("train_depths must be sorted unique integers within bounds, ending at max_train_depth")
49
+
50
+ def paired(self, split: str) -> bool:
51
+ """Match evaluation horizons; retain independent graphs in training."""
52
+ return self.paired_horizons and split != "train"
53
+
54
+ def count(self, split: str) -> int:
55
+ if split not in SPLITS:
56
+ raise ValueError(f"Unknown split: {split}")
57
+ return getattr(self, f"{split}_count")
58
+
59
+ def depths(self, split: str) -> range | list[int]:
60
+ if split not in SPLITS:
61
+ raise ValueError(f"Unknown split: {split}")
62
+ if split == "train" and self.train_depths is not None:
63
+ return self.train_depths
64
+ if split == "depth_test":
65
+ return range(self.max_train_depth + 1, self.max_eval_depth + 1)
66
+ return range(self.min_depth, self.max_train_depth + 1)
67
+
68
+
69
+ def preview_indices(config: DatasetConfig, count: int) -> list[tuple[str, int]]:
70
+ """Select actual records spanning available depths, without sampling programs.
71
+
72
+ This is a depth showcase, not a random sample of split frequencies. Keep only
73
+ enough candidate indices to fill a preview, even for very large split counts.
74
+ """
75
+ config.validate()
76
+ if count not in (5, 10):
77
+ raise ValueError("Preview count must be 5 or 10")
78
+ candidates = [
79
+ (split, index, config.depths(split)[index % len(config.depths(split))])
80
+ for split in SPLITS
81
+ for index in range(min(config.count(split), count * len(config.depths(split))))
82
+ ]
83
+ depths = sorted({depth for _, _, depth in candidates})
84
+ count = min(count, len(candidates))
85
+ selected = []
86
+ for position in range(count):
87
+ target = depths[position * (len(depths) - 1) // (count - 1)]
88
+ candidate = min(candidates, key=lambda item: (
89
+ abs(item[2] - target), (SPLITS.index(item[0]) - position) % len(SPLITS), item[1],
90
+ ))
91
+ candidates.remove(candidate)
92
+ selected.append(candidate[:2])
93
+ return selected
94
+
95
+
96
+ def make_record(config: DatasetConfig, split: str, index: int, tokenizer: Any, token_ids: dict[str, int]) -> dict:
97
+ """Index determines balanced depth and an independently derived random seed."""
98
+ depths = config.depths(split)
99
+ depth = depths[index % len(depths)]
100
+ graph_index = index // len(depths) if config.paired(split) else index
101
+ seed = example_seed(config.seed, split, graph_index)
102
+ example = (generate_example(seed, depth, split, index) if config.graph_mode == "unique_path" else
103
+ generate_unconditioned_example(seed, depth, split, index, config.graph_mode))
104
+ validate_example(example)
105
+ position = validate_prompt_tokens(tokenizer, example.prompt, token_ids)
106
+ return {
107
+ **example.to_dict(),
108
+ **({"graph_sampling": config.graph_mode, **orbit_structure(dict(example.mapping), example.initial_state)}
109
+ if config.graph_mode != "unique_path" else {}),
110
+ "answer_position": position,
111
+ "prompt_token_count": position + 1,
112
+ "intermediate_token_ids": [token_ids[symbol] for symbol in example.intermediate_states],
113
+ "final_token_id": token_ids[example.final_state],
114
+ }
115
+
116
+
117
+ def generate_dataset(config: DatasetConfig, tokenizer: Any, token_ids: dict[str, int]) -> dict[str, list[dict]]:
118
+ config.validate()
119
+ dataset = {}
120
+ seen = {}
121
+ for split in SPLITS:
122
+ records = []
123
+ for index in range(config.count(split)):
124
+ record = make_record(config, split, index, tokenizer, token_ids)
125
+ fingerprint = record["mapping_sha256"]
126
+ if fingerprint in seen and (not config.paired(split) or seen[fingerprint] != split):
127
+ raise ValueError("Repeated rule table within/across splits; select a different master seed")
128
+ seen[fingerprint] = split
129
+ records.append(record)
130
+ dataset[split] = records
131
+ return dataset
132
+
133
+
134
+ def encode_records(records: list[dict]) -> bytes:
135
+ return "".join(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n" for record in records).encode()
136
+
137
+
138
+ def summarize(records: list[dict]) -> dict:
139
+ return {
140
+ "count": len(records),
141
+ "depth_counts": dict(sorted(Counter(str(record["task_depth"]) for record in records).items())),
142
+ "prompt_tokens_min": min(record["prompt_token_count"] for record in records),
143
+ "prompt_tokens_max": max(record["prompt_token_count"] for record in records),
144
+ "sha256": hashlib.sha256(encode_records(records)).hexdigest(),
145
+ }
146
+
147
+
148
+ def write_dataset(
149
+ output: Path, dataset: dict[str, list[dict]], config: DatasetConfig,
150
+ token_ids: dict[str, int], provenance: dict,
151
+ ) -> dict:
152
+ """Write a new directory only; the manifest is the final completion marker."""
153
+ manifest = {
154
+ "schema_version": 1,
155
+ "generator": "pointer-v1" if config.graph_mode == "unique_path" else "pointer-v2",
156
+ "config": asdict(config),
157
+ "tokenizer": {
158
+ "model_id": MODEL_ID, "revision": MODEL_REVISION,
159
+ "add_special_tokens": False, "chat_template": False,
160
+ "answer_text_prefix": " ", "symbol_token_ids": token_ids,
161
+ },
162
+ "semantics": ("explicit_steps_unique_nominal_path_no_post_completion_targets" if config.graph_mode == "unique_path"
163
+ else "depth_independent_graphs_exact_steps_cycles_allowed"),
164
+ "splits": {split: summarize(dataset[split]) for split in SPLITS},
165
+ "validation": {"unique_mapping_tables": not config.paired_horizons, "exact_targets": True, "prompt_symbol_tokens": True},
166
+ "provenance": provenance,
167
+ }
168
+ output.mkdir(parents=True, exist_ok=False)
169
+ for split in SPLITS:
170
+ (output / f"{split}.jsonl").write_bytes(encode_records(dataset[split]))
171
+ (output / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
172
+ return manifest
173
+
174
+
175
+ def verify_dataset(output: Path, tokenizer: Any) -> dict:
176
+ """Check bytes, labels, split boundaries, tokenizer contexts, and seed replay.
177
+
178
+ Regeneration alone is insufficient: validate_example separately reparses the
179
+ rendered rules and executes them to check the actual stored trajectory.
180
+ """
181
+ manifest = json.loads((output / "manifest.json").read_text())
182
+ if manifest["schema_version"] != 1 or manifest["generator"] not in ("pointer-v1", "pointer-v2"):
183
+ raise ValueError("Unsupported dataset version")
184
+ config = DatasetConfig(**manifest["config"])
185
+ config.validate()
186
+ expected_generator = "pointer-v1" if config.graph_mode == "unique_path" else "pointer-v2"
187
+ if manifest["generator"] != expected_generator:
188
+ raise ValueError("Generator version and graph sampling mode disagree")
189
+ token_ids = validate_symbols(tokenizer)
190
+ if manifest["tokenizer"] != {
191
+ "model_id": MODEL_ID, "revision": MODEL_REVISION,
192
+ "add_special_tokens": False, "chat_template": False,
193
+ "answer_text_prefix": " ", "symbol_token_ids": token_ids,
194
+ }:
195
+ raise ValueError("Tokenizer metadata mismatch")
196
+ seen = {}
197
+ for split in SPLITS:
198
+ payload = (output / f"{split}.jsonl").read_bytes()
199
+ if hashlib.sha256(payload).hexdigest() != manifest["splits"][split]["sha256"]:
200
+ raise ValueError(f"{split}: file checksum mismatch")
201
+ records = [json.loads(line) for line in payload.splitlines()]
202
+ if len(records) != config.count(split) or summarize(records) != manifest["splits"][split]:
203
+ raise ValueError(f"{split}: split count/summary mismatch")
204
+ for index, record in enumerate(records):
205
+ example = PointerExample(**{key: record[key] for key in PointerExample.__dataclass_fields__})
206
+ validate_example(example)
207
+ if record != make_record(config, split, index, tokenizer, token_ids):
208
+ raise ValueError(f"{split} record {index}: seed replay/token metadata mismatch")
209
+ if example.mapping_sha256 in seen and (not config.paired(split) or seen[example.mapping_sha256] != split):
210
+ raise ValueError("Repeated rule table within/across splits")
211
+ seen[example.mapping_sha256] = split
212
+ return manifest
scripts/dataset/pointer.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit-step pointer programs and exact, one-based execution targets."""
2
+
3
+ from dataclasses import asdict, dataclass
4
+ import hashlib
5
+ import json
6
+ import random
7
+ import re
8
+ from typing import Sequence
9
+
10
+
11
+ SYMBOLS = tuple("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
12
+ PAIR = re.compile(r"\(\s*([A-Z])\s*,\s*([A-Z])\s*\)")
13
+
14
+
15
+ def parse_mapping(text: str) -> dict[str, str]:
16
+ """Parse pairs such as '(A,C) (C,D) (D, E)', rejecting ambiguous rules."""
17
+ matches = list(PAIR.finditer(text))
18
+ if not matches or PAIR.sub("", text).strip():
19
+ raise ValueError("Mapping must be whitespace-separated (A,C) pairs")
20
+ mapping = {}
21
+ for match in matches:
22
+ source, target = match.groups()
23
+ if source in mapping:
24
+ raise ValueError(f"Duplicate source symbol: {source}")
25
+ mapping[source] = target
26
+ return mapping
27
+
28
+
29
+ def execute(mapping: dict[str, str], start: str, steps: int) -> list[str]:
30
+ """Return states after transitions 1..steps, excluding the initial state.
31
+
32
+ This reference interpreter permits cycles and partial tables, provided every
33
+ requested transition exists. The dataset sampler applies stricter constraints.
34
+ """
35
+ if type(steps) is not int or steps < 0:
36
+ raise ValueError("steps must be a nonnegative integer")
37
+ states = []
38
+ current = start
39
+ for step in range(1, steps + 1):
40
+ if current not in mapping:
41
+ raise ValueError(f"No rule for {current!r} at step {step}")
42
+ current = mapping[current]
43
+ states.append(current)
44
+ return states
45
+
46
+
47
+ def check_predictions(
48
+ mapping: dict[str, str], start: str, predictions: Sequence[str], steps: int,
49
+ ) -> list[bool]:
50
+ """Score decoded symbols at steps 1..N against the exact nominal trajectory.
51
+
52
+ Predictions may be a prefix (too few model loops), but cannot extend beyond
53
+ the requested depth. Invalid/multi-symbol answers are incorrect; no lenient
54
+ extraction of a letter from prose is performed. Leading whitespace is allowed.
55
+ """
56
+ targets = execute(mapping, start, steps)
57
+ if isinstance(predictions, str) or len(predictions) > steps:
58
+ raise ValueError("Provide a sequence of at most 'steps' decoded predictions")
59
+ return [prediction.strip() == target for prediction, target in zip(predictions, targets)]
60
+
61
+
62
+ def render_prompt(pairs: list[list[str]], start: str, steps: int) -> str:
63
+ # Spaces before symbols prevent Qwen punctuation/letter token merges.
64
+ rules = " ".join(f"( {source}, {target})" for source, target in pairs)
65
+ return f"Rules: {rules}\nStart: {start}\nSteps: {steps}\nAnswer:"
66
+
67
+
68
+ def mapping_fingerprint(mapping: dict[str, str]) -> str:
69
+ """Ignore rule order, start, and depth when detecting reused rule tables."""
70
+ payload = json.dumps(sorted(mapping.items()), separators=(",", ":"))
71
+ return hashlib.sha256(payload.encode()).hexdigest()
72
+
73
+
74
+ def example_seed(master_seed: int, split: str, index: int, attempt: int = 0) -> int:
75
+ payload = f"pointer-v1:{master_seed}:{split}:{index}:{attempt}"
76
+ return int.from_bytes(hashlib.sha256(payload.encode()).digest()[:8], "big")
77
+
78
+
79
+ @dataclass
80
+ class PointerExample:
81
+ schema_version: int
82
+ example_id: str
83
+ family: str
84
+ split: str
85
+ seed: int
86
+ task_depth: int
87
+ mapping: list[list[str]]
88
+ mapping_sha256: str
89
+ initial_state: str
90
+ intermediate_states: list[str]
91
+ final_state: str
92
+ prompt: str
93
+
94
+ def to_dict(self) -> dict:
95
+ return asdict(self)
96
+
97
+
98
+ def generate_example(seed: int, depth: int, split: str, index: int) -> PointerExample:
99
+ """Sample a full table, conditioned on a non-repeating nominal path.
100
+
101
+ Every example uses all 26 symbols, keeping rule count independent of depth.
102
+ The d+1 distinct path states prevent an early/repeated final answer; remaining
103
+ edges are independent random choices and can cycle outside the nominal path.
104
+ Rule order is shuffled so the solution is not printed in execution order.
105
+ """
106
+ if type(depth) is not int or not 1 <= depth < len(SYMBOLS):
107
+ raise ValueError("depth must be an integer between 1 and 25")
108
+ if type(seed) is not int or seed < 0:
109
+ raise ValueError("seed must be a nonnegative integer")
110
+ rng = random.Random(seed)
111
+ path = rng.sample(SYMBOLS, depth + 1)
112
+ mapping = dict(zip(path[:-1], path[1:]))
113
+ for symbol in SYMBOLS:
114
+ if symbol not in mapping:
115
+ mapping[symbol] = rng.choice(SYMBOLS)
116
+ pairs = [[source, mapping[source]] for source in SYMBOLS]
117
+ rng.shuffle(pairs)
118
+ states = execute(mapping, path[0], depth)
119
+ return PointerExample(
120
+ schema_version=1, example_id=f"{split}-{index:06d}-{seed:016x}",
121
+ family="pointer", split=split, seed=seed, task_depth=depth,
122
+ mapping=pairs, mapping_sha256=mapping_fingerprint(mapping),
123
+ initial_state=path[0], intermediate_states=states, final_state=states[-1],
124
+ prompt=render_prompt(pairs, path[0], depth),
125
+ )
126
+
127
+
128
+ def validate_example(example: PointerExample) -> None:
129
+ """Reparse the actual prompt and independently recompute every saved target."""
130
+ if example.schema_version not in (1, 2) or example.family != "pointer":
131
+ raise ValueError("Unsupported pointer record schema/family")
132
+ if type(example.task_depth) is not int or not 1 <= example.task_depth <= (25 if example.schema_version == 1 else 256):
133
+ raise ValueError("Invalid task depth")
134
+ if any(len(pair) != 2 for pair in example.mapping):
135
+ raise ValueError("Every mapping entry must have two symbols")
136
+ mapping = dict(example.mapping)
137
+ if len(example.mapping) != len(SYMBOLS) or set(mapping) != set(SYMBOLS):
138
+ raise ValueError("Each vocabulary symbol must have exactly one rule")
139
+ if not set(mapping.values()) <= set(SYMBOLS):
140
+ raise ValueError("Mapping target outside vocabulary")
141
+ if example.mapping_sha256 != mapping_fingerprint(mapping):
142
+ raise ValueError("Mapping fingerprint mismatch")
143
+ if example.prompt != render_prompt(example.mapping, example.initial_state, example.task_depth):
144
+ raise ValueError("Prompt disagrees with structured task")
145
+ parsed = parse_mapping(example.prompt.splitlines()[0].removeprefix("Rules: "))
146
+ states = execute(parsed, example.initial_state, example.task_depth)
147
+ if states != example.intermediate_states or example.final_state != states[-1]:
148
+ raise ValueError("Intermediate/final labels disagree with reference execution")
149
+ if example.schema_version == 1 and len(set([example.initial_state, *states])) != example.task_depth + 1:
150
+ raise ValueError("Nominal path must not repeat a state")
151
+
152
+
153
+ def generate_unconditioned_example(seed: int, depth: int, split: str, index: int,
154
+ graph_mode: str = "mixture") -> PointerExample:
155
+ """Sample the graph/start/order before using depth; permit cyclic execution.
156
+
157
+ Equal-probability mixture: random function, random permutation, full cycle.
158
+ All three have 26 rules; graph selection never consults requested horizon.
159
+ """
160
+ if type(depth) is not int or not 1 <= depth <= 256 or type(seed) is not int or seed < 0:
161
+ raise ValueError("Need nonnegative seed and depth in 1..256")
162
+ rng = random.Random(seed)
163
+ kinds = ("random_function", "permutation", "full_cycle")
164
+ kind = rng.choice(kinds) if graph_mode == "mixture" else graph_mode
165
+ if kind not in kinds:
166
+ raise ValueError("Unknown depth-independent graph mode")
167
+ if kind == "random_function":
168
+ mapping = {s: rng.choice(SYMBOLS) for s in SYMBOLS}
169
+ else:
170
+ order = rng.sample(SYMBOLS, len(SYMBOLS))
171
+ mapping = (dict(zip(SYMBOLS, order)) if kind == "permutation" else
172
+ {s: order[(i + 1) % len(order)] for i, s in enumerate(order)})
173
+ start = rng.choice(SYMBOLS)
174
+ pairs = [[s, mapping[s]] for s in SYMBOLS]
175
+ rng.shuffle(pairs)
176
+ states = execute(mapping, start, depth)
177
+ return PointerExample(2, f"{split}-{index:06d}-{seed:016x}", "pointer", split, seed,
178
+ depth, pairs, mapping_fingerprint(mapping), start, states, states[-1],
179
+ render_prompt(pairs, start, depth))
180
+
181
+
182
+ def orbit_structure(mapping: dict[str, str], start: str) -> dict[str, int]:
183
+ """Length before the first cycle and cycle period, independent of horizon."""
184
+ seen = {}
185
+ state = start
186
+ while state not in seen:
187
+ seen[state] = len(seen)
188
+ state = mapping[state]
189
+ return {"transient_length": seen[state], "cycle_period": len(seen) - seen[state]}
scripts/dataset/symbols.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate the actual tokenizer in the exact pointer prompt/answer contexts."""
2
+
3
+ from typing import Any
4
+ import re
5
+
6
+ from scripts.dataset.pointer import SYMBOLS
7
+
8
+
9
+ MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
10
+ MODEL_REVISION = "7ae557604adf67be50417f59c2c2f167def9a775"
11
+
12
+
13
+ def validate_prompt_tokens(tokenizer: Any, prompt: str, token_ids: dict[str, int]) -> int:
14
+ """Check every space-prefixed symbol span; return the answer readout index.
15
+
16
+ Prompts are raw text, with no chat template or added special tokens. The
17
+ answer position is the final ':' token's index (a next-token readout).
18
+ """
19
+ if not prompt.endswith("\nAnswer:"):
20
+ raise ValueError("Prompt must end with '\\nAnswer:' without a trailing space")
21
+ encoded = tokenizer(prompt, add_special_tokens=False, return_offsets_mapping=True)
22
+ spans = dict(zip(map(tuple, encoded["offset_mapping"]), encoded["input_ids"]))
23
+ for match in re.finditer(r" [A-Z](?=[,)\n])", prompt):
24
+ if spans.get(match.span()) != token_ids[match.group()[1:]]:
25
+ raise ValueError(f"Symbol does not retain its token at {match.span()}: {match.group()!r}")
26
+ return len(encoded["input_ids"]) - 1
27
+
28
+
29
+ def validate_symbols(tokenizer: Any) -> dict[str, int]:
30
+ """Require one unique token per ' '+symbol in rules, starts, and answers."""
31
+ token_ids = {}
32
+ for symbol in SYMBOLS:
33
+ ids = tokenizer.encode(" " + symbol, add_special_tokens=False)
34
+ if len(ids) != 1 or tokenizer.decode(ids) != " " + symbol:
35
+ raise ValueError(f"Symbol {symbol!r} is not one reversible space-prefixed token")
36
+ token_ids[symbol] = ids[0]
37
+ if len(set(token_ids.values())) != len(SYMBOLS):
38
+ raise ValueError("Answer tokens must be unique")
39
+ # Exhaustive pair contexts, and all 26 possible answer continuations.
40
+ for source in SYMBOLS:
41
+ prompt = "Rules: " + " ".join(f"( {source}, {target})" for target in SYMBOLS)
42
+ prompt += f"\nStart: {source}\nSteps: 1\nAnswer:"
43
+ validate_prompt_tokens(tokenizer, prompt, token_ids)
44
+ prefix = tokenizer.encode(prompt, add_special_tokens=False)
45
+ for target in SYMBOLS:
46
+ if tokenizer.encode(prompt + " " + target, add_special_tokens=False) != [*prefix, token_ids[target]]:
47
+ raise ValueError(f"Answer boundary merges for {target!r}")
48
+ return token_ids
scripts/dataset/terminal.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic, evaluation-only absorbing-terminal variants of pointer tasks."""
2
+
3
+ from dataclasses import replace
4
+
5
+ from .pointer import PointerExample, execute, mapping_fingerprint, render_prompt, validate_example
6
+
7
+
8
+ TRANSFORM_VERSION = "absorbing-terminal-v1"
9
+
10
+
11
+ def absorbing_terminal(example: PointerExample) -> PointerExample:
12
+ """Replace only the final state's outgoing edge, preserving nominal execution.
13
+
14
+ The original d+1 path states are distinct, so that edge is not followed until
15
+ loop d+1. The changed prompt is a new distribution, not a retention label
16
+ retroactively applied to the original task. Source IDs/seeds support pairing.
17
+ """
18
+ validate_example(example)
19
+ if example.final_state in [example.initial_state, *example.intermediate_states[:-1]]:
20
+ raise ValueError("Terminal transform requires a final state not visited earlier; cyclic tasks need exact-step evaluation")
21
+ final = example.final_state
22
+ pairs = [[source, final if source == final else target] for source, target in example.mapping]
23
+ result = replace(
24
+ example, mapping=pairs, mapping_sha256=mapping_fingerprint(dict(pairs)),
25
+ prompt=render_prompt(pairs, example.initial_state, example.task_depth),
26
+ )
27
+ validate_example(result)
28
+ states = execute(dict(pairs), result.initial_state, result.task_depth + 2)
29
+ if states != [*example.intermediate_states, final, final]:
30
+ raise ValueError("Terminal transform changed the nominal trajectory")
31
+ return result
scripts/demo/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Public pointer demo: validated inputs and observations of native inference."""
scripts/demo/__main__.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Download/run the final checkpoint, printing one JSON snapshot per readout."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
9
+ import torch
10
+ from huggingface_hub import snapshot_download
11
+
12
+ from scripts.demo.inference import DemoRequest, stream_inference
13
+ from scripts.recurrent_qwen.checkpoint import load_recurrent_checkpoint
14
+
15
+
16
+ def main() -> None:
17
+ parser = argparse.ArgumentParser(description=__doc__)
18
+ source = parser.add_mutually_exclusive_group(required=True)
19
+ source.add_argument("--checkpoint", type=Path)
20
+ source.add_argument("--model", help="Hugging Face model repository ID")
21
+ parser.add_argument("--revision", default="main")
22
+ parser.add_argument("--request", required=True, type=Path, help="JSON exported from the demo")
23
+ parser.add_argument("--device", choices=("cpu", "cuda", "mps"), default="cpu")
24
+ parser.add_argument("--download", action="store_true", help="Allow explicit Hub/base downloads")
25
+ parser.add_argument("--max-steps", type=int, default=256)
26
+ parser.add_argument("--loop-cap", type=int, default=272)
27
+ args = parser.parse_args()
28
+ request = DemoRequest.parse(json.loads(args.request.read_text()), max_steps=args.max_steps)
29
+ if args.loop_cap < request.steps:
30
+ parser.error("--loop-cap must cover the requested steps")
31
+ path = args.checkpoint or Path(snapshot_download(args.model, revision=args.revision,
32
+ local_files_only=not args.download))
33
+ torch.set_num_threads(4)
34
+ torch.use_deterministic_algorithms(True)
35
+ torch.backends.cuda.matmul.allow_tf32 = False
36
+ torch.backends.cudnn.allow_tf32 = False
37
+ model, tokenizer, spec = load_recurrent_checkpoint(path, device=args.device, download=args.download)
38
+ model.config._attn_implementation = "sdpa"
39
+ model.requires_grad_(False)
40
+ for frame in stream_inference(model, tokenizer, spec, request,
41
+ checkpoint_id=args.model or str(path), loop_cap=args.loop_cap):
42
+ print(json.dumps(frame, allow_nan=False), flush=True)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
scripts/demo/gradio_app.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The same small queued streaming API for Hugging Face and local serving."""
2
+
3
+ from typing import Callable
4
+
5
+ import gradio as gr
6
+
7
+
8
+ def create_app(predict: Callable, *, model_url: str | None = None) -> gr.Blocks:
9
+ with gr.Blocks(title="LoopFormer · live inference", analytics_enabled=False) as app:
10
+ gr.Markdown("# LoopFormer\nFollow a graph one recurrent loop at a time. "
11
+ "This endpoint streams real model predictions; it does not substitute reference answers.")
12
+ links = "[Open the interactive playground](https://mukundshankar-dev.github.io/projects/loopformer/demo/)"
13
+ if model_url:
14
+ links += f" · [Download the model]({model_url})"
15
+ gr.Markdown(links)
16
+ request_input = gr.JSON(label="Graph request: mapping, start, steps")
17
+ result = gr.JSON(label="Streamed trace")
18
+ run = gr.Button("Run model", variant="primary")
19
+ run.click(predict, inputs=request_input, outputs=result, api_name="predict",
20
+ concurrency_limit=1, stream_every=0.05)
21
+ return app.queue(max_size=16, default_concurrency_limit=1)
scripts/demo/inference.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stream native loop observations without reference-state feedback."""
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ from queue import Queue
6
+ from threading import Event, Thread
7
+ from time import perf_counter
8
+ from typing import Any, Iterator
9
+
10
+ import torch
11
+
12
+ from scripts.dataset.pointer import SYMBOLS, execute, render_prompt
13
+
14
+
15
+ SAFETY_CAP = 272
16
+ STOP_THRESHOLD = 0.5
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DemoRequest:
21
+ """Ordered full A–Z table; order is part of the model's actual prompt."""
22
+
23
+ pairs: tuple[tuple[str, str], ...]
24
+ start: str
25
+ steps: int
26
+
27
+ @classmethod
28
+ def parse(cls, value: Any, *, max_steps: int = 256) -> "DemoRequest":
29
+ if not isinstance(value, dict) or set(value) != {"mapping", "start", "steps"}:
30
+ raise ValueError("Supply exactly mapping, start, and steps")
31
+ pairs = value["mapping"]
32
+ if not isinstance(pairs, list) or len(pairs) != 26:
33
+ raise ValueError("Supply all 26 A–Z rules, with one destination per node")
34
+ if any(not isinstance(p, list) or len(p) != 2 or
35
+ any(not isinstance(s, str) or s not in SYMBOLS for s in p) for p in pairs):
36
+ raise ValueError("Each rule must be a pair of uppercase A–Z letters")
37
+ if {p[0] for p in pairs} != set(SYMBOLS):
38
+ raise ValueError("Each A–Z source must occur exactly once")
39
+ if not isinstance(value["start"], str) or value["start"] not in SYMBOLS:
40
+ raise ValueError("Start must be an uppercase A–Z letter")
41
+ if type(value["steps"]) is not int or not 1 <= value["steps"] <= max_steps:
42
+ raise ValueError(f"Steps must be an integer from 1 to {max_steps}")
43
+ return cls(tuple(tuple(p) for p in pairs), value["start"], value["steps"])
44
+
45
+ def payload(self) -> dict:
46
+ return {"mapping": [list(p) for p in self.pairs], "start": self.start, "steps": self.steps}
47
+
48
+ def prompt(self) -> str:
49
+ return render_prompt([list(p) for p in self.pairs], self.start, self.steps)
50
+
51
+
52
+ class InferenceCancelled(Exception):
53
+ """The client closed its generator; stop at the next completed readout."""
54
+
55
+
56
+ def score_trace(request: DemoRequest, rows: list[dict], stop_reason: str) -> dict:
57
+ """Final letter, strict requested prefix and timing are separate metrics."""
58
+ expected = execute(dict(request.pairs), request.start, request.steps)
59
+ predictions = [row["prediction"] for row in rows]
60
+ exact_stop = stop_reason == "head" and len(rows) == request.steps
61
+ correct = bool(predictions) and predictions[-1] == expected[-1]
62
+ complete = len(predictions) >= request.steps and predictions[:request.steps] == expected
63
+ return {"answer_correct": correct, "complete_trajectory": complete,
64
+ "exact_stop": exact_stop, "joint_success": correct and exact_stop,
65
+ "stop_reason": stop_reason, "executed_loops": len(rows),
66
+ "first_error": next((r["loop"] for r in rows[:request.steps] if not r["correct"]), None)}
67
+
68
+
69
+ def stream_inference(model: Any, tokenizer: Any, spec: dict, request: DemoRequest,
70
+ *, checkpoint_id: str, cancel: Event | None = None,
71
+ loop_cap: int | None = None) -> Iterator[dict]:
72
+ """Yield cumulative JSON snapshots as loops arrive, then a final summary.
73
+
74
+ One worker owns one native forward. The bounded 272-loop cap also bounds
75
+ the queue. A consumer must serialize calls to its model (Gradio concurrency
76
+ one). Closing the iterator cancels and joins the worker before releasing GPU
77
+ ownership. Targets are computed only in the consumer, never passed to R.
78
+ """
79
+ if model.training or model.completion_head is None:
80
+ raise ValueError("Demo requires an eval-mode model with learned completion")
81
+ loop_cap = SAFETY_CAP if loop_cap is None else loop_cap
82
+ if type(loop_cap) is not int or loop_cap < 1:
83
+ raise ValueError("Loop cap must be a positive integer")
84
+ cancel = cancel if cancel is not None else Event()
85
+ queue: Queue = Queue()
86
+ rows: list[dict] = []
87
+ mapping = dict(request.pairs)
88
+ reference = request.start
89
+ previous_prediction = request.start
90
+ started = perf_counter()
91
+ metadata = {"schema_version": 1, "source": "live", "checkpoint": checkpoint_id,
92
+ "request": request.payload(), "threshold": STOP_THRESHOLD, "safety_cap": loop_cap}
93
+
94
+ def observe(loop, scores, stop_logits, state_logits):
95
+ if cancel.is_set():
96
+ raise InferenceCancelled()
97
+ scores = scores[0].float().cpu()
98
+ probability = float(stop_logits.sigmoid().item())
99
+ if not torch.isfinite(scores).all() or not math.isfinite(probability):
100
+ raise FloatingPointError("Nonfinite model output")
101
+ top = scores.topk(3)
102
+ probabilities = scores.softmax(-1)
103
+ queue.put(("loop", {"loop": loop, "prediction": SYMBOLS[int(scores.argmax())],
104
+ "direct_prediction": SYMBOLS[int(state_logits[0].argmax())] if state_logits is not None else None,
105
+ "stop_probability": probability,
106
+ "top3": [{"symbol": SYMBOLS[i], "probability": float(probabilities[i])}
107
+ for i in top.indices.tolist()],
108
+ "arrival_seconds": perf_counter() - started}))
109
+
110
+ def run():
111
+ try:
112
+ if cancel.is_set():
113
+ raise InferenceCancelled()
114
+ device = next(model.parameters()).device
115
+ encoded = tokenizer(request.prompt(), add_special_tokens=False, return_tensors="pt")
116
+ with torch.inference_mode():
117
+ result = model(input_ids=encoded["input_ids"].to(device),
118
+ attention_mask=encoded["attention_mask"].to(device),
119
+ num_loops=loop_cap, completion_threshold=STOP_THRESHOLD,
120
+ readout_token_ids=spec["token_ids"], loop_callback=observe)
121
+ signaled = result.stop_logits[0, -1].sigmoid().item() >= STOP_THRESHOLD
122
+ queue.put(("done", "head" if signaled else "cap"))
123
+ except InferenceCancelled:
124
+ queue.put(("cancelled", None))
125
+ except Exception as error:
126
+ # Transport the original failure to the owning generator, never hide it.
127
+ queue.put(("error", error))
128
+
129
+ worker = Thread(target=run, name="loopformer-inference", daemon=True)
130
+ worker.start()
131
+ try:
132
+ while True:
133
+ kind, value = queue.get()
134
+ if kind == "error":
135
+ raise value
136
+ if kind == "cancelled":
137
+ return
138
+ if kind == "done":
139
+ yield {**metadata, "status": "complete", "trace": list(rows),
140
+ "summary": score_trace(request, rows, value),
141
+ "inference_seconds": perf_counter() - started}
142
+ return
143
+ reference = mapping[reference]
144
+ value.update(expected=reference, correct=value["prediction"] == reference,
145
+ edge_valid=mapping[previous_prediction] == value["prediction"])
146
+ previous_prediction = value["prediction"]
147
+ rows.append(value)
148
+ yield {**metadata, "status": "streaming", "trace": list(rows)}
149
+ finally:
150
+ cancel.set()
151
+ worker.join()
scripts/recurrent_qwen/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Shared-depth Qwen model mechanics, without import-time loading."""
2
+
3
+ from .model import RecurrentQwen
4
+ from .outputs import RecurrentOutput
5
+
6
+ __all__ = ["RecurrentQwen", "RecurrentOutput"]
scripts/recurrent_qwen/checkpoint.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Portable trainable recurrent tensors plus explicit architecture/base identity."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import torch
8
+ from transformers import AutoTokenizer, Qwen2ForCausalLM
9
+
10
+ from .lora_utils import attach_recurrent_lora
11
+ from .model import RecurrentQwen
12
+ from scripts.dataset.pointer import SYMBOLS
13
+
14
+
15
+ EXECUTOR_FORMAT = "loopformer-executor-v2"
16
+ FORMAT = "loopformer-stage1-v1"
17
+ COMPLETION_FORMAT = "loopformer-stage1-completion-v1"
18
+ FIXED_PROMPT_FORMAT = "loopformer-stage1-fixed-prompt-v1"
19
+
20
+
21
+ def checkpoint_mode(spec: dict) -> str:
22
+ """Legacy checkpoints mean full-sequence recurrence; never infer new modes."""
23
+ mode = spec.get("recurrence_mode", "full_sequence")
24
+ if spec.get("format") not in (FORMAT, COMPLETION_FORMAT, FIXED_PROMPT_FORMAT, EXECUTOR_FORMAT):
25
+ raise ValueError("Unsupported recurrent checkpoint format")
26
+ if (spec["format"] == EXECUTOR_FORMAT) != ("executor" in spec):
27
+ raise ValueError("Executor architecture requires the versioned executor checkpoint format")
28
+ expected = "fixed_prompt" if spec["format"] in (FIXED_PROMPT_FORMAT, EXECUTOR_FORMAT) else "full_sequence"
29
+ if mode != expected:
30
+ raise ValueError("Checkpoint format and recurrence_mode disagree")
31
+ return mode
32
+
33
+
34
+ def cpu_tree(value: Any) -> Any:
35
+ if isinstance(value, torch.Tensor):
36
+ return value.detach().cpu().clone()
37
+ if isinstance(value, dict):
38
+ return {key: cpu_tree(item) for key, item in value.items()}
39
+ if isinstance(value, list):
40
+ return [cpu_tree(item) for item in value]
41
+ if isinstance(value, tuple):
42
+ return tuple(cpu_tree(item) for item in value)
43
+ return value
44
+
45
+
46
+ def save_checkpoint(path: Path, model: RecurrentQwen, tokenizer: Any, spec: dict, training_state: dict) -> None:
47
+ """Write a new checkpoint directory; metadata is the final completion marker."""
48
+ if spec.get("recurrence_mode", "full_sequence") != model.recurrence_mode:
49
+ raise ValueError("Checkpoint spec and recurrence_mode disagree")
50
+ weights = {name: parameter for name, parameter in model.named_parameters() if parameter.requires_grad}
51
+ completion = model.completion_head is not None
52
+ expected_completion = spec.get("completion_head")
53
+ if completion != (expected_completion is not None):
54
+ raise ValueError("Checkpoint spec and completion head disagree")
55
+ if completion and expected_completion != {"intermediate": model.completion_head.intermediate}:
56
+ raise ValueError("Checkpoint completion-head architecture differs from spec")
57
+ new_architecture = "executor" in spec
58
+ if not new_architecture and (not weights or any(not ((name.startswith("recurrent.") and ".lora_" in name) or
59
+ (completion and name.startswith("completion_head."))) for name in weights)):
60
+ raise ValueError("Checkpoint expects recurrent LoRA and optional completion head only")
61
+ if new_architecture:
62
+ if spec["executor"]["train_scope"] != model.train_scope:
63
+ raise ValueError("Checkpoint training scope differs from model")
64
+ if not weights:
65
+ raise ValueError("Cannot save an empty executor")
66
+ path.mkdir(parents=True, exist_ok=False)
67
+ torch.save(cpu_tree(weights), path / "adapter_model.pt")
68
+ torch.save(cpu_tree(training_state), path / "training_state.pt")
69
+ tokenizer.save_pretrained(path)
70
+ version = FIXED_PROMPT_FORMAT if model.recurrence_mode == "fixed_prompt" else (COMPLETION_FORMAT if completion else FORMAT)
71
+ if new_architecture:
72
+ version = EXECUTOR_FORMAT
73
+ (path / "recurrent_config.json").write_text(json.dumps({**spec, "format": version}, indent=2) + "\n")
74
+
75
+
76
+ def restore_adapters(model: RecurrentQwen, path: Path) -> None:
77
+ if checkpoint_mode(json.loads((path / "recurrent_config.json").read_text())) != model.recurrence_mode:
78
+ raise ValueError("Adapter checkpoint recurrence_mode differs from model")
79
+ state = torch.load(path / "adapter_model.pt", map_location="cpu", weights_only=True)
80
+ expected = {name: value for name, value in model.named_parameters() if value.requires_grad}
81
+ if set(state) != set(expected):
82
+ raise ValueError("Adapter checkpoint names do not match the recurrent model")
83
+ with torch.no_grad():
84
+ for name, parameter in expected.items():
85
+ tensor = state[name]
86
+ if tensor.shape != parameter.shape or not torch.isfinite(tensor).all():
87
+ raise ValueError(f"Invalid adapter tensor: {name}")
88
+ parameter.copy_(tensor)
89
+
90
+
91
+ def load_recurrent_checkpoint(path: Path, *, device: str = "cpu", dtype: str = "float32",
92
+ download: bool = False) -> tuple[RecurrentQwen, Any, dict]:
93
+ spec = json.loads((path / "recurrent_config.json").read_text())
94
+ mode = checkpoint_mode(spec)
95
+ if spec.get("prompt_format") != "dataset_raw" or spec.get("loss_vocabulary") != "symbols":
96
+ raise ValueError("Unsupported recurrent checkpoint format/prompt/loss")
97
+ completion = spec["format"] == COMPLETION_FORMAT or (mode == "fixed_prompt" and "completion_head" in spec)
98
+ if completion and (not isinstance(spec.get("completion_head"), dict) or
99
+ type(spec["completion_head"].get("intermediate")) is not int or
100
+ spec["completion_head"]["intermediate"] < 1):
101
+ raise ValueError("Invalid saved completion-head architecture")
102
+ if not completion and "completion_head" in spec:
103
+ raise ValueError("Legacy checkpoint cannot contain a completion head")
104
+ if spec.get("symbols") != list(SYMBOLS) or len(set(spec.get("token_ids", []))) != len(SYMBOLS):
105
+ raise ValueError("Checkpoint requires the complete A–Z symbol vocabulary")
106
+ tokenizer = AutoTokenizer.from_pretrained(path, local_files_only=True, trust_remote_code=False)
107
+ for symbol, token in zip(spec["symbols"], spec["token_ids"], strict=True):
108
+ if tokenizer.encode(" " + symbol, add_special_tokens=False) != [token]:
109
+ raise ValueError("Checkpoint tokenizer and symbol IDs disagree")
110
+ base = Qwen2ForCausalLM.from_pretrained(
111
+ spec["base_model"], revision=spec["revision"], local_files_only=not download,
112
+ dtype=getattr(torch, dtype), attn_implementation="eager", trust_remote_code=False,
113
+ ).to(device).eval()
114
+ model = RecurrentQwen(base, spec["recurrent_start"], spec["recurrent_end"], recurrence_mode=mode)
115
+ executor = spec.get("executor")
116
+ if executor is None or executor["train_scope"] == "lora":
117
+ attach_recurrent_lora(model, rank=spec["lora_rank"], alpha=spec["lora_alpha"])
118
+ if executor is not None:
119
+ model.configure_executor(tokenizer, **executor)
120
+ if completion and model.completion_head is None:
121
+ model.enable_completion(spec["completion_head"]["intermediate"])
122
+ restore_adapters(model, path)
123
+ model.eval()
124
+ return model, tokenizer, spec
scripts/recurrent_qwen/completion.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hidden-state-only completion head for explicit-step pointer recurrence."""
2
+
3
+ import torch
4
+ from torch import Tensor, nn
5
+
6
+
7
+ class CompletionHead(nn.Module):
8
+ """One stop logit per answer-position recurrent state [B,H].
9
+
10
+ No numeric loop index, parsed requested depth, decoded symbol, or target is
11
+ accepted by this interface. The prompt can encode its requested Steps.
12
+ """
13
+
14
+ def __init__(self, width: int, intermediate: int = 128) -> None:
15
+ super().__init__()
16
+ if width < 1 or intermediate < 1:
17
+ raise ValueError("Completion-head widths must be positive")
18
+ self.width = width
19
+ self.intermediate = intermediate
20
+ self.network = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, intermediate),
21
+ nn.GELU(), nn.Linear(intermediate, 1))
22
+
23
+ def forward(self, answer_state: Tensor) -> Tensor:
24
+ if answer_state.ndim != 2 or answer_state.shape[-1] != self.width:
25
+ raise ValueError("Completion head requires answer states [batch, hidden]")
26
+ return self.network(answer_state).squeeze(-1)
scripts/recurrent_qwen/interfaces.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit text routing and learned interfaces for isolated pointer execution."""
2
+ from functools import lru_cache
3
+ import re
4
+ from typing import Any
5
+
6
+ import torch
7
+ from torch import Tensor, nn
8
+
9
+ from .number_reader import SharedNumberReader
10
+
11
+
12
+ class PromptRouter:
13
+ """Remove the Steps line without parsing its value or changing public inputs.
14
+
15
+ Preserve the original answer index for existing diagnostic callers. Padding
16
+ inserted before that position is masked and RoPE positions are compacted, so
17
+ neither count digits nor their token length reach the executor. Cached CPU
18
+ tokenization is bounded; no prompt or activation survives in a checkpoint.
19
+ """
20
+ def __init__(self, tokenizer: Any) -> None:
21
+ self.tokenizer = tokenizer
22
+ self.executor_ids = lru_cache(maxsize=4096)(self._executor_ids)
23
+
24
+ def _executor_ids(self, ids: tuple[int, ...]) -> tuple[int, ...]:
25
+ prompt = self.tokenizer.decode(list(ids), skip_special_tokens=False,
26
+ clean_up_tokenization_spaces=False)
27
+ if not re.fullmatch(r"Rules: [^\n]+\nStart: [A-Z]\nSteps: [1-9][0-9]*\nAnswer:", prompt):
28
+ raise ValueError("Isolated execution requires the raw Rules/Start/Steps/Answer prompt")
29
+ routed = re.sub(r"\nSteps: [1-9][0-9]*", "", prompt)
30
+ return tuple(self.tokenizer.encode(routed, add_special_tokens=False))
31
+
32
+ def __call__(self, ids: Tensor, mask: Tensor) -> tuple[Tensor, Tensor, Tensor]:
33
+ rows = ids.detach().cpu().tolist()
34
+ masks = mask.detach().cpu().tolist()
35
+ routed = torch.full_like(ids, self.tokenizer.pad_token_id)
36
+ attention = torch.zeros_like(mask)
37
+ for row, (tokens, valid) in enumerate(zip(rows, masks, strict=True)):
38
+ positions = [i for i, active in enumerate(valid) if active]
39
+ executor = self.executor_ids(tuple(tokens[i] for i in positions))
40
+ end = positions[-1]
41
+ if not executor or len(executor) > end + 1:
42
+ raise ValueError("Routed executor prompt does not fit original input positions")
43
+ routed[row, :len(executor) - 1] = torch.tensor(executor[:-1], device=ids.device)
44
+ routed[row, end] = executor[-1]
45
+ attention[row, :len(executor) - 1] = 1
46
+ attention[row, end] = 1
47
+ positions = (attention.cumsum(-1) - 1).clamp_min(0)
48
+ return routed, attention, positions
49
+
50
+
51
+ class ReentryBridge(nn.Module):
52
+ """Learned map from R output back to R input, applied only on re-entry.
53
+
54
+ Normalize direction, then restore the *initial* working-state RMS scale.
55
+ The scale contains rules/start information only and stays differentiable.
56
+ Identity initialization of the projection does not claim T>1 equivalence.
57
+ """
58
+ def __init__(self, width: int) -> None:
59
+ super().__init__()
60
+ self.projection = nn.Linear(width, width, bias=False)
61
+ nn.init.eye_(self.projection.weight)
62
+ self.gain = nn.Parameter(torch.ones(width))
63
+
64
+ def forward(self, state: Tensor, initial: Tensor) -> Tensor:
65
+ rms = state.float().square().mean(-1, keepdim=True).add(1e-6).sqrt()
66
+ scale = initial.float().square().mean(-1, keepdim=True).add(1e-6).sqrt()
67
+ normalized = (state.float() / rms * scale).to(state.dtype)
68
+ return self.projection(normalized * self.gain)
69
+
70
+
71
+ class RecurrentController(nn.Module):
72
+ """Own learned memory; detached prompt/executor features, no numeric clock."""
73
+ kind = 'gru'
74
+ def __init__(self, width: int, intermediate: int = 128) -> None:
75
+ super().__init__()
76
+ self.intermediate = intermediate
77
+ self.context = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, intermediate), nn.Tanh())
78
+ self.observation = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, intermediate), nn.Tanh())
79
+ self.cell = nn.GRUCell(intermediate, intermediate)
80
+ self.readout = nn.Linear(intermediate, 1)
81
+
82
+ def initialize(self, prompt_state: Tensor) -> Tensor:
83
+ return self.context(prompt_state.detach())
84
+
85
+ def advance(self, state: Tensor, memory: Tensor) -> tuple[Tensor, Tensor]:
86
+ memory = self.cell(self.observation(state.detach()), memory)
87
+ return self.readout(memory).squeeze(-1), memory
88
+
89
+
90
+ class AffineSuffixController(RecurrentController):
91
+ """Learned prompt reading and unbounded scalar recurrence; no parsed clock.
92
+
93
+ Initialization consumes flattened final-eight frozen token embeddings.
94
+ The trainable cell implements a*m+b, initialized to identity (b=0), never
95
+ a programmed decrement. Numerical labels train the cell through free-running
96
+ recurrence; stop BCE trains its readout without distorting counting dynamics.
97
+ Executor observations are intentionally ignored for this requested-count
98
+ controller: timing is independent of the pointer's identity or correctness.
99
+ """
100
+ kind = 'affine_suffix'
101
+ suffix_tokens = 8
102
+
103
+ def __init__(self, width: int) -> None:
104
+ nn.Module.__init__(self)
105
+ self.width = width
106
+ self.intermediate = 1
107
+ self.context = nn.Linear(width * self.suffix_tokens, 1)
108
+ self.observation = nn.Identity()
109
+ self.cell = nn.Linear(1, 1)
110
+ self.readout = nn.Linear(1, 1)
111
+ nn.init.ones_(self.cell.weight)
112
+ nn.init.zeros_(self.cell.bias)
113
+ nn.init.zeros_(self.readout.weight)
114
+ nn.init.zeros_(self.readout.bias)
115
+
116
+ def advance(self, state: Tensor, memory: Tensor) -> tuple[Tensor, Tensor]:
117
+ self.observation(state.detach()) # Preserve observation hooks for cache/native checks.
118
+ memory = self.cell(memory)
119
+ return self.readout(memory.detach()).squeeze(-1), memory
120
+
121
+
122
+ class SharedNumberController(AffineSuffixController):
123
+ """Same learned countdown; share the number reader across positions/lengths."""
124
+ kind = 'shared_number'
125
+
126
+ def __init__(self, width: int) -> None:
127
+ super().__init__(width)
128
+ self.context = SharedNumberReader(width)
129
+
130
+
131
+ def controller_head(width: int, intermediate: int, kind: str = 'gru') -> RecurrentController:
132
+ """Explicit architecture factory; historical checkpoints default to GRU."""
133
+ if kind == 'gru':
134
+ return RecurrentController(width, intermediate)
135
+ if kind == 'affine_suffix' and intermediate == 1:
136
+ return AffineSuffixController(width)
137
+ if kind == 'shared_number' and intermediate == 1:
138
+ return SharedNumberController(width)
139
+ raise ValueError('Unknown controller kind or incompatible memory width')
scripts/recurrent_qwen/lora_utils.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attach PEFT LoRA only to the shared recurrent decoder group."""
2
+
3
+ from peft import LoraConfig, inject_adapter_in_model
4
+
5
+ from .model import RecurrentQwen
6
+
7
+
8
+ def attach_recurrent_lora(
9
+ model: RecurrentQwen,
10
+ *,
11
+ rank: int = 8,
12
+ alpha: int = 16,
13
+ targets: tuple[str, ...] = ("q_proj", "v_proj"),
14
+ ) -> None:
15
+ """Mutate R in place; zero-initialized B preserves the original forward.
16
+
17
+ No bias training, dropout, bridge, or adapters outside R. Original weights
18
+ remain frozen. An initial zero gradient for A is expected when B is zero.
19
+ """
20
+ if rank < 1 or alpha < 1:
21
+ raise ValueError("LoRA rank and alpha must be positive")
22
+ valid = {"q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"}
23
+ if not targets or len(set(targets)) != len(targets) or not set(targets) <= valid:
24
+ raise ValueError(f"Choose unique LoRA targets from {sorted(valid)}")
25
+ if getattr(model.recurrent, "peft_config", None):
26
+ raise ValueError("Recurrent LoRA is already attached")
27
+ config = LoraConfig(
28
+ r=rank,
29
+ lora_alpha=alpha,
30
+ target_modules=list(targets),
31
+ lora_dropout=0.0,
32
+ bias="none",
33
+ init_lora_weights=True,
34
+ )
35
+ inject_adapter_in_model(config, model.recurrent)
scripts/recurrent_qwen/memory.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Per-forward, differentiable prompt memory for a single writable position."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import torch
6
+ from torch import Tensor
7
+
8
+
9
+ @dataclass
10
+ class PromptMemory:
11
+ """Capture layer inputs on pass one and reuse their non-working positions.
12
+
13
+ write_mask is [B,S,1]; captured tensors are [B,S,H]. Never detach: later
14
+ losses must reach adapters that formed the prompt memory on the first pass.
15
+ Objects live for one model forward only, not across examples or updates.
16
+ This is an activation-memory reference path, not an attention KV cache.
17
+ """
18
+
19
+ write_mask: Tensor
20
+ layers: list[Tensor] = field(default_factory=list)
21
+ output: Tensor | None = None
22
+ keys_values: list[tuple[Tensor, Tensor]] = field(default_factory=list)
23
+ layer_outputs: list[Tensor] = field(default_factory=list)
24
+
25
+ def layer_input(self, index: int, hidden: Tensor) -> Tensor:
26
+ if self.output is None:
27
+ self.layers.append(hidden)
28
+ return hidden
29
+ return torch.where(self.write_mask, hidden, self.layers[index])
30
+
31
+ def block_output(self, hidden: Tensor) -> Tensor:
32
+ if self.output is None:
33
+ self.output = hidden
34
+ return hidden
35
+ return torch.where(self.write_mask, hidden, self.output)
scripts/recurrent_qwen/model.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Qwen2 decoder surgery for the pinned Transformers implementation."""
2
+
3
+ import math
4
+ from typing import Any, Callable, Literal
5
+
6
+ import torch
7
+ from torch import Tensor, nn
8
+ from transformers import Qwen2ForCausalLM
9
+ from transformers.masking_utils import create_causal_mask
10
+
11
+ from .outputs import RecurrentOutput, answer_margin
12
+ from .completion import CompletionHead
13
+ from .memory import PromptMemory
14
+ from .prefix import fixed_prefix_layer
15
+ from torch.utils.checkpoint import checkpoint
16
+ from .interfaces import PromptRouter, ReentryBridge, RecurrentController, AffineSuffixController, SharedNumberController, controller_head
17
+ from .number_reader import MAX_NUMBER_TOKENS, number_token_ids
18
+
19
+
20
+ def _answer_readout(hidden: Tensor, positions: list[int]) -> Tensor:
21
+ # Integer slices avoid MPS's nondeterministic scatter backward for advanced
22
+ # indexing/gather. Batches are deliberately tiny; positions sync once per forward.
23
+ return torch.stack([hidden[row, position] for row, position in enumerate(positions)])
24
+
25
+
26
+ class DecoderBlock(nn.Module):
27
+ """An ordered group of original decoder objects, with no copies."""
28
+
29
+ def __init__(self, layers: list[nn.Module]) -> None:
30
+ super().__init__()
31
+ self.layers = nn.ModuleList(layers)
32
+ self.prefix_reuse = False
33
+ self.gradient_checkpointing = False
34
+
35
+ def forward(
36
+ self,
37
+ hidden: Tensor,
38
+ attention_mask: Tensor | None,
39
+ position_ids: Tensor,
40
+ position_embeddings: tuple[Tensor, Tensor],
41
+ memory: PromptMemory | None = None,
42
+ ) -> Tensor:
43
+ for index, layer in enumerate(self.layers):
44
+ if memory is not None:
45
+ hidden = memory.layer_input(index, hidden)
46
+ if self.prefix_reuse and memory is not None:
47
+ hidden = fixed_prefix_layer(layer, hidden, position_embeddings, attention_mask,
48
+ memory, index, self.gradient_checkpointing)
49
+ continue
50
+ call = layer
51
+ if self.gradient_checkpointing and torch.is_grad_enabled():
52
+ def call(x, _layer=layer, **kwargs):
53
+ return checkpoint(_layer, x, use_reentrant=False, **kwargs)
54
+ hidden = call(
55
+ hidden,
56
+ attention_mask=attention_mask,
57
+ position_ids=position_ids,
58
+ position_embeddings=position_embeddings,
59
+ past_key_values=None,
60
+ use_cache=False,
61
+ )
62
+ return memory.block_output(hidden) if memory is not None else hidden
63
+
64
+
65
+ class RecurrentQwen(nn.Module):
66
+ """P -> R repeated T times -> C, reusing the supplied model's weights.
67
+
68
+ Construction freezes and shares the supplied base model in place. Load a
69
+ fresh base for an independent reference after adapters have been attached.
70
+ Only full attention and default RoPE are supported in Stage 0, as used by
71
+ Qwen2.5-0.5B-Instruct. Device/dtype are inherited, never selected here.
72
+ """
73
+
74
+ def __init__(
75
+ self, base: Qwen2ForCausalLM, recurrent_start: int = 6, recurrent_end: int = 18,
76
+ *, recurrence_mode: str = "full_sequence",
77
+ ) -> None:
78
+ super().__init__()
79
+ if not isinstance(base, Qwen2ForCausalLM):
80
+ raise TypeError("Stage 0 requires Qwen2ForCausalLM")
81
+ if recurrence_mode not in ("full_sequence", "fixed_prompt"):
82
+ raise ValueError("recurrence_mode must be full_sequence or fixed_prompt")
83
+ self.recurrence_mode = recurrence_mode
84
+ layers = list(base.model.layers)
85
+ if not 0 <= recurrent_start < recurrent_end <= len(layers):
86
+ raise ValueError("Require 0 <= recurrent_start < recurrent_end <= layer count")
87
+ if any(kind != "full_attention" for kind in base.config.layer_types):
88
+ raise ValueError("Stage 0 supports full-attention Qwen layers only")
89
+ if base.config.rope_parameters["rope_type"] != "default":
90
+ raise ValueError("Stage 0 supports default RoPE only")
91
+ if getattr(base, "peft_config", None):
92
+ raise ValueError("Supply an unadapted base; attach LoRA to recurrence afterwards")
93
+ base.requires_grad_(False)
94
+ base.zero_grad(set_to_none=True)
95
+ base.config.use_cache = False
96
+ self.config = base.config
97
+ self.recurrent_start = recurrent_start
98
+ self.recurrent_end = recurrent_end
99
+ self.embed_tokens = base.model.embed_tokens
100
+ self.rotary_emb = base.model.rotary_emb
101
+ self.prelude = DecoderBlock(layers[:recurrent_start])
102
+ self.recurrent = DecoderBlock(layers[recurrent_start:recurrent_end])
103
+ self.coda = DecoderBlock(layers[recurrent_end:])
104
+ self.norm = base.model.norm
105
+ self.lm_head = base.lm_head
106
+ self.completion_head: CompletionHead | RecurrentController | None = None
107
+ self.router = None
108
+ self.bridge = None
109
+ self.state_head = None
110
+ self.train_scope = "lora"
111
+ self.prefix_reuse = False
112
+ self.gradient_checkpointing = False
113
+ self.train(base.training)
114
+
115
+ def enable_completion(self, intermediate: int = 128) -> None:
116
+ """Attach a trainable stop readout without changing recurrent inputs."""
117
+ if self.completion_head is not None:
118
+ raise ValueError("Completion head is already attached")
119
+ self.completion_head = CompletionHead(self.config.hidden_size, intermediate).to(
120
+ device=self.embed_tokens.weight.device, dtype=self.embed_tokens.weight.dtype)
121
+
122
+ def configure_executor(self, tokenizer: Any, *, train_scope: str = "recurrent",
123
+ bridge: bool = True, direct_readout: bool = True,
124
+ controller_size: int = 128, isolated: bool = True,
125
+ prefix_reuse: bool = False, gradient_checkpointing: bool = False,
126
+ controller_kind: str = 'gru') -> None:
127
+ """Opt-in architecture; legacy forwards/checkpoints keep their semantics."""
128
+ if train_scope not in ("lora", "recurrent", "full"):
129
+ raise ValueError("train_scope must be lora, recurrent, or full")
130
+ if self.recurrence_mode != "fixed_prompt":
131
+ raise ValueError("Executor interfaces require fixed_prompt recurrence")
132
+ if train_scope != "lora" and any(".lora_" in name for name, _ in self.named_parameters()):
133
+ raise ValueError("Full-weight scopes require an unadapted base, not an attached LoRA model")
134
+ if prefix_reuse and self.config.attention_dropout != 0:
135
+ raise ValueError("Prefix reuse requires zero attention dropout for equivalent execution")
136
+ self.train_scope = train_scope
137
+ if train_scope == "recurrent":
138
+ self.recurrent.requires_grad_(True)
139
+ elif train_scope == "full":
140
+ self.requires_grad_(True)
141
+ device, dtype = self.embed_tokens.weight.device, self.embed_tokens.weight.dtype
142
+ if isolated:
143
+ self.router = PromptRouter(tokenizer)
144
+ if controller_size:
145
+ self.completion_head = controller_head(self.config.hidden_size, controller_size, controller_kind).to(device=device, dtype=dtype)
146
+ if bridge:
147
+ self.bridge = ReentryBridge(self.config.hidden_size).to(device=device, dtype=dtype)
148
+ if direct_readout:
149
+ self.state_head = nn.Linear(self.config.hidden_size, 26).to(device=device, dtype=dtype)
150
+ self.prefix_reuse = prefix_reuse
151
+ self.gradient_checkpointing = gradient_checkpointing
152
+ for block in (self.prelude, self.recurrent, self.coda):
153
+ block.prefix_reuse = prefix_reuse
154
+ block.gradient_checkpointing = gradient_checkpointing
155
+
156
+ def controller_prompt_state(self, input_ids: Tensor, attention_mask: Tensor,
157
+ answer_positions: Tensor | None = None) -> Tensor:
158
+ """Frozen native initializer features [B,H_context], without R/C execution.
159
+
160
+ This interface runs neither R nor C and consumes only token IDs/mask.
161
+ GRU uses full-prompt P's answer position; affine_suffix uses the final
162
+ eight frozen token embeddings. Shared-number uses routed field-token
163
+ embeddings and validity masks. No branch interprets a numeric value.
164
+ """
165
+ if input_ids.ndim != 2 or attention_mask.shape != input_ids.shape or not attention_mask.any(-1).all():
166
+ raise ValueError('Need aligned nonempty [B,S] prompt IDs and mask')
167
+ if answer_positions is None:
168
+ answer_positions = (attention_mask.long() * torch.arange(1, input_ids.shape[1] + 1,
169
+ device=input_ids.device)).argmax(-1)
170
+ if isinstance(self.completion_head, SharedNumberController):
171
+ rows = []
172
+ for ids, mask in zip(input_ids.detach().cpu().tolist(), attention_mask.detach().cpu().tolist(), strict=True):
173
+ valid = tuple(t for t, active in zip(ids, mask, strict=True) if active)
174
+ rows.append(number_token_ids(self.router.tokenizer, valid))
175
+ with torch.no_grad():
176
+ context = self.embed_tokens.weight.new_zeros(len(rows), MAX_NUMBER_TOKENS, self.config.hidden_size + 1)
177
+ for i, tokens in enumerate(rows):
178
+ context[i, :len(tokens), :-1] = self.embed_tokens(torch.tensor(tokens, device=input_ids.device))
179
+ context[i, :len(tokens), -1] = 1
180
+ return context.flatten(1)
181
+ if isinstance(self.completion_head, AffineSuffixController):
182
+ k = self.completion_head.suffix_tokens
183
+ if (answer_positions < k - 1).any():
184
+ raise ValueError('Affine suffix initialization requires eight final prompt tokens')
185
+ with torch.no_grad():
186
+ embeddings = self.embed_tokens(input_ids)
187
+ return torch.stack([embeddings[i, end - k + 1:end + 1].flatten()
188
+ for i, end in enumerate(answer_positions.tolist())])
189
+ with torch.no_grad():
190
+ context = self.embed_tokens(input_ids)
191
+ positions = torch.arange(input_ids.shape[1], device=input_ids.device).unsqueeze(0)
192
+ mask = create_causal_mask(config=self.config, inputs_embeds=context,
193
+ attention_mask=attention_mask, past_key_values=None, position_ids=positions)
194
+ context = self.prelude(context, mask, positions, self.rotary_emb(context, positions))
195
+ return _answer_readout(context, answer_positions.tolist())
196
+
197
+ def forward(
198
+ self,
199
+ input_ids: Tensor,
200
+ attention_mask: Tensor | None = None,
201
+ *,
202
+ num_loops: int = 1,
203
+ position_ids: Tensor | None = None,
204
+ answer_positions: Tensor | None = None,
205
+ labels: Tensor | None = None,
206
+ allowed_token_ids: Tensor | None = None,
207
+ return_hidden_states: bool = False,
208
+ logits_mode: Literal["answer", "all"] = "answer",
209
+ stop_policy: Callable[[int, Tensor, Tensor], bool] | None = None,
210
+ completion_threshold: float | None = None,
211
+ readout_token_ids: list[int] | None = None,
212
+ loop_callback: Callable[[int, Tensor, Tensor | None, Tensor | None], None] | None = None,
213
+ ) -> RecurrentOutput:
214
+ """Run complete prompt states through shared recurrence, without KV cache.
215
+
216
+ Inputs/mask: [B,S]; positions: [1,S] or [B,S], default arange(S),
217
+ matching ordinary Qwen forward (including padding). Answer positions:
218
+ [B], default last unmasked token. Labels: [B,T] *token IDs*, one
219
+ target per loop, with an explicit allowed answer set [A]. No CE loss
220
+ or causal label shifting is performed. Pass repeated final labels
221
+ explicitly if final-answer margins, rather than step margins, are wanted.
222
+ Optional stop_policy(t, h_t, answer_logits_t) is inference-only, batch 1,
223
+ and may return True to end after loop t. It never sees labels.
224
+ completion_threshold enables the checkpoint's hidden-state-only head,
225
+ independent of task depth, under the num_loops safety cap (batch 1).
226
+ fixed_prompt keeps only the final unmasked position writable; returned
227
+ full-shaped states contain fixed first-pass prefix outputs for diagnostics.
228
+ loop_callback receives one-based loop, C logits [1,A], optional stop
229
+ logits [1], and optional R logits [1,26] after each readout, including
230
+ the stopping loop. Detached copies protect inference from observer edits.
231
+ It is inference-only; its return value cannot choose when to stop.
232
+ """
233
+ if type(num_loops) is not int or num_loops < 1:
234
+ raise ValueError("num_loops must be a positive integer")
235
+ if loop_callback is not None and (self.training or torch.is_grad_enabled() or
236
+ input_ids.shape[0] != 1 or labels is not None or logits_mode != "answer"):
237
+ raise ValueError("Loop observation requires eval/no_grad, batch 1, no labels, and answer logits")
238
+ if stop_policy is not None and (self.training or torch.is_grad_enabled() or input_ids.shape[0] != 1 or labels is not None or logits_mode != "answer"):
239
+ raise ValueError("Stopping requires eval/no_grad, batch 1, no labels, and answer logits")
240
+ if completion_threshold is not None:
241
+ if (not math.isfinite(completion_threshold) or not 0 < completion_threshold < 1 or
242
+ self.completion_head is None or stop_policy is not None):
243
+ raise ValueError("Completion stopping requires a head, threshold in (0,1), and no other stop policy")
244
+ if self.training or torch.is_grad_enabled() or input_ids.shape[0] != 1 or labels is not None or logits_mode != "answer":
245
+ raise ValueError("Completion stopping requires eval/no_grad, batch 1, no labels, and answer logits")
246
+ if logits_mode not in ("answer", "all"):
247
+ raise ValueError("logits_mode must be 'answer' or 'all'")
248
+ if input_ids.ndim != 2 or input_ids.dtype != torch.long or 0 in input_ids.shape:
249
+ raise ValueError("input_ids must be a nonempty torch.long [batch, sequence] tensor")
250
+ if input_ids.device != self.embed_tokens.weight.device:
251
+ raise ValueError("Input IDs and model must share a device")
252
+ if (input_ids < 0).any() or (input_ids >= self.config.vocab_size).any():
253
+ raise ValueError("Input IDs must be within the model vocabulary")
254
+ batch, sequence = input_ids.shape
255
+ device = input_ids.device
256
+ if attention_mask is None:
257
+ attention_mask = torch.ones_like(input_ids)
258
+ if attention_mask.shape != input_ids.shape or attention_mask.device != device:
259
+ raise ValueError("attention_mask must match input_ids shape and device")
260
+ if not ((attention_mask == 0) | (attention_mask == 1)).all():
261
+ raise ValueError("attention_mask must contain only 0 and 1")
262
+ if not attention_mask.bool().any(dim=-1).all():
263
+ raise ValueError("Each example must have at least one unmasked token")
264
+ if position_ids is None:
265
+ position_ids = torch.arange(sequence, device=device).unsqueeze(0)
266
+ if (
267
+ position_ids.shape not in ((1, sequence), (batch, sequence))
268
+ or position_ids.dtype != torch.long
269
+ or position_ids.device != device
270
+ or (position_ids < 0).any()
271
+ ):
272
+ raise ValueError("position_ids must be nonnegative long [1,S] or [B,S] on the input device")
273
+ if answer_positions is None:
274
+ indices = torch.arange(sequence, device=device).expand(batch, -1)
275
+ answer_positions = indices.masked_fill(~attention_mask.bool(), -1).max(-1).values
276
+ if (
277
+ answer_positions.shape != (batch,)
278
+ or answer_positions.dtype != torch.long
279
+ or answer_positions.device != device
280
+ or (answer_positions < 0).any()
281
+ or (answer_positions >= sequence).any()
282
+ ):
283
+ raise ValueError("answer_positions must be long [batch] valid sequence indices on the input device")
284
+ rows = torch.arange(batch, device=device)
285
+ if not attention_mask[rows, answer_positions].bool().all():
286
+ raise ValueError("Answer positions cannot point to padding")
287
+ if self.recurrence_mode == "fixed_prompt":
288
+ last = torch.arange(sequence, device=device).expand(batch, -1).masked_fill(~attention_mask.bool(), -1).max(-1).values
289
+ if not torch.equal(answer_positions, last):
290
+ raise ValueError("fixed_prompt requires the final unmasked position as its working state")
291
+ readout_positions = answer_positions.tolist()
292
+ if (labels is None) != (allowed_token_ids is None):
293
+ raise ValueError("Provide both labels and allowed_token_ids to compute margins")
294
+ if labels is not None and labels.shape != (batch, num_loops):
295
+ raise ValueError("labels must have shape [batch, num_loops]")
296
+
297
+ controller_initial = None
298
+ if self.router is not None:
299
+ if logits_mode != "answer":
300
+ raise ValueError("Isolated execution supports answer-position logits only")
301
+ # Controller prompt encoding is isolated even for full-model training.
302
+ controller_initial = self.controller_prompt_state(input_ids, attention_mask, answer_positions)
303
+ input_ids, attention_mask, position_ids = self.router(input_ids, attention_mask)
304
+ hidden = self.embed_tokens(input_ids)
305
+ causal_mask = create_causal_mask(
306
+ config=self.config,
307
+ inputs_embeds=hidden,
308
+ attention_mask=attention_mask,
309
+ past_key_values=None,
310
+ position_ids=position_ids,
311
+ )
312
+ position_embeddings = self.rotary_emb(hidden, position_ids)
313
+ block_args = (causal_mask, position_ids, position_embeddings)
314
+ hidden = self.prelude(hidden, *block_args)
315
+ initial_hidden = hidden if return_hidden_states else None
316
+ bridge_initial = _answer_readout(hidden, readout_positions)
317
+ controller_memory = (self.completion_head.initialize(controller_initial)
318
+ if isinstance(self.completion_head, RecurrentController) else None)
319
+ recurrent_memory = coda_memory = None
320
+ if self.recurrence_mode == "fixed_prompt":
321
+ write_mask = (torch.arange(sequence, device=device)[None, :] == answer_positions[:, None]).unsqueeze(-1)
322
+ recurrent_memory, coda_memory = PromptMemory(write_mask), PromptMemory(write_mask)
323
+ states, logits, margins, stop_logits, state_logits = [], [], [], [], []
324
+ for loop in range(num_loops):
325
+ if loop and self.bridge is not None:
326
+ working = self.bridge(_answer_readout(hidden, readout_positions), bridge_initial)
327
+ hidden = torch.where(write_mask, working[:, None, :], hidden)
328
+ hidden = (self.recurrent(hidden, *block_args, memory=recurrent_memory)
329
+ if recurrent_memory is not None else self.recurrent(hidden, *block_args))
330
+ working = _answer_readout(hidden, readout_positions)
331
+ if isinstance(self.completion_head, RecurrentController):
332
+ stop, controller_memory = self.completion_head.advance(working, controller_memory)
333
+ stop_logits.append(stop)
334
+ elif self.completion_head is not None:
335
+ stop_logits.append(self.completion_head(working))
336
+ if self.state_head is not None:
337
+ state_logits.append(self.state_head(working))
338
+ if return_hidden_states:
339
+ states.append(hidden)
340
+ # C stays differentiable even when frozen: readout loss must reach R.
341
+ decoded = self.norm(self.coda(hidden, *block_args, memory=coda_memory)
342
+ if coda_memory is not None else self.coda(hidden, *block_args))
343
+ readout = decoded if logits_mode == "all" else _answer_readout(decoded, readout_positions)
344
+ if readout_token_ids is None:
345
+ scores = self.lm_head(readout)
346
+ else:
347
+ if labels is not None:
348
+ raise ValueError("Use full-vocabulary readout when requesting token-ID margins")
349
+ ids = torch.tensor(readout_token_ids, device=device)
350
+ scores = nn.functional.linear(readout, self.lm_head.weight.index_select(0, ids))
351
+ logits.append(scores)
352
+ if loop_callback is not None:
353
+ loop_callback(loop + 1, scores.detach().clone(),
354
+ stop_logits[-1].detach().clone() if stop_logits else None,
355
+ state_logits[-1].detach().clone() if state_logits else None)
356
+ if labels is not None:
357
+ answer_scores = _answer_readout(scores, readout_positions) if logits_mode == "all" else scores
358
+ margins.append(answer_margin(answer_scores, labels[:, loop], allowed_token_ids))
359
+ if stop_policy is not None and stop_policy(loop + 1, hidden, scores):
360
+ break
361
+ if completion_threshold is not None:
362
+ probability = stop_logits[-1].sigmoid().item()
363
+ if not math.isfinite(probability):
364
+ raise FloatingPointError("Nonfinite completion probability")
365
+ if probability >= completion_threshold:
366
+ break
367
+ return RecurrentOutput(
368
+ loop_logits=tuple(logits),
369
+ state_logits=torch.stack(state_logits, dim=1) if state_logits else None,
370
+ hidden_states=tuple(states) if return_hidden_states else None,
371
+ initial_hidden_state=initial_hidden,
372
+ margins=torch.stack(margins, dim=1) if margins else None,
373
+ stop_logits=torch.stack(stop_logits, dim=1) if stop_logits else None,
374
+ )
scripts/recurrent_qwen/number_reader.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared learned token composition, with no numeric parser in inference."""
2
+ import re
3
+
4
+ import torch
5
+ from torch import Tensor, nn
6
+
7
+
8
+ MAX_NUMBER_TOKENS = 8
9
+
10
+
11
+ def number_token_ids(tokenizer, ids: tuple[int, ...]) -> tuple[int, ...]:
12
+ """Route only the raw Steps text; never convert it to a number.
13
+
14
+ One token per character is a tokenizer contract, not a digit-value lookup.
15
+ The public prompt and executor routing remain unchanged.
16
+ """
17
+ text = tokenizer.decode(list(ids), skip_special_tokens=False,
18
+ clean_up_tokenization_spaces=False)
19
+ match = re.fullmatch(r'Rules: [^\n]+\nStart: [A-Z]\nSteps: ([1-9][0-9]*)\nAnswer:', text)
20
+ if match is None:
21
+ raise ValueError('Number reader requires the raw Rules/Start/Steps/Answer prompt')
22
+ field = match.group(1)
23
+ tokens = tokenizer.encode(field, add_special_tokens=False)
24
+ if not 1 <= len(tokens) <= MAX_NUMBER_TOKENS or len(tokens) != len(field):
25
+ raise ValueError('Number reader requires one token per decimal character and at most eight characters')
26
+ # Length alone does not rule out a tokenizer that groups/reorders characters.
27
+ if any(tokenizer.decode([t], clean_up_tokenization_spaces=False) != c
28
+ for t, c in zip(tokens, field, strict=True)):
29
+ raise ValueError('Number reader tokenizer does not preserve individual decimal characters')
30
+ return tuple(tokens)
31
+
32
+
33
+ class SharedNumberReader(nn.Module):
34
+ """Read padded frozen embeddings [B,8*(H+1)] using the same map at each slot.
35
+
36
+ Last channel of each slot is a validity mask. Valid tokens are left aligned.
37
+ The gain starts at identity, not a decimal radix. Training labels fit both
38
+ gain and token projection. Accumulate in float64 to reduce numerical error,
39
+ then return the controller's scalar-memory dtype [B,1].
40
+ """
41
+ def __init__(self, width: int) -> None:
42
+ super().__init__()
43
+ self.width = width
44
+ self.in_features = MAX_NUMBER_TOKENS * (width + 1)
45
+ self.token_value = nn.Linear(width, 1)
46
+ self.gain = nn.Parameter(torch.ones(()))
47
+
48
+ def unpack(self, context: Tensor) -> tuple[Tensor, Tensor]:
49
+ if context.ndim != 2 or context.shape[1] != self.in_features:
50
+ raise ValueError('Need padded number embeddings with token-validity masks')
51
+ slots = context.reshape(-1, MAX_NUMBER_TOKENS, self.width + 1)
52
+ return slots[..., :self.width], slots[..., self.width:]
53
+
54
+ def forward(self, context: Tensor) -> Tensor:
55
+ embeddings, mask = self.unpack(context.detach())
56
+ values = torch.nn.functional.linear(embeddings.double(), self.token_value.weight.double(),
57
+ self.token_value.bias.double())
58
+ state = values.new_zeros(len(values), 1)
59
+ for value, valid in zip(values.unbind(1), mask.unbind(1), strict=True):
60
+ state = torch.where(valid.bool(), self.gain.double() * state + value, state)
61
+ return state.to(self.token_value.weight.dtype)
scripts/recurrent_qwen/outputs.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loop outputs and symbolic readout semantics."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import torch
6
+ from torch import Tensor
7
+
8
+
9
+ @dataclass
10
+ class RecurrentOutput:
11
+ """Tuple index t-1 represents loop t; h_0 is not a decoded start symbol.
12
+
13
+ Logits are [batch, vocab] in answer mode or [batch, sequence, vocab]
14
+ in all-token mode. Optional hidden states are [batch, sequence, hidden].
15
+ Margins are [batch, loops]. Tensors retain autograd unless the caller
16
+ disables it; observations never feed back into the recurrent trajectory.
17
+ """
18
+
19
+ loop_logits: tuple[Tensor, ...]
20
+ hidden_states: tuple[Tensor, ...] | None = None
21
+ initial_hidden_state: Tensor | None = None
22
+ margins: Tensor | None = None
23
+ state_logits: Tensor | None = None # Direct R readout [B,T,26], never fed back.
24
+ stop_logits: Tensor | None = None # [batch, executed_loops], when completion is enabled.
25
+
26
+ @property
27
+ def logits(self) -> Tensor:
28
+ """The final loop's logits."""
29
+ return self.loop_logits[-1]
30
+
31
+
32
+ def answer_margin(logits: Tensor, labels: Tensor, allowed_token_ids: Tensor) -> Tensor:
33
+ """Correct raw logit minus the best *other allowed* logit, per example.
34
+
35
+ Shapes: logits [batch, vocab], labels [batch] (token IDs), allowed IDs
36
+ [answers]. Labels must belong to a unique answer set of at least two IDs.
37
+ A tie has margin zero, so it is not margin-based correctness.
38
+ """
39
+ if logits.ndim != 2 or labels.shape != logits.shape[:1]:
40
+ raise ValueError("Expected logits [batch, vocab] and labels [batch]")
41
+ if labels.dtype != torch.long or allowed_token_ids.dtype != torch.long:
42
+ raise ValueError("Labels and allowed_token_ids must be torch.long token IDs")
43
+ if labels.device != logits.device or allowed_token_ids.device != logits.device:
44
+ raise ValueError("Logits, labels, and allowed_token_ids must share a device")
45
+ if allowed_token_ids.ndim != 1 or allowed_token_ids.numel() < 2:
46
+ raise ValueError("Provide at least two allowed token IDs in a 1D tensor")
47
+ if allowed_token_ids.unique().numel() != allowed_token_ids.numel():
48
+ raise ValueError("Allowed token IDs must be unique")
49
+ if (allowed_token_ids < 0).any() or (allowed_token_ids >= logits.shape[-1]).any():
50
+ raise ValueError("Allowed token IDs must be within the model vocabulary")
51
+ correct = labels[:, None] == allowed_token_ids[None, :]
52
+ if not correct.any(dim=-1).all():
53
+ raise ValueError("Every label must be an allowed token ID")
54
+ scores = logits.index_select(-1, allowed_token_ids)
55
+ wrong = scores.masked_fill(correct, -torch.inf).max(dim=-1).values
56
+ return logits.gather(-1, labels[:, None]).squeeze(-1) - wrong
scripts/recurrent_qwen/partitioned.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference-only causal factorization for count-dependent historical models.
2
+
3
+ Identical prompt prefixes are computed once per graph, at *each* loop for
4
+ full-sequence recurrence. Count-specific suffixes remain separate continuous
5
+ states. Fixed-prompt recurrence retains first-pass layer K/V, replacing only
6
+ the working position. No symbols, labels, depth values or stops enter R.
7
+ """
8
+ from dataclasses import dataclass
9
+
10
+ import torch
11
+ from torch import Tensor, nn
12
+ from transformers.models.qwen2.modeling_qwen2 import apply_rotary_pos_emb, eager_attention_forward
13
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
14
+
15
+ from .prefix import layer_compute
16
+
17
+
18
+ def suffix_layer(layer: nn.Module, hidden: Tensor, embeddings: tuple[Tensor, Tensor],
19
+ mask: Tensor, prefix: tuple[Tensor, Tensor],
20
+ replace_positions: Tensor | None = None) -> tuple[Tensor, Tensor, Tensor]:
21
+ """Queries [B,Q,H], rotary K/V prefix [B,KV,P,D]; full additive query mask."""
22
+ attention = layer.self_attn
23
+ x = layer.input_layernorm(hidden)
24
+ shape = (*x.shape[:-1], -1, attention.head_dim)
25
+ q = attention.q_proj(x).view(shape).transpose(1, 2)
26
+ k = attention.k_proj(x).view(shape).transpose(1, 2)
27
+ v = attention.v_proj(x).view(shape).transpose(1, 2)
28
+ q, k = apply_rotary_pos_emb(q, k, *embeddings)
29
+ if replace_positions is None:
30
+ k, v = torch.cat((prefix[0], k), -2), torch.cat((prefix[1], v), -2)
31
+ else:
32
+ # Work on fresh tensors: original fixed memory is immutable across loops.
33
+ k_full, v_full = prefix[0].clone(), prefix[1].clone()
34
+ rows = torch.arange(len(hidden), device=hidden.device)
35
+ k_full[rows, :, replace_positions, :] = k[:, :, 0, :]
36
+ v_full[rows, :, replace_positions, :] = v[:, :, 0, :]
37
+ k, v = k_full, v_full
38
+ interface = ALL_ATTENTION_FUNCTIONS.get_interface(attention.config._attn_implementation, eager_attention_forward)
39
+ output, _ = interface(attention, q, k, v, mask,
40
+ dropout=0.0, scaling=attention.scaling, sliding_window=None)
41
+ output = output.reshape(*x.shape[:-1], -1).contiguous()
42
+ result = hidden + attention.o_proj(output)
43
+ result = result + layer.mlp(layer.post_attention_layernorm(result))
44
+ return result, k, v
45
+
46
+
47
+ @dataclass
48
+ class PartitionedResult:
49
+ """Readouts [graph*count, loop, symbol], stop logits or None, final R working."""
50
+ scores: Tensor
51
+ stops: Tensor | None
52
+ working: Tensor
53
+
54
+
55
+ @torch.inference_mode()
56
+ def partitioned_forward(model: nn.Module, prefix_ids: Tensor, suffix_ids: Tensor,
57
+ suffix_mask: Tensor, graph_indices: Tensor,
58
+ token_ids: list[int], loops: int, horizons: Tensor | None = None) -> PartitionedResult:
59
+ """Exact causal factorization, modulo documented FP32 kernel roundoff.
60
+
61
+ Prefix [G,P] has no padding, suffix [B,S] is right padded. Each suffix maps
62
+ to one prefix via [B] graph_indices. Positions match the dense concatenation.
63
+ Supports historical full_sequence/fixed_prompt models, without routed inputs,
64
+ bridges, direct heads or private recurrent controllers. Never modifies model.
65
+ """
66
+ if (model.training or model.router is not None or model.bridge is not None or
67
+ model.state_head is not None or loops < 1):
68
+ raise ValueError('Partitioned path requires an unmodified historical eval model')
69
+ if prefix_ids.ndim != 2 or suffix_ids.shape != suffix_mask.shape or suffix_ids.ndim != 2:
70
+ raise ValueError('Expected prefix and aligned suffix token matrices')
71
+ if (not suffix_mask.any(-1).all() or not torch.equal(suffix_mask.long().cumprod(-1), suffix_mask.long())
72
+ or graph_indices.shape != (len(suffix_ids),) or
73
+ (graph_indices < 0).any() or (graph_indices >= len(prefix_ids)).any()):
74
+ raise ValueError('Need right-padded suffixes and valid graph indices')
75
+ prefix = model.embed_tokens(prefix_ids)
76
+ suffix = model.embed_tokens(suffix_ids)
77
+ device, dtype = prefix.device, prefix.dtype
78
+ p, s = prefix.shape[1], suffix.shape[1]
79
+ pp = torch.arange(p, device=device)[None]
80
+ sp = torch.arange(p, p + s, device=device)[None]
81
+ pe, se = model.rotary_emb(prefix, pp), model.rotary_emb(suffix, sp)
82
+ negative = torch.finfo(dtype).min
83
+ pm = torch.zeros(p, p, device=device, dtype=dtype).masked_fill(
84
+ torch.arange(p, device=device)[None] > torch.arange(p, device=device)[:, None], negative)[None, None]
85
+ keep = torch.cat((torch.ones(len(suffix), p, device=device, dtype=torch.bool), suffix_mask.bool()), -1)
86
+ allowed = (torch.arange(p + s, device=device)[None] <= sp[0, :, None])[None] & keep[:, None, :]
87
+ sm = torch.zeros_like(allowed, dtype=dtype).masked_fill(~allowed, negative)[:, None]
88
+ ends = suffix_mask.sum(-1).long() - 1
89
+ rows = torch.arange(len(suffix), device=device)
90
+ am = sm[rows, :, ends, :].unsqueeze(2)
91
+ ae = tuple(e.expand(len(suffix), -1, -1)[rows, ends, :].unsqueeze(1) for e in se)
92
+
93
+ def block(layers, ph, sh, record=False):
94
+ memory = []
95
+ for layer in layers:
96
+ ph, k, v = layer_compute(layer, ph, pe, pm)
97
+ sh, keys, values = suffix_layer(layer, sh, se, sm,
98
+ (k.index_select(0, graph_indices), v.index_select(0, graph_indices)))
99
+ if record:
100
+ memory.append((keys, values))
101
+ return ph, sh, memory
102
+
103
+ def working_block(layers, state, memory):
104
+ for layer, kv in zip(layers, memory, strict=True):
105
+ state, _, _ = suffix_layer(layer, state, ae, am, kv, ends + p)
106
+ return state
107
+
108
+ prefix, suffix, _ = block(model.prelude.layers, prefix, suffix)
109
+ fixed = model.recurrence_mode == 'fixed_prompt'
110
+ scores, stops = [], []
111
+ original_batch = len(suffix_ids)
112
+ active_rows = torch.arange(original_batch, device=device)
113
+ crossed = torch.zeros(original_batch, device=device, dtype=torch.bool)
114
+ if horizons is not None and (horizons.shape != (original_batch,) or (horizons < 1).any() or (horizons > loops).any()):
115
+ raise ValueError('Requested readout horizons must fit the loop cap')
116
+ r_memory = c_memory = None
117
+ working = None
118
+ selected = model.lm_head.weight[token_ids]
119
+ for loop in range(loops):
120
+ if fixed and loop:
121
+ working = working_block(model.recurrent.layers, working, r_memory)
122
+ decoded = working_block(model.coda.layers, working, c_memory)
123
+ else:
124
+ prefix, suffix, r_memory = block(model.recurrent.layers, prefix, suffix, fixed)
125
+ working = suffix[rows, ends, :].unsqueeze(1)
126
+ _, decoded_suffix, c_memory = block(model.coda.layers, prefix, suffix, fixed)
127
+ decoded = decoded_suffix[rows, ends, :].unsqueeze(1)
128
+ score = nn.functional.linear(model.norm(decoded[:, 0]), selected).float()
129
+ if not torch.isfinite(score).all():
130
+ raise FloatingPointError('Nonfinite partitioned readout')
131
+ full_score = score.new_zeros(original_batch, score.shape[-1])
132
+ full_score[active_rows] = score
133
+ scores.append(full_score)
134
+ if model.completion_head is not None:
135
+ stop = model.completion_head(working[:, 0]).float()
136
+ if not torch.isfinite(stop).all():
137
+ raise FloatingPointError('Nonfinite partitioned completion head')
138
+ full_stop = stop.new_full((original_batch,), float('-inf'))
139
+ full_stop[active_rows] = stop
140
+ stops.append(full_stop)
141
+ crossed[active_rows] |= stop >= 0
142
+ if horizons is not None and loop + 1 < loops:
143
+ need = horizons[active_rows] > loop + 1
144
+ if model.completion_head is not None:
145
+ need |= ~crossed[active_rows]
146
+ # A CUDA boolean index discovers its output shape on the host.
147
+ # Discover the same ordered rows once, then reuse them for every
148
+ # state/cache tensor instead of repeating that synchronization.
149
+ kept = need.nonzero().flatten()
150
+ if not kept.numel():
151
+ for _ in range(loop + 1, loops):
152
+ scores.append(full_score.new_zeros(full_score.shape))
153
+ if stops: stops.append(full_stop.new_full(full_stop.shape, float('-inf')))
154
+ break
155
+ active_rows = active_rows.index_select(0, kept)
156
+ graph_indices = graph_indices.index_select(0, kept)
157
+ suffix = suffix.index_select(0, kept)
158
+ working = working.index_select(0, kept)
159
+ ends = ends.index_select(0, kept)
160
+ rows = torch.arange(len(active_rows), device=device)
161
+ sm = sm.index_select(0, kept); am = am.index_select(0, kept)
162
+ ae = tuple(e.index_select(0, kept) for e in ae)
163
+ if fixed:
164
+ r_memory = [(key.index_select(0, kept), value.index_select(0, kept)) for key, value in r_memory]
165
+ c_memory = [(key.index_select(0, kept), value.index_select(0, kept)) for key, value in c_memory]
166
+ return PartitionedResult(torch.stack(scores, 1), torch.stack(stops, 1) if stops else None, working[:, 0])
scripts/recurrent_qwen/prefix.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Differentiable fixed-prefix execution; one working query after the first pass."""
2
+ import torch
3
+ from torch import Tensor, nn
4
+ from .memory import PromptMemory
5
+ from torch.utils.checkpoint import checkpoint
6
+ from transformers.models.qwen2.modeling_qwen2 import apply_rotary_pos_emb, eager_attention_forward
7
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
8
+
9
+
10
+ def pick(x: Tensor, positions: list[int]) -> Tensor:
11
+ return torch.stack([x[row, position:position + 1] for row, position in enumerate(positions)])
12
+
13
+
14
+ def layer_compute(layer: nn.Module, hidden: Tensor, embeddings: tuple[Tensor, Tensor],
15
+ mask: Tensor | None, positions: list[int] | None = None,
16
+ cached: tuple[Tensor, Tensor] | None = None) -> tuple[Tensor, Tensor, Tensor]:
17
+ """Pure operation so non-reentrant checkpointing preserves prefix gradients."""
18
+ attention = layer.self_attn
19
+ residual = hidden
20
+ x = layer.input_layernorm(hidden)
21
+ shape = (*x.shape[:-1], -1, attention.head_dim)
22
+ q = attention.q_proj(x).view(shape).transpose(1, 2)
23
+ k = attention.k_proj(x).view(shape).transpose(1, 2)
24
+ v = attention.v_proj(x).view(shape).transpose(1, 2)
25
+ q, k = apply_rotary_pos_emb(q, k, *embeddings)
26
+ if cached is not None:
27
+ keys, values = cached
28
+ active = (torch.arange(keys.shape[-2], device=x.device)[None, :] ==
29
+ torch.tensor(positions, device=x.device)[:, None])[:, None, :, None]
30
+ k = torch.where(active, k, keys)
31
+ v = torch.where(active, v, values)
32
+ interface = ALL_ATTENTION_FUNCTIONS.get_interface(attention.config._attn_implementation, eager_attention_forward)
33
+ output, _ = interface(attention, q, k, v, mask,
34
+ dropout=attention.attention_dropout if layer.training else 0.0,
35
+ scaling=attention.scaling, sliding_window=None)
36
+ output = output.reshape(*x.shape[:-1], -1).contiguous()
37
+ hidden = residual + attention.o_proj(output)
38
+ hidden = hidden + layer.mlp(layer.post_attention_layernorm(hidden))
39
+ return hidden, k, v
40
+
41
+
42
+ def fixed_prefix_layer(layer: nn.Module, hidden: Tensor, embeddings: tuple[Tensor, Tensor],
43
+ mask: Tensor | None, memory: PromptMemory, index: int,
44
+ use_checkpoint: bool) -> Tensor:
45
+ """Reuse prefix K/V without detaching it or caching across model forwards."""
46
+ positions = memory.write_mask[..., 0].long().argmax(-1).tolist()
47
+ cached = None
48
+ first = memory.output is None
49
+ if not first:
50
+ hidden = pick(hidden, positions)
51
+ embeddings = tuple(pick(e.expand(memory.write_mask.shape[0], -1, -1), positions) for e in embeddings)
52
+ cached = memory.keys_values[index]
53
+ if mask is None:
54
+ keep = torch.arange(cached[0].shape[-2], device=hidden.device)[None, :] <= torch.tensor(positions, device=hidden.device)[:, None]
55
+ mask = torch.zeros_like(keep, dtype=hidden.dtype).masked_fill(~keep, torch.finfo(hidden.dtype).min)[:, None, None, :]
56
+ else:
57
+ mask = torch.stack([mask[row if mask.shape[0] > 1 else 0, :, pos:pos + 1, :] for row, pos in enumerate(positions)])
58
+ def compute(x, cos, sin, *kv):
59
+ return layer_compute(layer, x, (cos, sin), mask, positions, tuple(kv) if kv else None)
60
+ args = (hidden, *embeddings, *(cached or ()))
61
+ result, keys, values = (checkpoint(compute, *args, use_reentrant=False)
62
+ if use_checkpoint and torch.is_grad_enabled() else compute(*args))
63
+ if first:
64
+ memory.keys_values.append((keys, values))
65
+ memory.layer_outputs.append(result)
66
+ return result
67
+ return torch.where(memory.write_mask, result, memory.layer_outputs[index])
scripts/recurrent_qwen/validation.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage 0 checks shared by the pretrained CLI and a small integration test."""
2
+
3
+ from collections.abc import Callable
4
+ from time import perf_counter
5
+ from typing import Any
6
+
7
+ import torch
8
+ from torch import Tensor
9
+ from torch.nn import functional as F
10
+ from transformers import Qwen2ForCausalLM
11
+
12
+ from .lora_utils import attach_recurrent_lora
13
+ from .model import RecurrentQwen
14
+
15
+
16
+ def validate_stage0(
17
+ base: Qwen2ForCausalLM,
18
+ cases: dict[str, dict[str, Tensor]],
19
+ *,
20
+ recurrent_start: int = 6,
21
+ recurrent_end: int = 18,
22
+ rank: int = 8,
23
+ alpha: int = 16,
24
+ loop_counts: tuple[int, ...] = (1, 2, 4),
25
+ atol: float = 1e-5,
26
+ rtol: float = 1e-5,
27
+ progress: Callable[[str], None] = print,
28
+ ) -> dict[str, Any]:
29
+ """Assert architecture contracts; return serializable measured evidence.
30
+
31
+ Mutates/freezes base and attaches fresh LoRA. No optimizer step is taken.
32
+ The supplied cases must be short, nonempty input_ids/attention_mask pairs;
33
+ equivalence compares every vocabulary logit at every sequence position.
34
+ """
35
+ if not cases or not loop_counts or min(loop_counts) < 1 or max(loop_counts) < 2:
36
+ raise ValueError("Provide input cases and positive loop counts including a depth > 1")
37
+ base.eval()
38
+ base.config.use_cache = False
39
+ originals = tuple(base.parameters())
40
+ original_ids = {id(p) for p in originals}
41
+ original_layers = tuple(base.model.layers)
42
+ progress("Computing ordinary-Qwen reference logits")
43
+ with torch.no_grad():
44
+ references = {name: base(**inputs, use_cache=False).logits for name, inputs in cases.items()}
45
+ model = RecurrentQwen(base, recurrent_start, recurrent_end).eval()
46
+ assert tuple(model.recurrent.layers) == original_layers[recurrent_start:recurrent_end]
47
+ errors = []
48
+ for adapted in (False, True):
49
+ if adapted:
50
+ attach_recurrent_lora(model, rank=rank, alpha=alpha)
51
+ progress(f"Checking T=1 equivalence ({'with' if adapted else 'without'} LoRA)")
52
+ with torch.no_grad():
53
+ for name, inputs in cases.items():
54
+ actual = model(**inputs, logits_mode="all").logits
55
+ expected = references[name]
56
+ torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
57
+ difference = (actual - expected).abs()
58
+ errors.append({
59
+ "case": name,
60
+ "lora": adapted,
61
+ "max_absolute_error": difference.max().item(),
62
+ "mean_absolute_error": difference.mean().item(),
63
+ "logits_shape": list(actual.shape),
64
+ })
65
+ del references
66
+
67
+ progress("Checking shared weights, loop observability, and configurable depth")
68
+ parameters_before = tuple(id(p) for p in model.parameters())
69
+ calls: list[tuple[int, tuple[int, ...], Tensor, Tensor]] = []
70
+ block_calls: list[str] = []
71
+ layer_calls: list[int] = []
72
+ handles = [model.recurrent.register_forward_hook(
73
+ lambda module, args, output: calls.append(
74
+ (id(module), tuple(id(p) for p in module.parameters()), args[0], output)
75
+ )
76
+ )]
77
+ for name in ("prelude", "coda"):
78
+ handles.append(getattr(model, name).register_forward_hook(
79
+ lambda module, args, output, name=name: block_calls.append(name)
80
+ ))
81
+ for layer in model.recurrent.layers:
82
+ handles.append(layer.register_forward_hook(
83
+ lambda module, args, output: layer_calls.append(id(module))
84
+ ))
85
+ inputs = next(iter(cases.values()))
86
+ batch, sequence = inputs["input_ids"].shape
87
+ device = inputs["input_ids"].device
88
+ allowed = inputs["input_ids"].unique()
89
+ if allowed.numel() < 2:
90
+ raise ValueError("Validation inputs must contain at least two distinct token IDs")
91
+ recurrent_ids = tuple(id(p) for p in model.recurrent.parameters())
92
+ trajectories = []
93
+ try:
94
+ with torch.no_grad():
95
+ for depth in loop_counts:
96
+ calls.clear()
97
+ block_calls.clear()
98
+ layer_calls.clear()
99
+ labels = allowed[torch.arange(depth, device=device) % allowed.numel()].expand(batch, -1)
100
+ start = perf_counter()
101
+ output = model(**inputs, num_loops=depth, return_hidden_states=True,
102
+ labels=labels, allowed_token_ids=allowed)
103
+ if device.type == "mps":
104
+ torch.mps.synchronize()
105
+ seconds = perf_counter() - start
106
+ assert len(calls) == depth
107
+ assert block_calls.count("prelude") == 1 and block_calls.count("coda") == depth
108
+ assert layer_calls == [id(layer) for layer in model.recurrent.layers] * depth
109
+ assert all(call[0] == id(model.recurrent) and call[1] == recurrent_ids for call in calls)
110
+ assert tuple(id(p) for p in model.parameters()) == parameters_before
111
+ assert calls[0][2] is output.initial_hidden_state
112
+ assert all(calls[t][2] is calls[t - 1][3] for t in range(1, depth))
113
+ assert len(output.hidden_states) == len(output.loop_logits) == depth
114
+ assert output.margins.shape == (batch, depth)
115
+ for hidden, logits in zip(output.hidden_states, output.loop_logits):
116
+ assert hidden.shape == (batch, sequence, model.config.hidden_size)
117
+ assert logits.shape == (batch, model.config.vocab_size)
118
+ assert torch.isfinite(hidden).all() and torch.isfinite(logits).all()
119
+ assert torch.isfinite(output.margins).all()
120
+ trajectories.append({
121
+ "num_loops": depth,
122
+ "seconds": seconds,
123
+ "recurrent_calls": len(calls),
124
+ "recurrent_layer_calls": len(layer_calls),
125
+ "prelude_calls": block_calls.count("prelude"),
126
+ "coda_calls": block_calls.count("coda"),
127
+ "predicted_token_ids": [scores.argmax(-1).tolist() for scores in output.loop_logits],
128
+ "diagnostic_labels": labels.tolist(),
129
+ "allowed_token_ids": allowed.tolist(),
130
+ "margins": output.margins.tolist(),
131
+ })
132
+ finally:
133
+ for handle in handles:
134
+ handle.remove()
135
+ calls.clear()
136
+ del output
137
+
138
+ progress("Checking two-loop backward through the frozen coda (no optimizer step)")
139
+ model.train()
140
+ model.zero_grad(set_to_none=True)
141
+ start = perf_counter()
142
+ output = model(**inputs, num_loops=2, return_hidden_states=True)
143
+ for hidden in output.hidden_states:
144
+ hidden.retain_grad()
145
+ # Arbitrary token labels test graph connectivity, not task competence.
146
+ loss = F.cross_entropy(output.logits.float(), inputs["input_ids"][:, 0])
147
+ loss.backward()
148
+ if device.type == "mps":
149
+ torch.mps.synchronize()
150
+ backward_seconds = perf_counter() - start
151
+ assert all(not p.requires_grad and p.grad is None for p in originals)
152
+ trainable = [(name, p) for name, p in model.named_parameters() if p.requires_grad]
153
+ assert len(trainable) == (recurrent_end - recurrent_start) * 2 * 2
154
+ assert {id(p) for p in model.parameters()} - original_ids == {id(p) for _, p in trainable}
155
+ gradients = {}
156
+ for name, parameter in trainable:
157
+ assert name.startswith("recurrent.") and (".lora_A." in name or ".lora_B." in name)
158
+ assert parameter.grad is not None and torch.isfinite(parameter.grad).all(), name
159
+ norm = parameter.grad.float().norm().item()
160
+ if ".lora_B." in name:
161
+ assert norm > 0, name
162
+ gradients[name] = {"elements": parameter.numel(), "gradient_l2": norm}
163
+ hidden_gradients = [hidden.grad.float().norm().item() for hidden in output.hidden_states]
164
+ assert all(value > 0 and torch.isfinite(torch.tensor(value)) for value in hidden_gradients)
165
+ result = {
166
+ "gate_passed": True,
167
+ "checks": {name: "PASS" for name in (
168
+ "t1_equivalence", "shared_weights", "gradient_scope", "observability", "configurable_depth"
169
+ )},
170
+ "split": {"prelude": [0, recurrent_start], "recurrent": [recurrent_start, recurrent_end],
171
+ "coda": [recurrent_end, len(original_layers)], "end_indices_exclusive": True},
172
+ "lora": {"rank": rank, "alpha": alpha, "targets": ["q_proj", "v_proj"], "dropout": 0.0},
173
+ "bridge": None,
174
+ "tolerance": {"atol": atol, "rtol": rtol},
175
+ "equivalence": errors,
176
+ "trajectories": trajectories,
177
+ "parameters": {"original": sum(p.numel() for p in originals),
178
+ "trainable": sum(p.numel() for _, p in trainable),
179
+ "trainable_tensors": len(trainable)},
180
+ "gradient_check": {"loss": loss.item(), "forward_backward_seconds": backward_seconds,
181
+ "hidden_gradient_l2": hidden_gradients, "parameters": gradients,
182
+ "frozen_parameters_with_grad": 0, "optimizer_steps": 0},
183
+ }
184
+ model.zero_grad(set_to_none=True)
185
+ return result
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fd169731d2cbde95e10bf356d66d5997fd885dd8dbb6fb4684da3f23b2585d8
3
+ size 11421892
tokenizer_config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<|im_start|>",
10
+ "<|im_end|>",
11
+ "<|object_ref_start|>",
12
+ "<|object_ref_end|>",
13
+ "<|box_start|>",
14
+ "<|box_end|>",
15
+ "<|quad_start|>",
16
+ "<|quad_end|>",
17
+ "<|vision_start|>",
18
+ "<|vision_end|>",
19
+ "<|vision_pad|>",
20
+ "<|image_pad|>",
21
+ "<|video_pad|>"
22
+ ],
23
+ "is_local": false,
24
+ "local_files_only": true,
25
+ "model_max_length": 131072,
26
+ "pad_token": "<|endoftext|>",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null
30
+ }