topoexploit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topoexploit-0.1.0.dist-info/METADATA +523 -0
- topoexploit-0.1.0.dist-info/RECORD +23 -0
- topoexploit-0.1.0.dist-info/WHEEL +5 -0
- topoexploit-0.1.0.dist-info/entry_points.txt +6 -0
- topoexploit-0.1.0.dist-info/licenses/LICENSE +661 -0
- topoexploit-0.1.0.dist-info/top_level.txt +1 -0
- topogpt3/__init__.py +125 -0
- topogpt3/__main__.py +49 -0
- topogpt3/api_server.py +1178 -0
- topogpt3/continuation.py +114 -0
- topogpt3/ewc.py +369 -0
- topogpt3/exploitgym_config.py +130 -0
- topogpt3/exploitgym_loader.py +416 -0
- topogpt3/hodge_cm.py +160 -0
- topogpt3/inference.py +731 -0
- topogpt3/inference_hrm.py +1502 -0
- topogpt3/jlens.py +958 -0
- topogpt3/lens_model.py +389 -0
- topogpt3/merged_config.py +187 -0
- topogpt3/model.py +3824 -0
- topogpt3/structured_output.py +251 -0
- topogpt3/train.py +1939 -0
- topogpt3/vanilla_control.py +240 -0
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""TopoGPT3.1: Hierarchical Recursive Reasoning Inference Engine.
|
|
3
|
+
|
|
4
|
+
This module extends TopoGPT3 with a parameter-free hierarchical recursive
|
|
5
|
+
reasoning pipeline inspired by:
|
|
6
|
+
|
|
7
|
+
* Hierarchical Reasoning Model (HRM), Sapient Intelligence: a biologically
|
|
8
|
+
motivated two-speed architecture with a slow high-level loop and a fast
|
|
9
|
+
low-level loop.
|
|
10
|
+
* Tiny Recursive Model (TRM) and Generative Recursive Reasoning Models
|
|
11
|
+
(GRAM): latent-space recurrence that iterates token vectors until they
|
|
12
|
+
reach an attractor before projecting them outward.
|
|
13
|
+
|
|
14
|
+
The pipeline is intentionally built so that the underlying TopoGPT2 weight
|
|
15
|
+
matrices remain bit-identical to those produced by the TopoGPT3 trainer. No
|
|
16
|
+
new learnable parameters are introduced. The pretrained transformer layers
|
|
17
|
+
are repurposed as the recurrent step function of a hierarchical fixed-point
|
|
18
|
+
iteration whose halting condition is the empirical stabilization of the
|
|
19
|
+
latent state.
|
|
20
|
+
|
|
21
|
+
The high-level slow state is persisted across multiple emitted tokens to
|
|
22
|
+
achieve sparse temporal reasoning: the full network is iterated only at
|
|
23
|
+
configurable intervals, while a short suffix of layers refines the low-level
|
|
24
|
+
state at every emitted token.
|
|
25
|
+
|
|
26
|
+
All configurable values reside in dedicated configuration dataclasses; no
|
|
27
|
+
magic numbers or hardcoded constants are embedded in business logic. Path
|
|
28
|
+
resolution rejects traversal escapes. State dict loading defers strictness
|
|
29
|
+
to settings, so an architecturally aligned TopoGPT3 checkpoint loads
|
|
30
|
+
unchanged.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
|
|
34
|
+
python3 inference_hrm.py --prompt "def fibonacci(" --hrm-h-iters 4 \\
|
|
35
|
+
--hrm-l-iters 6 --hrm-l-window 3 --max-new 256
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import logging
|
|
41
|
+
import sys
|
|
42
|
+
import time
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
46
|
+
|
|
47
|
+
import torch
|
|
48
|
+
import torch.nn.functional as F
|
|
49
|
+
from safetensors import safe_open
|
|
50
|
+
from safetensors.torch import load_file
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class ScalePreset:
|
|
55
|
+
"""Immutable architecture preset for a named model scale."""
|
|
56
|
+
|
|
57
|
+
d_model: int
|
|
58
|
+
n_heads: int
|
|
59
|
+
n_layers: int
|
|
60
|
+
max_seq_len: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class RecursiveReasoningConfig:
|
|
65
|
+
"""Hyperparameters governing the hierarchical recursive thinking loop.
|
|
66
|
+
|
|
67
|
+
The semantics follow the HRM and GRAM literature, adapted to operate
|
|
68
|
+
safely with zero additional learnable parameters on a model that was
|
|
69
|
+
not trained with recurrence in its computational graph. The reasoner
|
|
70
|
+
performs damped fixed-point iteration entirely in the residual-stream
|
|
71
|
+
space produced by the baseline forward pass; deep activations are never
|
|
72
|
+
fed back into the token-embedding-input layers, preserving the trained
|
|
73
|
+
activation distribution at every layer boundary.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
enabled: master switch; when False the pipeline degrades to the
|
|
77
|
+
standard non-recursive autoregressive loop.
|
|
78
|
+
max_high_level_iters: maximum slow-loop iterations per emitted token.
|
|
79
|
+
Each iteration applies a deeper trailing window of layers.
|
|
80
|
+
max_low_level_iters: maximum fast-loop iterations per high-level step.
|
|
81
|
+
Each iteration applies the short trailing window of layers.
|
|
82
|
+
low_level_window: number of trailing transformer layers iterated by
|
|
83
|
+
the low-level fast loop.
|
|
84
|
+
high_level_window: number of trailing transformer layers iterated by
|
|
85
|
+
the high-level slow loop. Should be greater than or equal to
|
|
86
|
+
low_level_window so the hierarchy matches the HRM coarse/fine
|
|
87
|
+
split.
|
|
88
|
+
low_level_step: damping coefficient in [0, 1] for the low-level
|
|
89
|
+
update rule z <- z + step * (window(z) - z).
|
|
90
|
+
high_level_step: damping coefficient for the high-level update.
|
|
91
|
+
attractor_low_epsilon: relative L2 change threshold that declares the
|
|
92
|
+
low-level state converged.
|
|
93
|
+
attractor_high_epsilon: relative L2 change threshold that declares the
|
|
94
|
+
high-level state converged.
|
|
95
|
+
high_level_persist_tokens: tokens during which the refinement vector
|
|
96
|
+
is reused as a warm start before being re-initialized to zero.
|
|
97
|
+
This is the sparse temporal-memory dimension.
|
|
98
|
+
cache_warm_start_weight: scalar in [0, 1] applied to the cached
|
|
99
|
+
refinement before warm-starting the next token's iteration.
|
|
100
|
+
max_drift_relative: relative L2 distance ceiling between the iterated
|
|
101
|
+
latent and the baseline latent; exceeding it triggers a reset to
|
|
102
|
+
the baseline state and aborts thinking for the current token.
|
|
103
|
+
latent_change_eps: floor used in the denominator of relative change
|
|
104
|
+
computations to avoid division by zero.
|
|
105
|
+
safety_max_total_iterations: hard cap on total layer invocations per
|
|
106
|
+
emitted token regardless of configured iters.
|
|
107
|
+
minimum_low_level_iters: floor on low-level iterations before
|
|
108
|
+
convergence checks may halt the loop.
|
|
109
|
+
minimum_high_level_iters: floor on high-level iterations before
|
|
110
|
+
convergence checks may halt the loop.
|
|
111
|
+
diagnostic_logging: when True, emits per-token iteration statistics.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
enabled: bool = True
|
|
115
|
+
max_high_level_iters: int = 0
|
|
116
|
+
max_low_level_iters: int = 0
|
|
117
|
+
low_level_window: int = 2
|
|
118
|
+
high_level_window: int = 4
|
|
119
|
+
low_level_step: float = 0.1
|
|
120
|
+
high_level_step: float = 0.1
|
|
121
|
+
attractor_low_epsilon: float = 5e-3
|
|
122
|
+
attractor_high_epsilon: float = 1e-2
|
|
123
|
+
high_level_persist_tokens: int = 4
|
|
124
|
+
cache_warm_start_weight: float = 0.5
|
|
125
|
+
max_drift_relative: float = 0.3
|
|
126
|
+
latent_change_eps: float = 1e-8
|
|
127
|
+
safety_max_total_iterations: int = 256
|
|
128
|
+
minimum_low_level_iters: int = 0
|
|
129
|
+
minimum_high_level_iters: int = 0
|
|
130
|
+
diagnostic_logging: bool = False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class HRMInferenceSettings:
|
|
135
|
+
"""Centralized configuration for the TopoGPT3.1 inference pipeline.
|
|
136
|
+
|
|
137
|
+
Every value consumed downstream resides here. Extending the pipeline with
|
|
138
|
+
a new tunable means extending this dataclass; no other module should
|
|
139
|
+
embed literals.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
model_scale: str = "small"
|
|
143
|
+
device: str = field(
|
|
144
|
+
default_factory=lambda: "cuda" if torch.cuda.is_available() else "cpu"
|
|
145
|
+
)
|
|
146
|
+
seed: int = 42
|
|
147
|
+
|
|
148
|
+
checkpoint_dir: str = "checkpoints_topogpt3"
|
|
149
|
+
checkpoint_name: str = "last"
|
|
150
|
+
model_filename: str = "model.safetensors"
|
|
151
|
+
state_filename: str = "state.json"
|
|
152
|
+
|
|
153
|
+
topogpt31_source_filename: str = "inference_hrm.py"
|
|
154
|
+
topogpt3_source_filename: str = "train.py"
|
|
155
|
+
topogpt2_source_filename: str = "model.py"
|
|
156
|
+
source_directory: str = "."
|
|
157
|
+
|
|
158
|
+
tokenizer_encoding: str = "gpt2"
|
|
159
|
+
weight_probe_key: str = "layers.0.attn.k_proj.weight"
|
|
160
|
+
|
|
161
|
+
apply_gauss_patch_at_load: bool = True
|
|
162
|
+
strict_state_dict: bool = False
|
|
163
|
+
|
|
164
|
+
prompt: str = "def main("
|
|
165
|
+
max_new_tokens: int = 256
|
|
166
|
+
temperature: float = 0.3
|
|
167
|
+
top_k: int = 50
|
|
168
|
+
repetition_penalty: float = 1.1
|
|
169
|
+
end_of_text_token_id: int = 50256
|
|
170
|
+
auto_continue: bool = False
|
|
171
|
+
max_continuations: int = 3
|
|
172
|
+
continuation_tail_lines: int = 2
|
|
173
|
+
thinking_mode: bool = False
|
|
174
|
+
|
|
175
|
+
reasoning: RecursiveReasoningConfig = field(
|
|
176
|
+
default_factory=RecursiveReasoningConfig
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
log_level: str = "INFO"
|
|
180
|
+
log_name: str = "TopoGPT3.1Inference"
|
|
181
|
+
log_format: str = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
|
182
|
+
log_date_format: str = "%Y-%m-%d %H:%M:%S"
|
|
183
|
+
|
|
184
|
+
result_separator_char: str = "="
|
|
185
|
+
result_separator_width: int = 72
|
|
186
|
+
prompt_label: str = "PROMPT"
|
|
187
|
+
completion_label: str = "COMPLETION"
|
|
188
|
+
reasoning_label: str = "REASONING_STATS"
|
|
189
|
+
elapsed_floor_seconds: float = 1e-3
|
|
190
|
+
|
|
191
|
+
min_max_new_tokens: int = 1
|
|
192
|
+
max_max_new_tokens: int = 8192
|
|
193
|
+
min_temperature: float = 1e-3
|
|
194
|
+
max_temperature: float = 5.0
|
|
195
|
+
min_top_k: int = 0
|
|
196
|
+
min_repetition_penalty: float = 0.5
|
|
197
|
+
max_repetition_penalty: float = 5.0
|
|
198
|
+
max_checkpoint_bytes: int = 50 * (1024 ** 3)
|
|
199
|
+
min_checkpoint_bytes: int = 1024
|
|
200
|
+
|
|
201
|
+
min_hrm_h_iters: int = 0
|
|
202
|
+
max_hrm_h_iters: int = 32
|
|
203
|
+
min_hrm_l_iters: int = 0
|
|
204
|
+
max_hrm_l_iters: int = 64
|
|
205
|
+
min_hrm_l_window: int = 1
|
|
206
|
+
max_hrm_l_window: int = 64
|
|
207
|
+
min_hrm_h_window: int = 1
|
|
208
|
+
max_hrm_h_window: int = 64
|
|
209
|
+
min_hrm_persist: int = 1
|
|
210
|
+
max_hrm_persist: int = 256
|
|
211
|
+
min_hrm_step: float = 0.0
|
|
212
|
+
max_hrm_step: float = 1.0
|
|
213
|
+
min_hrm_warm_start: float = 0.0
|
|
214
|
+
max_hrm_warm_start: float = 1.0
|
|
215
|
+
min_hrm_drift: float = 0.0
|
|
216
|
+
max_hrm_drift: float = 10.0
|
|
217
|
+
|
|
218
|
+
runtime_module_alias: str = "topogpt3_runtime_hrm"
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def scale_presets() -> Mapping[str, ScalePreset]:
|
|
222
|
+
"""Return the architecture preset table indexed by scale name."""
|
|
223
|
+
return {
|
|
224
|
+
"micro": ScalePreset(d_model=64, n_heads=4, n_layers=2,
|
|
225
|
+
max_seq_len=128),
|
|
226
|
+
"small": ScalePreset(d_model=256, n_heads=8, n_layers=6,
|
|
227
|
+
max_seq_len=256),
|
|
228
|
+
"medium": ScalePreset(d_model=512, n_heads=8, n_layers=12,
|
|
229
|
+
max_seq_len=512),
|
|
230
|
+
"gpt2": ScalePreset(d_model=768, n_heads=12, n_layers=12,
|
|
231
|
+
max_seq_len=1024),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def preset(self) -> ScalePreset:
|
|
235
|
+
"""Return the resolved preset for the configured model scale."""
|
|
236
|
+
table = self.scale_presets()
|
|
237
|
+
if self.model_scale not in table:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"Unknown model scale '{self.model_scale}'. "
|
|
240
|
+
f"Allowed: {sorted(table.keys())}"
|
|
241
|
+
)
|
|
242
|
+
return table[self.model_scale]
|
|
243
|
+
|
|
244
|
+
def validate(self) -> None:
|
|
245
|
+
"""Raise ValueError if any setting falls outside its safety bounds."""
|
|
246
|
+
if not (self.min_max_new_tokens
|
|
247
|
+
<= self.max_new_tokens <= self.max_max_new_tokens):
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"max_new_tokens={self.max_new_tokens} outside "
|
|
250
|
+
f"[{self.min_max_new_tokens}, {self.max_max_new_tokens}]"
|
|
251
|
+
)
|
|
252
|
+
if not (self.min_temperature
|
|
253
|
+
<= self.temperature <= self.max_temperature):
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"temperature={self.temperature} outside "
|
|
256
|
+
f"[{self.min_temperature}, {self.max_temperature}]"
|
|
257
|
+
)
|
|
258
|
+
if self.top_k < self.min_top_k:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"top_k={self.top_k} must be >= {self.min_top_k}"
|
|
261
|
+
)
|
|
262
|
+
if not (self.min_repetition_penalty
|
|
263
|
+
<= self.repetition_penalty <= self.max_repetition_penalty):
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"repetition_penalty={self.repetition_penalty} outside "
|
|
266
|
+
f"[{self.min_repetition_penalty}, "
|
|
267
|
+
f"{self.max_repetition_penalty}]"
|
|
268
|
+
)
|
|
269
|
+
if self.result_separator_width <= 0:
|
|
270
|
+
raise ValueError("result_separator_width must be positive")
|
|
271
|
+
if self.elapsed_floor_seconds <= 0:
|
|
272
|
+
raise ValueError("elapsed_floor_seconds must be positive")
|
|
273
|
+
r = self.reasoning
|
|
274
|
+
if not (self.min_hrm_h_iters
|
|
275
|
+
<= r.max_high_level_iters <= self.max_hrm_h_iters):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"max_high_level_iters={r.max_high_level_iters} outside "
|
|
278
|
+
f"[{self.min_hrm_h_iters}, {self.max_hrm_h_iters}]"
|
|
279
|
+
)
|
|
280
|
+
if not (self.min_hrm_l_iters
|
|
281
|
+
<= r.max_low_level_iters <= self.max_hrm_l_iters):
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"max_low_level_iters={r.max_low_level_iters} outside "
|
|
284
|
+
f"[{self.min_hrm_l_iters}, {self.max_hrm_l_iters}]"
|
|
285
|
+
)
|
|
286
|
+
if not (self.min_hrm_l_window
|
|
287
|
+
<= r.low_level_window <= self.max_hrm_l_window):
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"low_level_window={r.low_level_window} outside "
|
|
290
|
+
f"[{self.min_hrm_l_window}, {self.max_hrm_l_window}]"
|
|
291
|
+
)
|
|
292
|
+
if not (self.min_hrm_h_window
|
|
293
|
+
<= r.high_level_window <= self.max_hrm_h_window):
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"high_level_window={r.high_level_window} outside "
|
|
296
|
+
f"[{self.min_hrm_h_window}, {self.max_hrm_h_window}]"
|
|
297
|
+
)
|
|
298
|
+
if r.high_level_window < r.low_level_window:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"high_level_window={r.high_level_window} must be >= "
|
|
301
|
+
f"low_level_window={r.low_level_window}"
|
|
302
|
+
)
|
|
303
|
+
if not (self.min_hrm_persist
|
|
304
|
+
<= r.high_level_persist_tokens <= self.max_hrm_persist):
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"high_level_persist_tokens={r.high_level_persist_tokens} "
|
|
307
|
+
f"outside [{self.min_hrm_persist}, {self.max_hrm_persist}]"
|
|
308
|
+
)
|
|
309
|
+
if not (self.min_hrm_step <= r.low_level_step <= self.max_hrm_step):
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"low_level_step={r.low_level_step} outside "
|
|
312
|
+
f"[{self.min_hrm_step}, {self.max_hrm_step}]"
|
|
313
|
+
)
|
|
314
|
+
if not (self.min_hrm_step <= r.high_level_step <= self.max_hrm_step):
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"high_level_step={r.high_level_step} outside "
|
|
317
|
+
f"[{self.min_hrm_step}, {self.max_hrm_step}]"
|
|
318
|
+
)
|
|
319
|
+
if not (self.min_hrm_warm_start
|
|
320
|
+
<= r.cache_warm_start_weight <= self.max_hrm_warm_start):
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"cache_warm_start_weight={r.cache_warm_start_weight} "
|
|
323
|
+
f"outside [{self.min_hrm_warm_start}, "
|
|
324
|
+
f"{self.max_hrm_warm_start}]"
|
|
325
|
+
)
|
|
326
|
+
if not (self.min_hrm_drift
|
|
327
|
+
<= r.max_drift_relative <= self.max_hrm_drift):
|
|
328
|
+
raise ValueError(
|
|
329
|
+
f"max_drift_relative={r.max_drift_relative} outside "
|
|
330
|
+
f"[{self.min_hrm_drift}, {self.max_hrm_drift}]"
|
|
331
|
+
)
|
|
332
|
+
if r.attractor_low_epsilon <= 0 or r.attractor_high_epsilon <= 0:
|
|
333
|
+
raise ValueError("attractor epsilons must be positive")
|
|
334
|
+
if r.latent_change_eps <= 0:
|
|
335
|
+
raise ValueError("latent_change_eps must be positive")
|
|
336
|
+
if r.safety_max_total_iterations < 1:
|
|
337
|
+
raise ValueError("safety_max_total_iterations must be >= 1")
|
|
338
|
+
if r.minimum_low_level_iters < 0 or r.minimum_high_level_iters < 0:
|
|
339
|
+
raise ValueError("minimum iteration floors must be non-negative")
|
|
340
|
+
self.preset()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class HRMLoggerFactory:
|
|
344
|
+
"""Builds a stdout-attached logger from inference settings."""
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def build(settings: HRMInferenceSettings) -> logging.Logger:
|
|
348
|
+
"""Return a configured Logger with a single deduplicated stdout handler."""
|
|
349
|
+
logger = logging.getLogger(settings.log_name)
|
|
350
|
+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
351
|
+
logger.setLevel(level)
|
|
352
|
+
if not logger.handlers:
|
|
353
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
354
|
+
handler.setFormatter(logging.Formatter(
|
|
355
|
+
fmt=settings.log_format,
|
|
356
|
+
datefmt=settings.log_date_format,
|
|
357
|
+
))
|
|
358
|
+
logger.addHandler(handler)
|
|
359
|
+
logger.propagate = False
|
|
360
|
+
return logger
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class SecurePathResolver:
|
|
364
|
+
"""Resolves filesystem paths while rejecting traversal outside their root."""
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def resolve_under(root: Path, *parts: str) -> Path:
|
|
368
|
+
"""Join parts under root and return the canonical resolved path.
|
|
369
|
+
|
|
370
|
+
Raises ValueError if the resolved path escapes root.
|
|
371
|
+
"""
|
|
372
|
+
resolved_root = root.resolve()
|
|
373
|
+
candidate = resolved_root.joinpath(*parts).resolve()
|
|
374
|
+
try:
|
|
375
|
+
candidate.relative_to(resolved_root)
|
|
376
|
+
except ValueError as exc:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f"Path '{candidate}' escapes root '{resolved_root}'."
|
|
379
|
+
) from exc
|
|
380
|
+
return candidate
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def require_existing_file(path: Path,
|
|
384
|
+
expected_suffix: Optional[str] = None) -> Path:
|
|
385
|
+
"""Validate path points to an existing regular file with the expected suffix."""
|
|
386
|
+
if not path.exists():
|
|
387
|
+
raise FileNotFoundError(f"Required file not found: {path}")
|
|
388
|
+
if not path.is_file():
|
|
389
|
+
raise ValueError(f"Path is not a regular file: {path}")
|
|
390
|
+
if expected_suffix is not None and path.suffix != expected_suffix:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Expected suffix '{expected_suffix}', got '{path.suffix}': {path}"
|
|
393
|
+
)
|
|
394
|
+
return path
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class SourceModuleLoader:
|
|
398
|
+
"""Resolves the TopoGPT3 runtime module via the package import system."""
|
|
399
|
+
|
|
400
|
+
def __init__(self, settings: HRMInferenceSettings, logger: logging.Logger):
|
|
401
|
+
self._settings = settings
|
|
402
|
+
self._logger = logger
|
|
403
|
+
|
|
404
|
+
def load(self) -> Any:
|
|
405
|
+
"""Return the topogpt3.train module which re-exports model symbols."""
|
|
406
|
+
from . import train as runtime_module
|
|
407
|
+
self._logger.info(
|
|
408
|
+
"Loaded TopoGPT3 runtime module: %s", runtime_module.__name__,
|
|
409
|
+
)
|
|
410
|
+
return runtime_module
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class CheckpointPaths:
|
|
414
|
+
"""Computes and validates checkpoint file paths under a single root."""
|
|
415
|
+
|
|
416
|
+
def __init__(self, settings: HRMInferenceSettings):
|
|
417
|
+
self._settings = settings
|
|
418
|
+
self._root = Path(settings.checkpoint_dir).resolve()
|
|
419
|
+
self._slot = SecurePathResolver.resolve_under(
|
|
420
|
+
self._root, settings.checkpoint_name,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def slot_dir(self) -> Path:
|
|
425
|
+
"""Directory holding the active checkpoint slot."""
|
|
426
|
+
return self._slot
|
|
427
|
+
|
|
428
|
+
def model_file(self) -> Path:
|
|
429
|
+
"""Resolved path to the safetensors weights file inside the slot."""
|
|
430
|
+
return SecurePathResolver.resolve_under(
|
|
431
|
+
self._slot, self._settings.model_filename,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def state_file(self) -> Path:
|
|
435
|
+
"""Resolved path to the JSON training-state file inside the slot."""
|
|
436
|
+
return SecurePathResolver.resolve_under(
|
|
437
|
+
self._slot, self._settings.state_filename,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def assert_ready(self) -> None:
|
|
441
|
+
"""Verify weights exist and the on-disk size lies within safety bounds."""
|
|
442
|
+
weights = self.model_file()
|
|
443
|
+
SecurePathResolver.require_existing_file(
|
|
444
|
+
weights, expected_suffix=Path(self._settings.model_filename).suffix,
|
|
445
|
+
)
|
|
446
|
+
size_bytes = weights.stat().st_size
|
|
447
|
+
if size_bytes < self._settings.min_checkpoint_bytes:
|
|
448
|
+
raise ValueError(
|
|
449
|
+
f"Checkpoint too small ({size_bytes} bytes): {weights}"
|
|
450
|
+
)
|
|
451
|
+
if size_bytes > self._settings.max_checkpoint_bytes:
|
|
452
|
+
raise ValueError(
|
|
453
|
+
f"Checkpoint exceeds allowed size "
|
|
454
|
+
f"({size_bytes} > {self._settings.max_checkpoint_bytes} bytes): "
|
|
455
|
+
f"{weights}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class WeightShapeProbe:
|
|
460
|
+
"""Reads tensor metadata from safetensors to infer architecture details."""
|
|
461
|
+
|
|
462
|
+
def __init__(self, settings: HRMInferenceSettings, logger: logging.Logger):
|
|
463
|
+
self._settings = settings
|
|
464
|
+
self._logger = logger
|
|
465
|
+
|
|
466
|
+
def detect_n_kv_heads(self, weights_path: Path,
|
|
467
|
+
d_model: int, n_heads: int) -> Optional[int]:
|
|
468
|
+
"""Recover N_KV_HEADS used at training by inspecting the k_proj shape.
|
|
469
|
+
|
|
470
|
+
Returns None when the probe key is absent, signalling the caller to
|
|
471
|
+
fall back to scale defaults rather than guess.
|
|
472
|
+
"""
|
|
473
|
+
if n_heads <= 0 or d_model <= 0:
|
|
474
|
+
raise ValueError("d_model and n_heads must be positive")
|
|
475
|
+
if d_model % n_heads != 0:
|
|
476
|
+
raise ValueError("d_model must be divisible by n_heads")
|
|
477
|
+
d_head = d_model // n_heads
|
|
478
|
+
|
|
479
|
+
probe_key = self._settings.weight_probe_key
|
|
480
|
+
with safe_open(str(weights_path), framework="pt", device="cpu") as handle:
|
|
481
|
+
keys = set(handle.keys())
|
|
482
|
+
if probe_key not in keys:
|
|
483
|
+
self._logger.warning(
|
|
484
|
+
"Probe key '%s' missing from checkpoint; "
|
|
485
|
+
"falling back to scale defaults.", probe_key,
|
|
486
|
+
)
|
|
487
|
+
return None
|
|
488
|
+
tensor = handle.get_tensor(probe_key)
|
|
489
|
+
k_dim = int(tensor.shape[0])
|
|
490
|
+
if k_dim % d_head != 0:
|
|
491
|
+
raise ValueError(
|
|
492
|
+
f"k_proj output dim ({k_dim}) is not divisible by d_head ({d_head})"
|
|
493
|
+
)
|
|
494
|
+
n_kv = k_dim // d_head
|
|
495
|
+
self._logger.info(
|
|
496
|
+
"Detected N_KV_HEADS=%d (k_dim=%d, d_head=%d).",
|
|
497
|
+
n_kv, k_dim, d_head,
|
|
498
|
+
)
|
|
499
|
+
return n_kv
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class TopoGPT2ConfigAligner:
|
|
503
|
+
"""Builds a TopoGPT2Config matching the loaded checkpoint and tokenizer."""
|
|
504
|
+
|
|
505
|
+
def __init__(self, settings: HRMInferenceSettings, source_module: Any,
|
|
506
|
+
logger: logging.Logger):
|
|
507
|
+
self._settings = settings
|
|
508
|
+
self._source = source_module
|
|
509
|
+
self._logger = logger
|
|
510
|
+
|
|
511
|
+
def build(self, n_kv_heads: Optional[int], vocab_size: int) -> Any:
|
|
512
|
+
"""Return a TopoGPT2Config dataclass ready to instantiate the model."""
|
|
513
|
+
cfg_cls = getattr(self._source, "TopoGPT2Config")
|
|
514
|
+
kwargs: Dict[str, Any] = {
|
|
515
|
+
"SCALE": self._settings.model_scale,
|
|
516
|
+
"DEVICE": self._settings.device,
|
|
517
|
+
"RANDOM_SEED": self._settings.seed,
|
|
518
|
+
"CHECKPOINT_DIR": self._settings.checkpoint_dir,
|
|
519
|
+
}
|
|
520
|
+
if n_kv_heads is not None and n_kv_heads > 0:
|
|
521
|
+
kwargs["N_KV_HEADS"] = n_kv_heads
|
|
522
|
+
cfg = cfg_cls(**kwargs)
|
|
523
|
+
cfg.VOCAB_SIZE = vocab_size
|
|
524
|
+
self._logger.info(
|
|
525
|
+
"Aligned TopoGPT2Config: scale=%s d_model=%d n_heads=%d "
|
|
526
|
+
"n_layers=%d n_kv_heads=%d vocab=%d max_seq=%d device=%s",
|
|
527
|
+
cfg.SCALE, cfg.D_MODEL, cfg.N_HEADS, cfg.N_LAYERS,
|
|
528
|
+
cfg.N_KV_HEADS, cfg.VOCAB_SIZE, cfg.MAX_SEQ_LEN, cfg.DEVICE,
|
|
529
|
+
)
|
|
530
|
+
return cfg
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class TokenizerFactory:
|
|
534
|
+
"""Builds a BPETokenizer instance using the configured encoding."""
|
|
535
|
+
|
|
536
|
+
def __init__(self, settings: HRMInferenceSettings, source_module: Any):
|
|
537
|
+
self._settings = settings
|
|
538
|
+
self._source = source_module
|
|
539
|
+
|
|
540
|
+
def build(self) -> Any:
|
|
541
|
+
"""Return an instance of BPETokenizer bound to the configured encoding."""
|
|
542
|
+
tokenizer_cls = getattr(self._source, "BPETokenizer")
|
|
543
|
+
return tokenizer_cls(self._settings.tokenizer_encoding)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class GaussPatchApplier:
|
|
547
|
+
"""Applies the idempotent Gauss complex-multiply patch when enabled."""
|
|
548
|
+
|
|
549
|
+
def __init__(self, settings: HRMInferenceSettings, source_module: Any,
|
|
550
|
+
logger: logging.Logger):
|
|
551
|
+
self._settings = settings
|
|
552
|
+
self._source = source_module
|
|
553
|
+
self._logger = logger
|
|
554
|
+
|
|
555
|
+
def apply_if_enabled(self) -> None:
|
|
556
|
+
"""Patch QuaternionSpectralLayer to use the 3-multiply Gauss contract."""
|
|
557
|
+
if not self._settings.apply_gauss_patch_at_load:
|
|
558
|
+
self._logger.info("Gauss complex-multiply patch disabled by settings.")
|
|
559
|
+
return
|
|
560
|
+
patch_fn = getattr(self._source, "apply_gauss_patch")
|
|
561
|
+
patch_fn(self._logger)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class ModelAssembler:
|
|
565
|
+
"""Instantiates the model and loads weights from safetensors."""
|
|
566
|
+
|
|
567
|
+
def __init__(self, settings: HRMInferenceSettings, source_module: Any,
|
|
568
|
+
logger: logging.Logger):
|
|
569
|
+
self._settings = settings
|
|
570
|
+
self._source = source_module
|
|
571
|
+
self._logger = logger
|
|
572
|
+
|
|
573
|
+
def assemble(self, aligned_cfg: Any,
|
|
574
|
+
paths: CheckpointPaths) -> torch.nn.Module:
|
|
575
|
+
"""Build the TopoGPT2 graph, load weights into it, and return it in eval mode."""
|
|
576
|
+
model_cls = getattr(self._source, "TopoGPT2")
|
|
577
|
+
model = model_cls(aligned_cfg)
|
|
578
|
+
model.to(aligned_cfg.DEVICE)
|
|
579
|
+
model.eval()
|
|
580
|
+
|
|
581
|
+
params = model.count_params()
|
|
582
|
+
self._logger.info(
|
|
583
|
+
"Model parameters: total=%s trainable=%s",
|
|
584
|
+
f"{params['total']:,}", f"{params['trainable']:,}",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
weights_path = paths.model_file()
|
|
588
|
+
self._logger.info("Loading weights from %s", weights_path)
|
|
589
|
+
state_dict = load_file(str(weights_path), device=aligned_cfg.DEVICE)
|
|
590
|
+
missing, unexpected = model.load_state_dict(
|
|
591
|
+
state_dict, strict=self._settings.strict_state_dict,
|
|
592
|
+
)
|
|
593
|
+
if missing:
|
|
594
|
+
self._logger.info("Missing keys (likely tied weights): %s", missing)
|
|
595
|
+
if unexpected:
|
|
596
|
+
self._logger.info("Unexpected keys ignored: %s", unexpected)
|
|
597
|
+
self._logger.info("Weights loaded successfully (zero new parameters introduced).")
|
|
598
|
+
return model
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class SeedSynchronizer:
|
|
602
|
+
"""Applies deterministic seeds across torch, CUDA and the model package."""
|
|
603
|
+
|
|
604
|
+
def __init__(self, settings: HRMInferenceSettings, source_module: Any,
|
|
605
|
+
logger: logging.Logger):
|
|
606
|
+
self._settings = settings
|
|
607
|
+
self._source = source_module
|
|
608
|
+
self._logger = logger
|
|
609
|
+
|
|
610
|
+
def apply(self) -> None:
|
|
611
|
+
"""Seed all relevant RNGs using the model package helper when available."""
|
|
612
|
+
torch.manual_seed(self._settings.seed)
|
|
613
|
+
if torch.cuda.is_available():
|
|
614
|
+
torch.cuda.manual_seed_all(self._settings.seed)
|
|
615
|
+
set_seed_fn = getattr(self._source, "set_seed", None)
|
|
616
|
+
if callable(set_seed_fn):
|
|
617
|
+
set_seed_fn(self._settings.seed, self._settings.device)
|
|
618
|
+
self._logger.info(
|
|
619
|
+
"Seed synchronized (seed=%d device=%s).",
|
|
620
|
+
self._settings.seed, self._settings.device,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class LatentChangeMetric:
|
|
625
|
+
"""Computes the relative L2 distance between two latent tensors."""
|
|
626
|
+
|
|
627
|
+
def __init__(self, epsilon_floor: float):
|
|
628
|
+
if epsilon_floor <= 0:
|
|
629
|
+
raise ValueError("epsilon_floor must be positive")
|
|
630
|
+
self._epsilon_floor = epsilon_floor
|
|
631
|
+
|
|
632
|
+
def relative_change(self, current: torch.Tensor,
|
|
633
|
+
previous: torch.Tensor) -> float:
|
|
634
|
+
"""Return ||current - previous|| / max(||previous||, epsilon_floor)."""
|
|
635
|
+
if current.shape != previous.shape:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
f"Shape mismatch in latent change: "
|
|
638
|
+
f"{tuple(current.shape)} vs {tuple(previous.shape)}"
|
|
639
|
+
)
|
|
640
|
+
delta = (current - previous).detach()
|
|
641
|
+
ref = previous.detach()
|
|
642
|
+
delta_norm = float(torch.linalg.vector_norm(delta).item())
|
|
643
|
+
ref_norm = float(torch.linalg.vector_norm(ref).item())
|
|
644
|
+
return delta_norm / max(ref_norm, self._epsilon_floor)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@dataclass
|
|
648
|
+
class ReasoningIterationStats:
|
|
649
|
+
"""Aggregated counters describing a single token's reasoning episode."""
|
|
650
|
+
|
|
651
|
+
high_level_iters: int = 0
|
|
652
|
+
low_level_iters_total: int = 0
|
|
653
|
+
converged_high_level: bool = False
|
|
654
|
+
halted_by_safety_cap: bool = False
|
|
655
|
+
final_high_level_change: float = float("nan")
|
|
656
|
+
final_low_level_change: float = float("nan")
|
|
657
|
+
h_reused_from_cache: bool = False
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@dataclass
|
|
661
|
+
class GenerationReasoningSummary:
|
|
662
|
+
"""Aggregated statistics over the full generation episode."""
|
|
663
|
+
|
|
664
|
+
tokens_generated: int = 0
|
|
665
|
+
total_high_level_iters: int = 0
|
|
666
|
+
total_low_level_iters: int = 0
|
|
667
|
+
safety_halts: int = 0
|
|
668
|
+
high_level_convergences: int = 0
|
|
669
|
+
h_cache_reuses: int = 0
|
|
670
|
+
|
|
671
|
+
def absorb(self, sample: ReasoningIterationStats) -> None:
|
|
672
|
+
"""Fold a per-token sample into the running totals."""
|
|
673
|
+
self.tokens_generated += 1
|
|
674
|
+
self.total_high_level_iters += sample.high_level_iters
|
|
675
|
+
self.total_low_level_iters += sample.low_level_iters_total
|
|
676
|
+
if sample.halted_by_safety_cap:
|
|
677
|
+
self.safety_halts += 1
|
|
678
|
+
if sample.converged_high_level:
|
|
679
|
+
self.high_level_convergences += 1
|
|
680
|
+
if sample.h_reused_from_cache:
|
|
681
|
+
self.h_cache_reuses += 1
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class SparseHighLevelStateCache:
|
|
685
|
+
"""Persists the high-level latent state across consecutive emitted tokens.
|
|
686
|
+
|
|
687
|
+
The cache is reset whenever its age in tokens reaches the configured
|
|
688
|
+
persistence horizon, at which point the next reasoning episode begins
|
|
689
|
+
with a zero high-level state. This is the temporal-sparsity mechanism:
|
|
690
|
+
expensive full-stack passes are amortized across multiple emissions.
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
def __init__(self, persist_tokens: int):
|
|
694
|
+
if persist_tokens < 1:
|
|
695
|
+
raise ValueError("persist_tokens must be >= 1")
|
|
696
|
+
self._persist_tokens = persist_tokens
|
|
697
|
+
self._state: Optional[torch.Tensor] = None
|
|
698
|
+
self._age: int = 0
|
|
699
|
+
|
|
700
|
+
def get_or_init(self, reference: torch.Tensor) -> Tuple[torch.Tensor, bool]:
|
|
701
|
+
"""Return the cached high-level state or a zeroed one when stale.
|
|
702
|
+
|
|
703
|
+
The boolean flag indicates whether the returned tensor came from a
|
|
704
|
+
live cache hit (True) or a fresh zero initialization (False).
|
|
705
|
+
"""
|
|
706
|
+
if (self._state is None
|
|
707
|
+
or self._state.shape != reference.shape
|
|
708
|
+
or self._state.dtype != reference.dtype
|
|
709
|
+
or self._state.device != reference.device
|
|
710
|
+
or self._age >= self._persist_tokens):
|
|
711
|
+
self._state = torch.zeros_like(reference)
|
|
712
|
+
self._age = 0
|
|
713
|
+
return self._state, False
|
|
714
|
+
return self._state, True
|
|
715
|
+
|
|
716
|
+
def commit(self, new_state: torch.Tensor) -> None:
|
|
717
|
+
"""Store a fresh high-level state and increment the cache age."""
|
|
718
|
+
self._state = new_state.detach()
|
|
719
|
+
self._age += 1
|
|
720
|
+
|
|
721
|
+
def invalidate(self) -> None:
|
|
722
|
+
"""Drop any cached state and reset the age counter."""
|
|
723
|
+
self._state = None
|
|
724
|
+
self._age = 0
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class HierarchicalRecursiveReasoner:
|
|
728
|
+
"""Parameter-free hierarchical recursive reasoning over a trained stack.
|
|
729
|
+
|
|
730
|
+
The reasoner does not own any learnable parameters. It treats the trained
|
|
731
|
+
TopoGPT2 transformer layers as a deterministic recurrent step function
|
|
732
|
+
and composes them into a two-speed damped fixed-point iteration that
|
|
733
|
+
mirrors HRM, while never violating the activation distribution the
|
|
734
|
+
trained layers expect.
|
|
735
|
+
|
|
736
|
+
Algorithm per emitted token:
|
|
737
|
+
|
|
738
|
+
1. Run the standard full forward pass once to obtain the baseline
|
|
739
|
+
residual-stream latent z_base and the per-layer kv caches that
|
|
740
|
+
will cross the token boundary. z_base is the trained model's
|
|
741
|
+
native answer for this position.
|
|
742
|
+
2. If recursion is disabled or both iteration budgets are zero,
|
|
743
|
+
return z_base unchanged.
|
|
744
|
+
3. Optionally warm-start z by adding a fraction of the cached
|
|
745
|
+
refinement vector from previous tokens (sparse temporal memory).
|
|
746
|
+
4. Hierarchical refinement, all in residual-stream space:
|
|
747
|
+
for h_step in range(max_high_level_iters):
|
|
748
|
+
for l_step in range(max_low_level_iters):
|
|
749
|
+
z <- z + low_level_step * (W_low(z) - z)
|
|
750
|
+
z <- z + high_level_step * (W_high(z) - z)
|
|
751
|
+
where W_low and W_high are the last low_level_window and
|
|
752
|
+
high_level_window trained layers respectively, invoked with the
|
|
753
|
+
prefix kv cache treated as immutable. Each update is damped, so
|
|
754
|
+
layer inputs remain close to the trained residual-stream
|
|
755
|
+
distribution.
|
|
756
|
+
5. Hard divergence guard: if the iterated latent drifts farther
|
|
757
|
+
from the baseline than max_drift_relative, reset to the baseline
|
|
758
|
+
and abort thinking for this token. This eliminates the
|
|
759
|
+
catastrophic-attractor failure mode without retraining.
|
|
760
|
+
6. Attractor halting per loop, plus a global cap on total layer
|
|
761
|
+
invocations.
|
|
762
|
+
|
|
763
|
+
The cached refinement returned to the sparse cache is z_final - z_base,
|
|
764
|
+
a small residual-stream displacement that persists across configurable
|
|
765
|
+
horizons to amortize thinking effort over multiple tokens.
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
def __init__(self, layers: List[torch.nn.Module],
|
|
769
|
+
final_norm: torch.nn.Module,
|
|
770
|
+
reasoning_config: RecursiveReasoningConfig,
|
|
771
|
+
logger: logging.Logger):
|
|
772
|
+
if reasoning_config.low_level_window > len(layers):
|
|
773
|
+
raise ValueError(
|
|
774
|
+
f"low_level_window={reasoning_config.low_level_window} "
|
|
775
|
+
f"exceeds number of layers={len(layers)}"
|
|
776
|
+
)
|
|
777
|
+
if reasoning_config.high_level_window > len(layers):
|
|
778
|
+
raise ValueError(
|
|
779
|
+
f"high_level_window={reasoning_config.high_level_window} "
|
|
780
|
+
f"exceeds number of layers={len(layers)}"
|
|
781
|
+
)
|
|
782
|
+
self._layers = layers
|
|
783
|
+
self._final_norm = final_norm
|
|
784
|
+
self._config = reasoning_config
|
|
785
|
+
self._logger = logger
|
|
786
|
+
self._metric = LatentChangeMetric(reasoning_config.latent_change_eps)
|
|
787
|
+
|
|
788
|
+
@property
|
|
789
|
+
def num_layers(self) -> int:
|
|
790
|
+
"""Return the number of trained transformer layers."""
|
|
791
|
+
return len(self._layers)
|
|
792
|
+
|
|
793
|
+
def _full_pass(self, z_in: torch.Tensor,
|
|
794
|
+
base_kvs: List[Optional[Tuple[torch.Tensor, torch.Tensor]]]
|
|
795
|
+
) -> Tuple[torch.Tensor, List[Tuple[torch.Tensor, torch.Tensor]]]:
|
|
796
|
+
"""Forward z_in through every layer using base_kvs as immutable prefix cache.
|
|
797
|
+
|
|
798
|
+
Returns the layer-stack output and the freshly produced per-layer kv
|
|
799
|
+
caches that incorporate the K and V derived from z_in.
|
|
800
|
+
"""
|
|
801
|
+
x = z_in
|
|
802
|
+
produced: List[Tuple[torch.Tensor, torch.Tensor]] = []
|
|
803
|
+
for layer_idx, layer in enumerate(self._layers):
|
|
804
|
+
x, _aux_loss, kv_cache = layer(x, past_kv=base_kvs[layer_idx])
|
|
805
|
+
produced.append(kv_cache)
|
|
806
|
+
return x, produced
|
|
807
|
+
|
|
808
|
+
def _window_pass(self, z_in: torch.Tensor,
|
|
809
|
+
base_kvs: List[Optional[Tuple[torch.Tensor, torch.Tensor]]],
|
|
810
|
+
window: int) -> torch.Tensor:
|
|
811
|
+
"""Forward z_in through the trailing `window` layers only.
|
|
812
|
+
|
|
813
|
+
The per-layer kv caches produced during this read-only pass are
|
|
814
|
+
discarded; only the baseline pass's committed kvs cross the token
|
|
815
|
+
boundary, preserving cache consistency across thinking iterations.
|
|
816
|
+
"""
|
|
817
|
+
if window <= 0:
|
|
818
|
+
return z_in
|
|
819
|
+
n = self.num_layers
|
|
820
|
+
x = z_in
|
|
821
|
+
for layer_idx in range(n - window, n):
|
|
822
|
+
x, _aux_loss, _kv_cache = self._layers[layer_idx](
|
|
823
|
+
x, past_kv=base_kvs[layer_idx],
|
|
824
|
+
)
|
|
825
|
+
return x
|
|
826
|
+
|
|
827
|
+
def reason(self,
|
|
828
|
+
z_initial: torch.Tensor,
|
|
829
|
+
base_kvs: List[Optional[Tuple[torch.Tensor, torch.Tensor]]],
|
|
830
|
+
cached_refinement: Optional[torch.Tensor],
|
|
831
|
+
) -> Tuple[torch.Tensor,
|
|
832
|
+
List[Tuple[torch.Tensor, torch.Tensor]],
|
|
833
|
+
torch.Tensor,
|
|
834
|
+
ReasoningIterationStats]:
|
|
835
|
+
"""Run hierarchical recursive thinking for a single emission step.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
z_initial: token embedding of the new position, shape [B, 1, D].
|
|
839
|
+
base_kvs: per-layer kv cache for all previously emitted tokens,
|
|
840
|
+
treated as immutable during thinking iterations.
|
|
841
|
+
cached_refinement: persistent refinement displacement from prior
|
|
842
|
+
tokens, or None to skip the warm start.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
A tuple (z_final, committed_kvs, refinement_for_cache, stats):
|
|
846
|
+
z_final is the latent state about to enter the final norm
|
|
847
|
+
and lm head; committed_kvs is the new per-layer kv cache
|
|
848
|
+
including this token's K and V from the baseline pass;
|
|
849
|
+
refinement_for_cache is z_final - z_baseline, to be
|
|
850
|
+
persisted across tokens; stats holds the loop counters.
|
|
851
|
+
"""
|
|
852
|
+
cfg = self._config
|
|
853
|
+
stats = ReasoningIterationStats()
|
|
854
|
+
|
|
855
|
+
z_baseline, committed_kvs = self._full_pass(z_initial, base_kvs)
|
|
856
|
+
|
|
857
|
+
recursion_requested = (cfg.enabled
|
|
858
|
+
and (cfg.max_high_level_iters > 0
|
|
859
|
+
or cfg.max_low_level_iters > 0))
|
|
860
|
+
if not recursion_requested:
|
|
861
|
+
return (z_baseline, committed_kvs,
|
|
862
|
+
torch.zeros_like(z_baseline), stats)
|
|
863
|
+
|
|
864
|
+
z = z_baseline
|
|
865
|
+
if (cached_refinement is not None
|
|
866
|
+
and cached_refinement.shape == z_baseline.shape):
|
|
867
|
+
z = z + cfg.cache_warm_start_weight * cached_refinement
|
|
868
|
+
stats.h_reused_from_cache = True
|
|
869
|
+
|
|
870
|
+
total_layer_calls = 0
|
|
871
|
+
low_change = float("nan")
|
|
872
|
+
high_change = float("nan")
|
|
873
|
+
|
|
874
|
+
for h_step in range(cfg.max_high_level_iters):
|
|
875
|
+
for l_step in range(cfg.max_low_level_iters):
|
|
876
|
+
z_new = self._window_pass(z, base_kvs, cfg.low_level_window)
|
|
877
|
+
delta = z_new - z
|
|
878
|
+
low_change = self._metric.relative_change(z_new, z)
|
|
879
|
+
z = z + cfg.low_level_step * delta
|
|
880
|
+
total_layer_calls += cfg.low_level_window
|
|
881
|
+
stats.low_level_iters_total += 1
|
|
882
|
+
stats.final_low_level_change = low_change
|
|
883
|
+
|
|
884
|
+
drift = self._metric.relative_change(z, z_baseline)
|
|
885
|
+
if drift > cfg.max_drift_relative:
|
|
886
|
+
z = z_baseline
|
|
887
|
+
stats.halted_by_safety_cap = True
|
|
888
|
+
break
|
|
889
|
+
if (l_step + 1 >= cfg.minimum_low_level_iters
|
|
890
|
+
and low_change < cfg.attractor_low_epsilon):
|
|
891
|
+
break
|
|
892
|
+
if total_layer_calls >= cfg.safety_max_total_iterations:
|
|
893
|
+
stats.halted_by_safety_cap = True
|
|
894
|
+
break
|
|
895
|
+
|
|
896
|
+
if stats.halted_by_safety_cap:
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
z_new = self._window_pass(z, base_kvs, cfg.high_level_window)
|
|
900
|
+
delta = z_new - z
|
|
901
|
+
high_change = self._metric.relative_change(z_new, z)
|
|
902
|
+
z = z + cfg.high_level_step * delta
|
|
903
|
+
total_layer_calls += cfg.high_level_window
|
|
904
|
+
stats.high_level_iters += 1
|
|
905
|
+
stats.final_high_level_change = high_change
|
|
906
|
+
|
|
907
|
+
drift = self._metric.relative_change(z, z_baseline)
|
|
908
|
+
if drift > cfg.max_drift_relative:
|
|
909
|
+
z = z_baseline
|
|
910
|
+
stats.halted_by_safety_cap = True
|
|
911
|
+
break
|
|
912
|
+
if (h_step + 1 >= cfg.minimum_high_level_iters
|
|
913
|
+
and high_change < cfg.attractor_high_epsilon):
|
|
914
|
+
stats.converged_high_level = True
|
|
915
|
+
break
|
|
916
|
+
if total_layer_calls >= cfg.safety_max_total_iterations:
|
|
917
|
+
stats.halted_by_safety_cap = True
|
|
918
|
+
break
|
|
919
|
+
|
|
920
|
+
refinement_for_cache = (z - z_baseline).detach()
|
|
921
|
+
|
|
922
|
+
if cfg.diagnostic_logging:
|
|
923
|
+
self._logger.debug(
|
|
924
|
+
"HRM token: h_iters=%d l_iters=%d converged=%s safety=%s "
|
|
925
|
+
"h_change=%.3e l_change=%.3e h_reused=%s",
|
|
926
|
+
stats.high_level_iters, stats.low_level_iters_total,
|
|
927
|
+
stats.converged_high_level, stats.halted_by_safety_cap,
|
|
928
|
+
stats.final_high_level_change, stats.final_low_level_change,
|
|
929
|
+
stats.h_reused_from_cache,
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
return z, committed_kvs, refinement_for_cache, stats
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
class LogitsSampler:
|
|
936
|
+
"""Applies temperature, repetition penalty, top-k filtering and multinomial draw."""
|
|
937
|
+
|
|
938
|
+
def __init__(self, logger: logging.Logger):
|
|
939
|
+
self._logger = logger
|
|
940
|
+
|
|
941
|
+
def sample(self, logits: torch.Tensor, token_history: torch.Tensor,
|
|
942
|
+
temperature: float, top_k: int,
|
|
943
|
+
repetition_penalty: float) -> torch.Tensor:
|
|
944
|
+
"""Return a sampled token id tensor of shape [B, 1] from raw logits [B, V]."""
|
|
945
|
+
if temperature <= 0:
|
|
946
|
+
raise ValueError("temperature must be positive at sampling time")
|
|
947
|
+
scaled = logits / temperature
|
|
948
|
+
if repetition_penalty != 1.0:
|
|
949
|
+
mask = torch.zeros_like(scaled, dtype=torch.bool)
|
|
950
|
+
mask.scatter_(1, token_history, True)
|
|
951
|
+
scaled = torch.where(mask, scaled / repetition_penalty, scaled)
|
|
952
|
+
if top_k > 0:
|
|
953
|
+
effective_k = min(top_k, scaled.size(-1))
|
|
954
|
+
top_values, _ = torch.topk(scaled, effective_k)
|
|
955
|
+
threshold = top_values[:, -1:].expand_as(scaled)
|
|
956
|
+
scaled = torch.where(scaled < threshold,
|
|
957
|
+
torch.full_like(scaled, float("-inf")), scaled)
|
|
958
|
+
probabilities = F.softmax(scaled, dim=-1)
|
|
959
|
+
return torch.multinomial(probabilities, 1)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
@dataclass(frozen=True)
|
|
963
|
+
class SamplingPolicy:
|
|
964
|
+
"""Immutable sampling parameters consumed by the generation engine."""
|
|
965
|
+
|
|
966
|
+
max_new_tokens: int
|
|
967
|
+
temperature: float
|
|
968
|
+
top_k: int
|
|
969
|
+
repetition_penalty: float
|
|
970
|
+
end_of_text_token_id: int
|
|
971
|
+
|
|
972
|
+
@classmethod
|
|
973
|
+
def from_settings(cls, settings: HRMInferenceSettings) -> "SamplingPolicy":
|
|
974
|
+
"""Construct a SamplingPolicy from inference settings."""
|
|
975
|
+
return cls(
|
|
976
|
+
max_new_tokens=settings.max_new_tokens,
|
|
977
|
+
temperature=settings.temperature,
|
|
978
|
+
top_k=settings.top_k,
|
|
979
|
+
repetition_penalty=settings.repetition_penalty,
|
|
980
|
+
end_of_text_token_id=settings.end_of_text_token_id,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
@dataclass(frozen=True)
|
|
985
|
+
class GenerationReport:
|
|
986
|
+
"""Quantitative summary of a single generation call."""
|
|
987
|
+
|
|
988
|
+
prompt: str
|
|
989
|
+
output: str
|
|
990
|
+
prompt_tokens: int
|
|
991
|
+
new_tokens: int
|
|
992
|
+
elapsed_seconds: float
|
|
993
|
+
reasoning_summary: GenerationReasoningSummary
|
|
994
|
+
|
|
995
|
+
def tokens_per_second(self, elapsed_floor: float) -> float:
|
|
996
|
+
"""Return throughput in tokens/sec, clamped to avoid divide-by-zero."""
|
|
997
|
+
return self.new_tokens / max(self.elapsed_seconds, elapsed_floor)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class HRMGenerationEngine:
|
|
1001
|
+
"""Runs autoregressive sampling driven by hierarchical recursive reasoning.
|
|
1002
|
+
|
|
1003
|
+
The engine reimplements the prompt encoding and token emission loop so
|
|
1004
|
+
that the per-token latent state can be intercepted before final norm and
|
|
1005
|
+
LM-head projection. The intercepted state is handed to a
|
|
1006
|
+
HierarchicalRecursiveReasoner, which iterates the trained layer stack in
|
|
1007
|
+
a two-speed loop until the attractor is reached. The final stabilized
|
|
1008
|
+
latent is then projected to logits and sampled in the standard fashion.
|
|
1009
|
+
"""
|
|
1010
|
+
|
|
1011
|
+
def __init__(self, settings: HRMInferenceSettings, logger: logging.Logger):
|
|
1012
|
+
self._settings = settings
|
|
1013
|
+
self._logger = logger
|
|
1014
|
+
self._sampler = LogitsSampler(logger)
|
|
1015
|
+
|
|
1016
|
+
def _encode_prompt(self, model: torch.nn.Module, prompt_ids: List[int]
|
|
1017
|
+
) -> Tuple[torch.Tensor,
|
|
1018
|
+
List[Tuple[torch.Tensor, torch.Tensor]],
|
|
1019
|
+
torch.Tensor]:
|
|
1020
|
+
"""Run the prompt through the full stack once, returning the final
|
|
1021
|
+
hidden state of the last position, the per-layer base kv caches that
|
|
1022
|
+
cover all prompt tokens except the last one, and the embedding of the
|
|
1023
|
+
last prompt token as the seed for the first reasoning episode.
|
|
1024
|
+
"""
|
|
1025
|
+
device = self._settings.device
|
|
1026
|
+
if len(prompt_ids) < 1:
|
|
1027
|
+
raise ValueError("Prompt must contain at least one token")
|
|
1028
|
+
|
|
1029
|
+
token_tensor = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
1030
|
+
if len(prompt_ids) == 1:
|
|
1031
|
+
base_kvs: List[Optional[Tuple[torch.Tensor, torch.Tensor]]] = [
|
|
1032
|
+
None for _ in model.layers
|
|
1033
|
+
]
|
|
1034
|
+
seed_embed = model.token_embed(token_tensor)
|
|
1035
|
+
return seed_embed, base_kvs, token_tensor
|
|
1036
|
+
|
|
1037
|
+
prefix_tokens = token_tensor[:, :-1]
|
|
1038
|
+
last_token = token_tensor[:, -1:]
|
|
1039
|
+
x = model.token_embed(prefix_tokens)
|
|
1040
|
+
base_kvs_built: List[Tuple[torch.Tensor, torch.Tensor]] = []
|
|
1041
|
+
for layer in model.layers:
|
|
1042
|
+
x, _aux_loss, kv_cache = layer(x, past_kv=None)
|
|
1043
|
+
base_kvs_built.append(kv_cache)
|
|
1044
|
+
seed_embed = model.token_embed(last_token)
|
|
1045
|
+
return seed_embed, base_kvs_built, token_tensor
|
|
1046
|
+
|
|
1047
|
+
@torch.no_grad()
|
|
1048
|
+
def run(self, model: torch.nn.Module, tokenizer: Any, prompt: str,
|
|
1049
|
+
policy: SamplingPolicy) -> GenerationReport:
|
|
1050
|
+
"""Generate a completion for prompt and return a GenerationReport."""
|
|
1051
|
+
prompt_ids = tokenizer.encode(prompt)
|
|
1052
|
+
if len(prompt_ids) == 0:
|
|
1053
|
+
raise ValueError("Prompt encodes to zero tokens; refusing to generate.")
|
|
1054
|
+
|
|
1055
|
+
device = self._settings.device
|
|
1056
|
+
reasoning_cfg = self._settings.reasoning
|
|
1057
|
+
reasoner = HierarchicalRecursiveReasoner(
|
|
1058
|
+
layers=list(model.layers),
|
|
1059
|
+
final_norm=model.final_norm,
|
|
1060
|
+
reasoning_config=reasoning_cfg,
|
|
1061
|
+
logger=self._logger,
|
|
1062
|
+
)
|
|
1063
|
+
h_cache = SparseHighLevelStateCache(
|
|
1064
|
+
persist_tokens=reasoning_cfg.high_level_persist_tokens,
|
|
1065
|
+
)
|
|
1066
|
+
summary = GenerationReasoningSummary()
|
|
1067
|
+
|
|
1068
|
+
self._logger.info(
|
|
1069
|
+
"Sampling: temp=%.3f top_k=%d rep_pen=%.3f max_new=%d "
|
|
1070
|
+
"prompt_tokens=%d hrm_enabled=%s h_iters=%d l_iters=%d "
|
|
1071
|
+
"l_window=%d persist=%d",
|
|
1072
|
+
policy.temperature, policy.top_k, policy.repetition_penalty,
|
|
1073
|
+
policy.max_new_tokens, len(prompt_ids), reasoning_cfg.enabled,
|
|
1074
|
+
reasoning_cfg.max_high_level_iters,
|
|
1075
|
+
reasoning_cfg.max_low_level_iters,
|
|
1076
|
+
reasoning_cfg.low_level_window,
|
|
1077
|
+
reasoning_cfg.high_level_persist_tokens,
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
start = time.time()
|
|
1081
|
+
|
|
1082
|
+
if self._settings.thinking_mode and reasoning_cfg.enabled:
|
|
1083
|
+
reasoning_cfg = RecursiveReasoningConfig(
|
|
1084
|
+
enabled=True,
|
|
1085
|
+
max_high_level_iters=max(4, reasoning_cfg.max_high_level_iters),
|
|
1086
|
+
max_low_level_iters=max(8, reasoning_cfg.max_low_level_iters),
|
|
1087
|
+
low_level_window=reasoning_cfg.low_level_window,
|
|
1088
|
+
high_level_window=reasoning_cfg.high_level_window,
|
|
1089
|
+
low_level_step=reasoning_cfg.low_level_step,
|
|
1090
|
+
high_level_step=reasoning_cfg.high_level_step,
|
|
1091
|
+
attractor_low_epsilon=reasoning_cfg.attractor_low_epsilon,
|
|
1092
|
+
attractor_high_epsilon=reasoning_cfg.attractor_high_epsilon,
|
|
1093
|
+
high_level_persist_tokens=reasoning_cfg.high_level_persist_tokens,
|
|
1094
|
+
cache_warm_start_weight=reasoning_cfg.cache_warm_start_weight,
|
|
1095
|
+
max_drift_relative=reasoning_cfg.max_drift_relative,
|
|
1096
|
+
diagnostic_logging=reasoning_cfg.diagnostic_logging,
|
|
1097
|
+
)
|
|
1098
|
+
reasoner = HierarchicalRecursiveReasoner(
|
|
1099
|
+
layers=list(model.layers),
|
|
1100
|
+
final_norm=model.final_norm,
|
|
1101
|
+
reasoning_config=reasoning_cfg,
|
|
1102
|
+
logger=self._logger,
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
z_seed, base_kvs, full_history = self._encode_prompt(model, prompt_ids)
|
|
1106
|
+
prompt_ids_base = prompt_ids
|
|
1107
|
+
active_history = full_history
|
|
1108
|
+
emitted_ids_all: List[int] = []
|
|
1109
|
+
full_text = ""
|
|
1110
|
+
continuation_count = 0
|
|
1111
|
+
|
|
1112
|
+
while True:
|
|
1113
|
+
current_prompt = tokenizer.decode(active_history[0].tolist())
|
|
1114
|
+
z_seed, base_kvs, full_history = self._encode_prompt(model, prompt_ids_base)
|
|
1115
|
+
z_input = z_seed
|
|
1116
|
+
emitted_ids: List[int] = []
|
|
1117
|
+
active_history = full_history
|
|
1118
|
+
|
|
1119
|
+
for new_token_index in range(policy.max_new_tokens):
|
|
1120
|
+
cached_refinement, _hit = h_cache.get_or_init(z_input)
|
|
1121
|
+
z_final, committed_kvs, refinement_next, stats = reasoner.reason(
|
|
1122
|
+
z_initial=z_input,
|
|
1123
|
+
base_kvs=base_kvs,
|
|
1124
|
+
cached_refinement=cached_refinement,
|
|
1125
|
+
)
|
|
1126
|
+
summary.absorb(stats)
|
|
1127
|
+
base_kvs = list(committed_kvs)
|
|
1128
|
+
h_cache.commit(refinement_next)
|
|
1129
|
+
|
|
1130
|
+
hidden = model.final_norm(z_final)
|
|
1131
|
+
logits = model.lm_head(hidden)[:, -1, :]
|
|
1132
|
+
next_tok = self._sampler.sample(
|
|
1133
|
+
logits=logits,
|
|
1134
|
+
token_history=active_history,
|
|
1135
|
+
temperature=policy.temperature,
|
|
1136
|
+
top_k=policy.top_k,
|
|
1137
|
+
repetition_penalty=policy.repetition_penalty,
|
|
1138
|
+
)
|
|
1139
|
+
next_id = int(next_tok.item())
|
|
1140
|
+
emitted_ids.append(next_id)
|
|
1141
|
+
active_history = torch.cat([active_history, next_tok], dim=1)
|
|
1142
|
+
|
|
1143
|
+
if next_id == policy.end_of_text_token_id:
|
|
1144
|
+
break
|
|
1145
|
+
|
|
1146
|
+
z_input = model.token_embed(next_tok)
|
|
1147
|
+
|
|
1148
|
+
emitted_ids_all.extend(emitted_ids)
|
|
1149
|
+
full_text = tokenizer.decode(active_history[0].tolist())
|
|
1150
|
+
|
|
1151
|
+
if not self._settings.auto_continue:
|
|
1152
|
+
break
|
|
1153
|
+
|
|
1154
|
+
from .continuation import is_response_complete, extract_tail_for_continuation
|
|
1155
|
+
|
|
1156
|
+
prompt_text = tokenizer.decode(torch.tensor([prompt_ids_base],
|
|
1157
|
+
dtype=torch.long, device=device)[0].tolist())
|
|
1158
|
+
generated = full_text[len(prompt_text):]
|
|
1159
|
+
|
|
1160
|
+
if is_response_complete(generated):
|
|
1161
|
+
break
|
|
1162
|
+
|
|
1163
|
+
tail = extract_tail_for_continuation(generated, tail_lines=self._settings.continuation_tail_lines)
|
|
1164
|
+
if not tail:
|
|
1165
|
+
break
|
|
1166
|
+
|
|
1167
|
+
continuation_count += 1
|
|
1168
|
+
if continuation_count > self._settings.max_continuations:
|
|
1169
|
+
break
|
|
1170
|
+
|
|
1171
|
+
tail_text = prompt_text + tail
|
|
1172
|
+
prompt_ids_base = tokenizer.encode(tail_text)
|
|
1173
|
+
h_cache = SparseHighLevelStateCache(
|
|
1174
|
+
persist_tokens=reasoning_cfg.high_level_persist_tokens,
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
elapsed = time.time() - start
|
|
1178
|
+
new_tokens = len(emitted_ids_all)
|
|
1179
|
+
return GenerationReport(
|
|
1180
|
+
prompt=prompt,
|
|
1181
|
+
output=full_text,
|
|
1182
|
+
prompt_tokens=len(prompt_ids),
|
|
1183
|
+
new_tokens=new_tokens,
|
|
1184
|
+
elapsed_seconds=elapsed,
|
|
1185
|
+
reasoning_summary=summary,
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
class ResultRenderer:
|
|
1190
|
+
"""Prints a GenerationReport to stdout using settings-defined formatting."""
|
|
1191
|
+
|
|
1192
|
+
def __init__(self, settings: HRMInferenceSettings, logger: logging.Logger):
|
|
1193
|
+
self._settings = settings
|
|
1194
|
+
self._logger = logger
|
|
1195
|
+
|
|
1196
|
+
def render(self, report: GenerationReport) -> None:
|
|
1197
|
+
"""Emit a banner with prompt, completion, throughput and reasoning stats."""
|
|
1198
|
+
s = self._settings
|
|
1199
|
+
sep = s.result_separator_char * s.result_separator_width
|
|
1200
|
+
completion = (
|
|
1201
|
+
report.output[len(report.prompt):]
|
|
1202
|
+
if report.output.startswith(report.prompt)
|
|
1203
|
+
else report.output
|
|
1204
|
+
)
|
|
1205
|
+
summary = report.reasoning_summary
|
|
1206
|
+
avg_h = (summary.total_high_level_iters / summary.tokens_generated
|
|
1207
|
+
if summary.tokens_generated > 0 else 0.0)
|
|
1208
|
+
avg_l = (summary.total_low_level_iters / summary.tokens_generated
|
|
1209
|
+
if summary.tokens_generated > 0 else 0.0)
|
|
1210
|
+
self._logger.info(
|
|
1211
|
+
"Generated %d new tokens in %.2fs (%.1f tok/s)",
|
|
1212
|
+
report.new_tokens, report.elapsed_seconds,
|
|
1213
|
+
report.tokens_per_second(s.elapsed_floor_seconds),
|
|
1214
|
+
)
|
|
1215
|
+
self._logger.info(
|
|
1216
|
+
"%s: avg_h=%.2f avg_l=%.2f h_converged=%d safety_halts=%d "
|
|
1217
|
+
"h_cache_reuses=%d",
|
|
1218
|
+
s.reasoning_label, avg_h, avg_l,
|
|
1219
|
+
summary.high_level_convergences, summary.safety_halts,
|
|
1220
|
+
summary.h_cache_reuses,
|
|
1221
|
+
)
|
|
1222
|
+
out = sys.stdout
|
|
1223
|
+
out.write("\n" + sep + "\n")
|
|
1224
|
+
out.write(f"[{s.prompt_label}] {report.prompt}\n")
|
|
1225
|
+
out.write(f"[{s.completion_label}]\n{completion}\n")
|
|
1226
|
+
out.write(sep + "\n")
|
|
1227
|
+
out.flush()
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
class HRMInferencePipeline:
|
|
1231
|
+
"""Orchestrator wiring loader, builder, reasoner, engine and renderer."""
|
|
1232
|
+
|
|
1233
|
+
def __init__(self, settings: HRMInferenceSettings,
|
|
1234
|
+
logger: Optional[logging.Logger] = None):
|
|
1235
|
+
settings.validate()
|
|
1236
|
+
self._settings = settings
|
|
1237
|
+
self._logger = logger or HRMLoggerFactory.build(settings)
|
|
1238
|
+
|
|
1239
|
+
def execute(self) -> GenerationReport:
|
|
1240
|
+
"""Run the full inference pipeline end-to-end and return the report."""
|
|
1241
|
+
source_module = SourceModuleLoader(
|
|
1242
|
+
self._settings, self._logger,
|
|
1243
|
+
).load()
|
|
1244
|
+
|
|
1245
|
+
SeedSynchronizer(
|
|
1246
|
+
self._settings, source_module, self._logger,
|
|
1247
|
+
).apply()
|
|
1248
|
+
|
|
1249
|
+
GaussPatchApplier(
|
|
1250
|
+
self._settings, source_module, self._logger,
|
|
1251
|
+
).apply_if_enabled()
|
|
1252
|
+
|
|
1253
|
+
paths = CheckpointPaths(self._settings)
|
|
1254
|
+
paths.assert_ready()
|
|
1255
|
+
|
|
1256
|
+
preset = self._settings.preset()
|
|
1257
|
+
n_kv = WeightShapeProbe(
|
|
1258
|
+
self._settings, self._logger,
|
|
1259
|
+
).detect_n_kv_heads(
|
|
1260
|
+
paths.model_file(),
|
|
1261
|
+
d_model=preset.d_model,
|
|
1262
|
+
n_heads=preset.n_heads,
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
tokenizer = TokenizerFactory(self._settings, source_module).build()
|
|
1266
|
+
aligned_cfg = TopoGPT2ConfigAligner(
|
|
1267
|
+
self._settings, source_module, self._logger,
|
|
1268
|
+
).build(n_kv_heads=n_kv, vocab_size=tokenizer.vocab_size)
|
|
1269
|
+
|
|
1270
|
+
model = ModelAssembler(
|
|
1271
|
+
self._settings, source_module, self._logger,
|
|
1272
|
+
).assemble(aligned_cfg, paths)
|
|
1273
|
+
|
|
1274
|
+
engine = HRMGenerationEngine(self._settings, self._logger)
|
|
1275
|
+
report = engine.run(
|
|
1276
|
+
model, tokenizer, self._settings.prompt,
|
|
1277
|
+
SamplingPolicy.from_settings(self._settings),
|
|
1278
|
+
)
|
|
1279
|
+
ResultRenderer(self._settings, self._logger).render(report)
|
|
1280
|
+
return report
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
class CliArgumentParser:
|
|
1284
|
+
"""Translates command-line arguments into an HRMInferenceSettings instance."""
|
|
1285
|
+
|
|
1286
|
+
@staticmethod
|
|
1287
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1288
|
+
"""Return the configured argparse.ArgumentParser."""
|
|
1289
|
+
defaults = HRMInferenceSettings()
|
|
1290
|
+
reasoning_defaults = defaults.reasoning
|
|
1291
|
+
parser = argparse.ArgumentParser(
|
|
1292
|
+
description=(
|
|
1293
|
+
"TopoGPT3.1 inference engine: hierarchical recursive reasoning "
|
|
1294
|
+
"over a parameter-frozen TopoGPT3 checkpoint."
|
|
1295
|
+
),
|
|
1296
|
+
)
|
|
1297
|
+
parser.add_argument(
|
|
1298
|
+
"--prompt", type=str, default=defaults.prompt,
|
|
1299
|
+
help="Code prefix to complete.",
|
|
1300
|
+
)
|
|
1301
|
+
parser.add_argument(
|
|
1302
|
+
"--scale", type=str, default=defaults.model_scale,
|
|
1303
|
+
choices=sorted(HRMInferenceSettings.scale_presets().keys()),
|
|
1304
|
+
help="Model scale preset.",
|
|
1305
|
+
)
|
|
1306
|
+
parser.add_argument(
|
|
1307
|
+
"--max-new", type=int, default=defaults.max_new_tokens,
|
|
1308
|
+
help="Maximum number of new tokens to generate.",
|
|
1309
|
+
)
|
|
1310
|
+
parser.add_argument(
|
|
1311
|
+
"--temp", type=float, default=defaults.temperature,
|
|
1312
|
+
help="Sampling temperature (lower means more deterministic).",
|
|
1313
|
+
)
|
|
1314
|
+
parser.add_argument(
|
|
1315
|
+
"--top-k", type=int, default=defaults.top_k,
|
|
1316
|
+
help="Top-k filtering (0 disables it).",
|
|
1317
|
+
)
|
|
1318
|
+
parser.add_argument(
|
|
1319
|
+
"--rep-penalty", type=float, default=defaults.repetition_penalty,
|
|
1320
|
+
help="Repetition penalty (values above 1 reduce repeats).",
|
|
1321
|
+
)
|
|
1322
|
+
parser.add_argument(
|
|
1323
|
+
"--device", type=str, default=defaults.device,
|
|
1324
|
+
help="Compute device, e.g. cuda, cpu, cuda:0.",
|
|
1325
|
+
)
|
|
1326
|
+
parser.add_argument(
|
|
1327
|
+
"--ckpt-dir", type=str, default=defaults.checkpoint_dir,
|
|
1328
|
+
help="Root directory holding TopoGPT3 checkpoints.",
|
|
1329
|
+
)
|
|
1330
|
+
parser.add_argument(
|
|
1331
|
+
"--ckpt-name", type=str, default=defaults.checkpoint_name,
|
|
1332
|
+
help="Checkpoint slot to load (e.g. 'last').",
|
|
1333
|
+
)
|
|
1334
|
+
parser.add_argument(
|
|
1335
|
+
"--source-dir", type=str, default=defaults.source_directory,
|
|
1336
|
+
help="Directory containing train.py and model.py.",
|
|
1337
|
+
)
|
|
1338
|
+
parser.add_argument(
|
|
1339
|
+
"--no-gauss", action="store_true",
|
|
1340
|
+
help="Disable the Gauss complex-multiply patch at load.",
|
|
1341
|
+
)
|
|
1342
|
+
parser.add_argument(
|
|
1343
|
+
"--strict-load", action="store_true",
|
|
1344
|
+
help="Require strict state_dict matching when loading weights.",
|
|
1345
|
+
)
|
|
1346
|
+
parser.add_argument(
|
|
1347
|
+
"--seed", type=int, default=defaults.seed,
|
|
1348
|
+
help="Deterministic seed for sampling reproducibility.",
|
|
1349
|
+
)
|
|
1350
|
+
parser.add_argument(
|
|
1351
|
+
"--log-level", type=str, default=defaults.log_level,
|
|
1352
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
1353
|
+
help="Logging verbosity.",
|
|
1354
|
+
)
|
|
1355
|
+
parser.add_argument(
|
|
1356
|
+
"--disable-hrm", action="store_true",
|
|
1357
|
+
help="Disable hierarchical recursive reasoning (fallback to "
|
|
1358
|
+
"standard autoregressive generation).",
|
|
1359
|
+
)
|
|
1360
|
+
parser.add_argument(
|
|
1361
|
+
"--hrm-h-iters", type=int,
|
|
1362
|
+
default=reasoning_defaults.max_high_level_iters,
|
|
1363
|
+
help="Maximum high-level (slow) iterations per emitted token.",
|
|
1364
|
+
)
|
|
1365
|
+
parser.add_argument(
|
|
1366
|
+
"--hrm-l-iters", type=int,
|
|
1367
|
+
default=reasoning_defaults.max_low_level_iters,
|
|
1368
|
+
help="Maximum low-level (fast) iterations per high-level step.",
|
|
1369
|
+
)
|
|
1370
|
+
parser.add_argument(
|
|
1371
|
+
"--hrm-l-window", type=int,
|
|
1372
|
+
default=reasoning_defaults.low_level_window,
|
|
1373
|
+
help="Number of trailing layers used by the low-level loop.",
|
|
1374
|
+
)
|
|
1375
|
+
parser.add_argument(
|
|
1376
|
+
"--hrm-h-window", type=int,
|
|
1377
|
+
default=reasoning_defaults.high_level_window,
|
|
1378
|
+
help="Number of trailing layers used by the high-level loop "
|
|
1379
|
+
"(must be >= --hrm-l-window).",
|
|
1380
|
+
)
|
|
1381
|
+
parser.add_argument(
|
|
1382
|
+
"--hrm-l-step", type=float,
|
|
1383
|
+
default=reasoning_defaults.low_level_step,
|
|
1384
|
+
help="Damping coefficient for the low-level update in [0, 1].",
|
|
1385
|
+
)
|
|
1386
|
+
parser.add_argument(
|
|
1387
|
+
"--hrm-h-step", type=float,
|
|
1388
|
+
default=reasoning_defaults.high_level_step,
|
|
1389
|
+
help="Damping coefficient for the high-level update in [0, 1].",
|
|
1390
|
+
)
|
|
1391
|
+
parser.add_argument(
|
|
1392
|
+
"--hrm-persist", type=int,
|
|
1393
|
+
default=reasoning_defaults.high_level_persist_tokens,
|
|
1394
|
+
help="Tokens during which the refinement vector is reused as a "
|
|
1395
|
+
"warm start before being re-initialized.",
|
|
1396
|
+
)
|
|
1397
|
+
parser.add_argument(
|
|
1398
|
+
"--hrm-warm-start", type=float,
|
|
1399
|
+
default=reasoning_defaults.cache_warm_start_weight,
|
|
1400
|
+
help="Scalar in [0, 1] applied to the cached refinement vector "
|
|
1401
|
+
"when warm-starting the next token's iteration.",
|
|
1402
|
+
)
|
|
1403
|
+
parser.add_argument(
|
|
1404
|
+
"--hrm-max-drift", type=float,
|
|
1405
|
+
default=reasoning_defaults.max_drift_relative,
|
|
1406
|
+
help="Relative L2 ceiling between the iterated latent and the "
|
|
1407
|
+
"baseline; exceeding it aborts thinking and resets to "
|
|
1408
|
+
"baseline for the current token.",
|
|
1409
|
+
)
|
|
1410
|
+
parser.add_argument(
|
|
1411
|
+
"--hrm-low-eps", type=float,
|
|
1412
|
+
default=reasoning_defaults.attractor_low_epsilon,
|
|
1413
|
+
help="Relative L2 threshold halting the low-level loop.",
|
|
1414
|
+
)
|
|
1415
|
+
parser.add_argument(
|
|
1416
|
+
"--hrm-high-eps", type=float,
|
|
1417
|
+
default=reasoning_defaults.attractor_high_epsilon,
|
|
1418
|
+
help="Relative L2 threshold halting the high-level loop.",
|
|
1419
|
+
)
|
|
1420
|
+
parser.add_argument(
|
|
1421
|
+
"--hrm-safety-cap", type=int,
|
|
1422
|
+
default=reasoning_defaults.safety_max_total_iterations,
|
|
1423
|
+
help="Hard cap on total layer invocations per emitted token.",
|
|
1424
|
+
)
|
|
1425
|
+
parser.add_argument(
|
|
1426
|
+
"--hrm-diagnostic", action="store_true",
|
|
1427
|
+
help="Enable per-token HRM diagnostic logging.",
|
|
1428
|
+
)
|
|
1429
|
+
parser.add_argument(
|
|
1430
|
+
"--thinking", action="store_true",
|
|
1431
|
+
default=defaults.thinking_mode,
|
|
1432
|
+
help="Enable thinking mode: HRM runs with higher iteration counts "
|
|
1433
|
+
"(high=4, low=8) for deeper reasoning per token.",
|
|
1434
|
+
)
|
|
1435
|
+
parser.add_argument(
|
|
1436
|
+
"--auto-continue", "-C", action="store_true",
|
|
1437
|
+
default=defaults.auto_continue,
|
|
1438
|
+
help="Auto-continue truncated responses by feeding tail lines back.",
|
|
1439
|
+
)
|
|
1440
|
+
parser.add_argument(
|
|
1441
|
+
"--max-continuations", type=int,
|
|
1442
|
+
default=defaults.max_continuations,
|
|
1443
|
+
help="Max number of continuation rounds (requires --auto-continue).",
|
|
1444
|
+
)
|
|
1445
|
+
return parser
|
|
1446
|
+
|
|
1447
|
+
@staticmethod
|
|
1448
|
+
def parse(argv: Optional[List[str]] = None) -> HRMInferenceSettings:
|
|
1449
|
+
"""Parse argv (or sys.argv) and return a populated HRMInferenceSettings."""
|
|
1450
|
+
namespace = CliArgumentParser.build_parser().parse_args(argv)
|
|
1451
|
+
defaults = HRMInferenceSettings()
|
|
1452
|
+
reasoning_defaults = defaults.reasoning
|
|
1453
|
+
reasoning = RecursiveReasoningConfig(
|
|
1454
|
+
enabled=not namespace.disable_hrm,
|
|
1455
|
+
max_high_level_iters=namespace.hrm_h_iters,
|
|
1456
|
+
max_low_level_iters=namespace.hrm_l_iters,
|
|
1457
|
+
low_level_window=namespace.hrm_l_window,
|
|
1458
|
+
high_level_window=namespace.hrm_h_window,
|
|
1459
|
+
low_level_step=namespace.hrm_l_step,
|
|
1460
|
+
high_level_step=namespace.hrm_h_step,
|
|
1461
|
+
attractor_low_epsilon=namespace.hrm_low_eps,
|
|
1462
|
+
attractor_high_epsilon=namespace.hrm_high_eps,
|
|
1463
|
+
high_level_persist_tokens=namespace.hrm_persist,
|
|
1464
|
+
cache_warm_start_weight=namespace.hrm_warm_start,
|
|
1465
|
+
max_drift_relative=namespace.hrm_max_drift,
|
|
1466
|
+
latent_change_eps=reasoning_defaults.latent_change_eps,
|
|
1467
|
+
safety_max_total_iterations=namespace.hrm_safety_cap,
|
|
1468
|
+
minimum_low_level_iters=reasoning_defaults.minimum_low_level_iters,
|
|
1469
|
+
minimum_high_level_iters=reasoning_defaults.minimum_high_level_iters,
|
|
1470
|
+
diagnostic_logging=namespace.hrm_diagnostic,
|
|
1471
|
+
)
|
|
1472
|
+
return HRMInferenceSettings(
|
|
1473
|
+
prompt=namespace.prompt,
|
|
1474
|
+
model_scale=namespace.scale,
|
|
1475
|
+
max_new_tokens=namespace.max_new,
|
|
1476
|
+
temperature=namespace.temp,
|
|
1477
|
+
top_k=namespace.top_k,
|
|
1478
|
+
repetition_penalty=namespace.rep_penalty,
|
|
1479
|
+
device=namespace.device,
|
|
1480
|
+
checkpoint_dir=namespace.ckpt_dir,
|
|
1481
|
+
checkpoint_name=namespace.ckpt_name,
|
|
1482
|
+
source_directory=namespace.source_dir,
|
|
1483
|
+
apply_gauss_patch_at_load=not namespace.no_gauss,
|
|
1484
|
+
strict_state_dict=namespace.strict_load,
|
|
1485
|
+
seed=namespace.seed,
|
|
1486
|
+
log_level=namespace.log_level,
|
|
1487
|
+
reasoning=reasoning,
|
|
1488
|
+
thinking_mode=namespace.thinking,
|
|
1489
|
+
auto_continue=namespace.auto_continue,
|
|
1490
|
+
max_continuations=namespace.max_continuations,
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
1495
|
+
"""CLI entry point. Returns a process exit code."""
|
|
1496
|
+
settings = CliArgumentParser.parse(argv)
|
|
1497
|
+
HRMInferencePipeline(settings).execute()
|
|
1498
|
+
return 0
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
if __name__ == "__main__":
|
|
1502
|
+
sys.exit(main())
|