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.
@@ -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())