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
topogpt3/inference.py
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TopoGPT3 inference engine.
|
|
4
|
+
|
|
5
|
+
Production-grade autoregressive code completion pipeline for TopoGPT3
|
|
6
|
+
checkpoints. Loads weights from safetensors, aligns the underlying TopoGPT2
|
|
7
|
+
architecture against the stored tensors, optionally applies the Gauss
|
|
8
|
+
complex-multiply patch for numerical parity with training, and performs
|
|
9
|
+
sampling with repetition penalty and top-k filtering.
|
|
10
|
+
|
|
11
|
+
The pipeline is decomposed into single-responsibility collaborators wired by
|
|
12
|
+
an orchestrator. All paths, sampling parameters, safety bounds and string
|
|
13
|
+
identifiers live inside InferenceSettings so that business logic contains no
|
|
14
|
+
magic numbers or hardcoded constants.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
from safetensors import safe_open
|
|
28
|
+
from safetensors.torch import load_file
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ScalePreset:
|
|
33
|
+
"""Immutable architecture preset for a named model scale."""
|
|
34
|
+
|
|
35
|
+
d_model: int
|
|
36
|
+
n_heads: int
|
|
37
|
+
n_layers: int
|
|
38
|
+
max_seq_len: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class InferenceSettings:
|
|
43
|
+
"""Centralized configuration container for the inference pipeline.
|
|
44
|
+
|
|
45
|
+
Every value consumed downstream resides here. Adding a new tunable means
|
|
46
|
+
extending this class; no other module should embed literals.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
model_scale: str = "small"
|
|
50
|
+
device: str = field(
|
|
51
|
+
default_factory=lambda: "cuda" if torch.cuda.is_available() else "cpu"
|
|
52
|
+
)
|
|
53
|
+
seed: int = 42
|
|
54
|
+
|
|
55
|
+
checkpoint_dir: str = "checkpoints_topogpt3"
|
|
56
|
+
checkpoint_name: str = "last"
|
|
57
|
+
model_filename: str = "model.safetensors"
|
|
58
|
+
state_filename: str = "state.json"
|
|
59
|
+
|
|
60
|
+
topogpt3_source_filename: str = "train.py"
|
|
61
|
+
topogpt2_source_filename: str = "model.py"
|
|
62
|
+
source_directory: str = "."
|
|
63
|
+
|
|
64
|
+
tokenizer_encoding: str = "gpt2"
|
|
65
|
+
weight_probe_key: str = "layers.0.attn.k_proj.weight"
|
|
66
|
+
|
|
67
|
+
apply_gauss_patch_at_load: bool = True
|
|
68
|
+
strict_state_dict: bool = False
|
|
69
|
+
|
|
70
|
+
prompt: str = "def main("
|
|
71
|
+
max_new_tokens: int = 256
|
|
72
|
+
temperature: float = 0.3
|
|
73
|
+
top_k: int = 50
|
|
74
|
+
repetition_penalty: float = 1.1
|
|
75
|
+
auto_continue: bool = False
|
|
76
|
+
max_continuations: int = 3
|
|
77
|
+
continuation_tail_lines: int = 2
|
|
78
|
+
|
|
79
|
+
log_level: str = "INFO"
|
|
80
|
+
log_name: str = "TopoGPT3Inference"
|
|
81
|
+
log_format: str = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
|
82
|
+
log_date_format: str = "%Y-%m-%d %H:%M:%S"
|
|
83
|
+
|
|
84
|
+
result_separator_char: str = "="
|
|
85
|
+
result_separator_width: int = 72
|
|
86
|
+
prompt_label: str = "PROMPT"
|
|
87
|
+
completion_label: str = "COMPLETION"
|
|
88
|
+
elapsed_floor_seconds: float = 1e-3
|
|
89
|
+
|
|
90
|
+
min_max_new_tokens: int = 1
|
|
91
|
+
max_max_new_tokens: int = 8192
|
|
92
|
+
min_temperature: float = 1e-3
|
|
93
|
+
max_temperature: float = 5.0
|
|
94
|
+
min_top_k: int = 0
|
|
95
|
+
min_repetition_penalty: float = 0.5
|
|
96
|
+
max_repetition_penalty: float = 5.0
|
|
97
|
+
max_checkpoint_bytes: int = 50 * (1024 ** 3)
|
|
98
|
+
min_checkpoint_bytes: int = 1024
|
|
99
|
+
|
|
100
|
+
runtime_module_alias: str = "topogpt3_inference_runtime"
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def scale_presets() -> Mapping[str, ScalePreset]:
|
|
104
|
+
"""Return the architecture preset table indexed by scale name."""
|
|
105
|
+
return {
|
|
106
|
+
"micro": ScalePreset(d_model=64, n_heads=4, n_layers=2,
|
|
107
|
+
max_seq_len=256),
|
|
108
|
+
"small": ScalePreset(d_model=256, n_heads=8, n_layers=6,
|
|
109
|
+
max_seq_len=512),
|
|
110
|
+
"medium": ScalePreset(d_model=512, n_heads=8, n_layers=12,
|
|
111
|
+
max_seq_len=1024),
|
|
112
|
+
"large": ScalePreset(d_model=512, n_heads=8, n_layers=13,
|
|
113
|
+
max_seq_len=1024),
|
|
114
|
+
"gpt2": ScalePreset(d_model=768, n_heads=12, n_layers=12,
|
|
115
|
+
max_seq_len=2048),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def preset(self) -> ScalePreset:
|
|
119
|
+
"""Return the resolved preset for the configured model scale."""
|
|
120
|
+
table = self.scale_presets()
|
|
121
|
+
if self.model_scale not in table:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Unknown model scale '{self.model_scale}'. "
|
|
124
|
+
f"Allowed: {sorted(table.keys())}"
|
|
125
|
+
)
|
|
126
|
+
return table[self.model_scale]
|
|
127
|
+
|
|
128
|
+
def validate(self) -> None:
|
|
129
|
+
"""Raise ValueError if any setting falls outside its safety bounds."""
|
|
130
|
+
if not (self.min_max_new_tokens
|
|
131
|
+
<= self.max_new_tokens <= self.max_max_new_tokens):
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"max_new_tokens={self.max_new_tokens} outside "
|
|
134
|
+
f"[{self.min_max_new_tokens}, {self.max_max_new_tokens}]"
|
|
135
|
+
)
|
|
136
|
+
if not (self.min_temperature
|
|
137
|
+
<= self.temperature <= self.max_temperature):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"temperature={self.temperature} outside "
|
|
140
|
+
f"[{self.min_temperature}, {self.max_temperature}]"
|
|
141
|
+
)
|
|
142
|
+
if self.top_k < self.min_top_k:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"top_k={self.top_k} must be >= {self.min_top_k}"
|
|
145
|
+
)
|
|
146
|
+
if not (self.min_repetition_penalty
|
|
147
|
+
<= self.repetition_penalty <= self.max_repetition_penalty):
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"repetition_penalty={self.repetition_penalty} outside "
|
|
150
|
+
f"[{self.min_repetition_penalty}, "
|
|
151
|
+
f"{self.max_repetition_penalty}]"
|
|
152
|
+
)
|
|
153
|
+
if self.result_separator_width <= 0:
|
|
154
|
+
raise ValueError("result_separator_width must be positive")
|
|
155
|
+
if self.elapsed_floor_seconds <= 0:
|
|
156
|
+
raise ValueError("elapsed_floor_seconds must be positive")
|
|
157
|
+
self.preset()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class InferenceLoggerFactory:
|
|
161
|
+
"""Builds a stdout-attached logger from inference settings."""
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def build(settings: InferenceSettings) -> logging.Logger:
|
|
165
|
+
"""Return a configured Logger with a single deduplicated stdout handler."""
|
|
166
|
+
logger = logging.getLogger(settings.log_name)
|
|
167
|
+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
168
|
+
logger.setLevel(level)
|
|
169
|
+
if not logger.handlers:
|
|
170
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
171
|
+
handler.setFormatter(logging.Formatter(
|
|
172
|
+
fmt=settings.log_format,
|
|
173
|
+
datefmt=settings.log_date_format,
|
|
174
|
+
))
|
|
175
|
+
logger.addHandler(handler)
|
|
176
|
+
logger.propagate = False
|
|
177
|
+
return logger
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SecurePathResolver:
|
|
181
|
+
"""Resolves filesystem paths while rejecting traversal outside their root."""
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def resolve_under(root: Path, *parts: str) -> Path:
|
|
185
|
+
"""Join `parts` under `root` and return the canonical resolved path.
|
|
186
|
+
|
|
187
|
+
Raises ValueError if the resolved path escapes `root`.
|
|
188
|
+
"""
|
|
189
|
+
resolved_root = root.resolve()
|
|
190
|
+
candidate = resolved_root.joinpath(*parts).resolve()
|
|
191
|
+
try:
|
|
192
|
+
candidate.relative_to(resolved_root)
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Path '{candidate}' escapes root '{resolved_root}'."
|
|
196
|
+
) from exc
|
|
197
|
+
return candidate
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def require_existing_file(path: Path,
|
|
201
|
+
expected_suffix: Optional[str] = None) -> Path:
|
|
202
|
+
"""Validate `path` points to an existing regular file with the expected suffix."""
|
|
203
|
+
if not path.exists():
|
|
204
|
+
raise FileNotFoundError(f"Required file not found: {path}")
|
|
205
|
+
if not path.is_file():
|
|
206
|
+
raise ValueError(f"Path is not a regular file: {path}")
|
|
207
|
+
if expected_suffix is not None and path.suffix != expected_suffix:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Expected suffix '{expected_suffix}', got '{path.suffix}': {path}"
|
|
210
|
+
)
|
|
211
|
+
return path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class SourceModuleLoader:
|
|
215
|
+
"""Resolves the TopoGPT3 runtime module via the package import system."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, settings: InferenceSettings, logger: logging.Logger):
|
|
218
|
+
self._settings = settings
|
|
219
|
+
self._logger = logger
|
|
220
|
+
|
|
221
|
+
def load(self) -> Any:
|
|
222
|
+
"""Return the topogpt3.train module which re-exports model symbols."""
|
|
223
|
+
from . import train as runtime_module
|
|
224
|
+
self._logger.info(
|
|
225
|
+
"Loaded TopoGPT3 runtime module: %s", runtime_module.__name__,
|
|
226
|
+
)
|
|
227
|
+
return runtime_module
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class CheckpointPaths:
|
|
231
|
+
"""Computes and validates checkpoint file paths under a single root."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, settings: InferenceSettings):
|
|
234
|
+
self._settings = settings
|
|
235
|
+
self._root = Path(settings.checkpoint_dir).resolve()
|
|
236
|
+
self._slot = SecurePathResolver.resolve_under(
|
|
237
|
+
self._root, settings.checkpoint_name,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def slot_dir(self) -> Path:
|
|
242
|
+
"""Directory holding the active checkpoint slot."""
|
|
243
|
+
return self._slot
|
|
244
|
+
|
|
245
|
+
def model_file(self) -> Path:
|
|
246
|
+
"""Resolved path to the safetensors weights file inside the slot."""
|
|
247
|
+
return SecurePathResolver.resolve_under(
|
|
248
|
+
self._slot, self._settings.model_filename,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def state_file(self) -> Path:
|
|
252
|
+
"""Resolved path to the JSON training-state file inside the slot."""
|
|
253
|
+
return SecurePathResolver.resolve_under(
|
|
254
|
+
self._slot, self._settings.state_filename,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def assert_ready(self) -> None:
|
|
258
|
+
"""Verify weights exist and the on-disk size lies within safety bounds."""
|
|
259
|
+
weights = self.model_file()
|
|
260
|
+
SecurePathResolver.require_existing_file(
|
|
261
|
+
weights, expected_suffix=Path(self._settings.model_filename).suffix,
|
|
262
|
+
)
|
|
263
|
+
size_bytes = weights.stat().st_size
|
|
264
|
+
if size_bytes < self._settings.min_checkpoint_bytes:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Checkpoint too small ({size_bytes} bytes): {weights}"
|
|
267
|
+
)
|
|
268
|
+
if size_bytes > self._settings.max_checkpoint_bytes:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Checkpoint exceeds allowed size "
|
|
271
|
+
f"({size_bytes} > {self._settings.max_checkpoint_bytes} bytes): "
|
|
272
|
+
f"{weights}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class WeightShapeProbe:
|
|
277
|
+
"""Reads tensor metadata from safetensors to infer architecture details."""
|
|
278
|
+
|
|
279
|
+
def __init__(self, settings: InferenceSettings, logger: logging.Logger):
|
|
280
|
+
self._settings = settings
|
|
281
|
+
self._logger = logger
|
|
282
|
+
|
|
283
|
+
def detect_n_kv_heads(self, weights_path: Path,
|
|
284
|
+
d_model: int, n_heads: int) -> Optional[int]:
|
|
285
|
+
"""Recover N_KV_HEADS used at training by inspecting the k_proj shape.
|
|
286
|
+
|
|
287
|
+
Returns None when the probe key is absent, signalling the caller to
|
|
288
|
+
fall back to scale defaults rather than guess.
|
|
289
|
+
"""
|
|
290
|
+
if n_heads <= 0 or d_model <= 0:
|
|
291
|
+
raise ValueError("d_model and n_heads must be positive")
|
|
292
|
+
if d_model % n_heads != 0:
|
|
293
|
+
raise ValueError("d_model must be divisible by n_heads")
|
|
294
|
+
d_head = d_model // n_heads
|
|
295
|
+
|
|
296
|
+
probe_key = self._settings.weight_probe_key
|
|
297
|
+
with safe_open(str(weights_path), framework="pt", device="cpu") as handle:
|
|
298
|
+
keys = set(handle.keys())
|
|
299
|
+
if probe_key not in keys:
|
|
300
|
+
self._logger.warning(
|
|
301
|
+
"Probe key '%s' missing from checkpoint; "
|
|
302
|
+
"falling back to scale defaults.",
|
|
303
|
+
probe_key,
|
|
304
|
+
)
|
|
305
|
+
return None
|
|
306
|
+
tensor = handle.get_tensor(probe_key)
|
|
307
|
+
k_dim = int(tensor.shape[0])
|
|
308
|
+
if k_dim % d_head != 0:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"k_proj output dim ({k_dim}) is not divisible by d_head ({d_head})"
|
|
311
|
+
)
|
|
312
|
+
n_kv = k_dim // d_head
|
|
313
|
+
self._logger.info(
|
|
314
|
+
"Detected N_KV_HEADS=%d (k_dim=%d, d_head=%d).",
|
|
315
|
+
n_kv, k_dim, d_head,
|
|
316
|
+
)
|
|
317
|
+
return n_kv
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TopoGPT2ConfigAligner:
|
|
321
|
+
"""Builds a TopoGPT2Config matching the loaded checkpoint and tokenizer."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, settings: InferenceSettings, source_module: Any,
|
|
324
|
+
logger: logging.Logger):
|
|
325
|
+
self._settings = settings
|
|
326
|
+
self._source = source_module
|
|
327
|
+
self._logger = logger
|
|
328
|
+
|
|
329
|
+
def build(self, n_kv_heads: Optional[int], vocab_size: int) -> Any:
|
|
330
|
+
"""Return a TopoGPT2Config dataclass ready to instantiate the model."""
|
|
331
|
+
cfg_cls = getattr(self._source, "TopoGPT2Config")
|
|
332
|
+
kwargs: Dict[str, Any] = {
|
|
333
|
+
"SCALE": self._settings.model_scale,
|
|
334
|
+
"DEVICE": self._settings.device,
|
|
335
|
+
"RANDOM_SEED": self._settings.seed,
|
|
336
|
+
"CHECKPOINT_DIR": self._settings.checkpoint_dir,
|
|
337
|
+
}
|
|
338
|
+
if n_kv_heads is not None and n_kv_heads > 0:
|
|
339
|
+
kwargs["N_KV_HEADS"] = n_kv_heads
|
|
340
|
+
cfg = cfg_cls(**kwargs)
|
|
341
|
+
cfg.VOCAB_SIZE = vocab_size
|
|
342
|
+
self._logger.info(
|
|
343
|
+
"Aligned TopoGPT2Config: scale=%s d_model=%d n_heads=%d "
|
|
344
|
+
"n_layers=%d n_kv_heads=%d vocab=%d max_seq=%d device=%s",
|
|
345
|
+
cfg.SCALE, cfg.D_MODEL, cfg.N_HEADS, cfg.N_LAYERS,
|
|
346
|
+
cfg.N_KV_HEADS, cfg.VOCAB_SIZE, cfg.MAX_SEQ_LEN, cfg.DEVICE,
|
|
347
|
+
)
|
|
348
|
+
return cfg
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class TokenizerFactory:
|
|
352
|
+
"""Builds a BPETokenizer instance using the configured encoding."""
|
|
353
|
+
|
|
354
|
+
def __init__(self, settings: InferenceSettings, source_module: Any):
|
|
355
|
+
self._settings = settings
|
|
356
|
+
self._source = source_module
|
|
357
|
+
|
|
358
|
+
def build(self) -> Any:
|
|
359
|
+
"""Return an instance of BPETokenizer bound to the configured encoding."""
|
|
360
|
+
tokenizer_cls = getattr(self._source, "BPETokenizer")
|
|
361
|
+
return tokenizer_cls(self._settings.tokenizer_encoding)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class GaussPatchApplier:
|
|
365
|
+
"""Applies the idempotent Gauss complex-multiply patch when enabled."""
|
|
366
|
+
|
|
367
|
+
def __init__(self, settings: InferenceSettings, source_module: Any,
|
|
368
|
+
logger: logging.Logger):
|
|
369
|
+
self._settings = settings
|
|
370
|
+
self._source = source_module
|
|
371
|
+
self._logger = logger
|
|
372
|
+
|
|
373
|
+
def apply_if_enabled(self) -> None:
|
|
374
|
+
"""Patch QuaternionSpectralLayer to use the 3-multiply Gauss contract."""
|
|
375
|
+
if not self._settings.apply_gauss_patch_at_load:
|
|
376
|
+
self._logger.info("Gauss complex-multiply patch disabled by settings.")
|
|
377
|
+
return
|
|
378
|
+
patch_fn = getattr(self._source, "apply_gauss_patch")
|
|
379
|
+
patch_fn(self._logger)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ModelAssembler:
|
|
383
|
+
"""Instantiates the model and loads weights from safetensors."""
|
|
384
|
+
|
|
385
|
+
def __init__(self, settings: InferenceSettings, source_module: Any,
|
|
386
|
+
logger: logging.Logger):
|
|
387
|
+
self._settings = settings
|
|
388
|
+
self._source = source_module
|
|
389
|
+
self._logger = logger
|
|
390
|
+
|
|
391
|
+
def assemble(self, aligned_cfg: Any,
|
|
392
|
+
paths: CheckpointPaths) -> torch.nn.Module:
|
|
393
|
+
"""Build the TopoGPT2 graph, load weights into it, and return it in eval mode."""
|
|
394
|
+
model_cls = getattr(self._source, "TopoGPT2")
|
|
395
|
+
model = model_cls(aligned_cfg)
|
|
396
|
+
model.to(aligned_cfg.DEVICE)
|
|
397
|
+
model.eval()
|
|
398
|
+
|
|
399
|
+
params = model.count_params()
|
|
400
|
+
self._logger.info(
|
|
401
|
+
"Model parameters: total=%s trainable=%s",
|
|
402
|
+
f"{params['total']:,}", f"{params['trainable']:,}",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
weights_path = paths.model_file()
|
|
406
|
+
self._logger.info("Loading weights from %s", weights_path)
|
|
407
|
+
state_dict = load_file(str(weights_path), device=aligned_cfg.DEVICE)
|
|
408
|
+
missing, unexpected = model.load_state_dict(
|
|
409
|
+
state_dict, strict=self._settings.strict_state_dict,
|
|
410
|
+
)
|
|
411
|
+
if missing:
|
|
412
|
+
self._logger.info("Missing keys (likely tied weights): %s", missing)
|
|
413
|
+
if unexpected:
|
|
414
|
+
self._logger.info("Unexpected keys ignored: %s", unexpected)
|
|
415
|
+
self._logger.info("Weights loaded successfully.")
|
|
416
|
+
return model
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class SeedSynchronizer:
|
|
420
|
+
"""Applies deterministic seeds across torch, CUDA and the model package."""
|
|
421
|
+
|
|
422
|
+
def __init__(self, settings: InferenceSettings, source_module: Any,
|
|
423
|
+
logger: logging.Logger):
|
|
424
|
+
self._settings = settings
|
|
425
|
+
self._source = source_module
|
|
426
|
+
self._logger = logger
|
|
427
|
+
|
|
428
|
+
def apply(self) -> None:
|
|
429
|
+
"""Seed all relevant RNGs using the model package helper when available."""
|
|
430
|
+
torch.manual_seed(self._settings.seed)
|
|
431
|
+
if torch.cuda.is_available():
|
|
432
|
+
torch.cuda.manual_seed_all(self._settings.seed)
|
|
433
|
+
set_seed_fn = getattr(self._source, "set_seed", None)
|
|
434
|
+
if callable(set_seed_fn):
|
|
435
|
+
set_seed_fn(self._settings.seed, self._settings.device)
|
|
436
|
+
self._logger.info(
|
|
437
|
+
"Seed synchronized (seed=%d device=%s).",
|
|
438
|
+
self._settings.seed, self._settings.device,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@dataclass(frozen=True)
|
|
443
|
+
class SamplingPolicy:
|
|
444
|
+
"""Immutable sampling parameters consumed by the generation engine."""
|
|
445
|
+
|
|
446
|
+
max_new_tokens: int
|
|
447
|
+
temperature: float
|
|
448
|
+
top_k: int
|
|
449
|
+
repetition_penalty: float
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def from_settings(cls, settings: InferenceSettings) -> "SamplingPolicy":
|
|
453
|
+
"""Construct a SamplingPolicy from inference settings."""
|
|
454
|
+
return cls(
|
|
455
|
+
max_new_tokens=settings.max_new_tokens,
|
|
456
|
+
temperature=settings.temperature,
|
|
457
|
+
top_k=settings.top_k,
|
|
458
|
+
repetition_penalty=settings.repetition_penalty,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@dataclass(frozen=True)
|
|
463
|
+
class GenerationReport:
|
|
464
|
+
"""Quantitative summary of a single generation call."""
|
|
465
|
+
|
|
466
|
+
prompt: str
|
|
467
|
+
output: str
|
|
468
|
+
prompt_tokens: int
|
|
469
|
+
new_tokens: int
|
|
470
|
+
elapsed_seconds: float
|
|
471
|
+
|
|
472
|
+
def tokens_per_second(self, elapsed_floor: float) -> float:
|
|
473
|
+
"""Return throughput in tokens/sec, clamped to avoid divide-by-zero."""
|
|
474
|
+
return self.new_tokens / max(self.elapsed_seconds, elapsed_floor)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class GenerationEngine:
|
|
478
|
+
"""Runs autoregressive sampling against a loaded model and tokenizer."""
|
|
479
|
+
|
|
480
|
+
def __init__(self, settings: InferenceSettings, logger: logging.Logger):
|
|
481
|
+
self._settings = settings
|
|
482
|
+
self._logger = logger
|
|
483
|
+
|
|
484
|
+
@torch.no_grad()
|
|
485
|
+
def run(self, model: torch.nn.Module, tokenizer: Any, prompt: str,
|
|
486
|
+
policy: SamplingPolicy) -> GenerationReport:
|
|
487
|
+
"""Generate a completion for `prompt` and return a GenerationReport."""
|
|
488
|
+
prompt_ids = tokenizer.encode(prompt)
|
|
489
|
+
if len(prompt_ids) == 0:
|
|
490
|
+
raise ValueError("Prompt encodes to zero tokens; refusing to generate.")
|
|
491
|
+
|
|
492
|
+
device = self._settings.device
|
|
493
|
+
input_ids = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
494
|
+
|
|
495
|
+
self._logger.info(
|
|
496
|
+
"Sampling: temp=%.3f top_k=%d rep_pen=%.3f "
|
|
497
|
+
"max_new=%d prompt_tokens=%d",
|
|
498
|
+
policy.temperature, policy.top_k, policy.repetition_penalty,
|
|
499
|
+
policy.max_new_tokens, len(prompt_ids),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
start = time.time()
|
|
503
|
+
if self._settings.auto_continue:
|
|
504
|
+
output_ids = model.generate_with_continuation(
|
|
505
|
+
input_ids,
|
|
506
|
+
tokenizer=tokenizer,
|
|
507
|
+
max_new_tokens=policy.max_new_tokens,
|
|
508
|
+
temperature=policy.temperature,
|
|
509
|
+
top_k=policy.top_k,
|
|
510
|
+
repetition_penalty=policy.repetition_penalty,
|
|
511
|
+
max_continuations=self._settings.max_continuations,
|
|
512
|
+
tail_lines=self._settings.continuation_tail_lines,
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
output_ids = model.generate(
|
|
516
|
+
input_ids,
|
|
517
|
+
max_new_tokens=policy.max_new_tokens,
|
|
518
|
+
temperature=policy.temperature,
|
|
519
|
+
top_k=policy.top_k,
|
|
520
|
+
repetition_penalty=policy.repetition_penalty,
|
|
521
|
+
)
|
|
522
|
+
elapsed = time.time() - start
|
|
523
|
+
|
|
524
|
+
full_text = tokenizer.decode(output_ids[0].tolist())
|
|
525
|
+
new_tokens = int(output_ids.shape[1]) - len(prompt_ids)
|
|
526
|
+
return GenerationReport(
|
|
527
|
+
prompt=prompt,
|
|
528
|
+
output=full_text,
|
|
529
|
+
prompt_tokens=len(prompt_ids),
|
|
530
|
+
new_tokens=new_tokens,
|
|
531
|
+
elapsed_seconds=elapsed,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class ResultRenderer:
|
|
536
|
+
"""Prints a GenerationReport to stdout using settings-defined formatting."""
|
|
537
|
+
|
|
538
|
+
def __init__(self, settings: InferenceSettings, logger: logging.Logger):
|
|
539
|
+
self._settings = settings
|
|
540
|
+
self._logger = logger
|
|
541
|
+
|
|
542
|
+
def render(self, report: GenerationReport) -> None:
|
|
543
|
+
"""Emit a banner with prompt and completion, plus a throughput log line."""
|
|
544
|
+
s = self._settings
|
|
545
|
+
sep = s.result_separator_char * s.result_separator_width
|
|
546
|
+
completion = (
|
|
547
|
+
report.output[len(report.prompt):]
|
|
548
|
+
if report.output.startswith(report.prompt)
|
|
549
|
+
else report.output
|
|
550
|
+
)
|
|
551
|
+
self._logger.info(
|
|
552
|
+
"Generated %d new tokens in %.2fs (%.1f tok/s)",
|
|
553
|
+
report.new_tokens, report.elapsed_seconds,
|
|
554
|
+
report.tokens_per_second(s.elapsed_floor_seconds),
|
|
555
|
+
)
|
|
556
|
+
out = sys.stdout
|
|
557
|
+
out.write("\n" + sep + "\n")
|
|
558
|
+
out.write(f"[{s.prompt_label}] {report.prompt}\n")
|
|
559
|
+
out.write(f"[{s.completion_label}]\n{completion}\n")
|
|
560
|
+
out.write(sep + "\n")
|
|
561
|
+
out.flush()
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class InferencePipeline:
|
|
565
|
+
"""Orchestrator wiring loader, builder, engine and renderer."""
|
|
566
|
+
|
|
567
|
+
def __init__(self, settings: InferenceSettings,
|
|
568
|
+
logger: Optional[logging.Logger] = None):
|
|
569
|
+
settings.validate()
|
|
570
|
+
self._settings = settings
|
|
571
|
+
self._logger = logger or InferenceLoggerFactory.build(settings)
|
|
572
|
+
|
|
573
|
+
def execute(self) -> GenerationReport:
|
|
574
|
+
"""Run the full inference pipeline end-to-end and return the report."""
|
|
575
|
+
source_module = SourceModuleLoader(
|
|
576
|
+
self._settings, self._logger,
|
|
577
|
+
).load()
|
|
578
|
+
|
|
579
|
+
SeedSynchronizer(
|
|
580
|
+
self._settings, source_module, self._logger,
|
|
581
|
+
).apply()
|
|
582
|
+
|
|
583
|
+
GaussPatchApplier(
|
|
584
|
+
self._settings, source_module, self._logger,
|
|
585
|
+
).apply_if_enabled()
|
|
586
|
+
|
|
587
|
+
paths = CheckpointPaths(self._settings)
|
|
588
|
+
paths.assert_ready()
|
|
589
|
+
|
|
590
|
+
preset = self._settings.preset()
|
|
591
|
+
n_kv = WeightShapeProbe(
|
|
592
|
+
self._settings, self._logger,
|
|
593
|
+
).detect_n_kv_heads(
|
|
594
|
+
paths.model_file(),
|
|
595
|
+
d_model=preset.d_model,
|
|
596
|
+
n_heads=preset.n_heads,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
tokenizer = TokenizerFactory(self._settings, source_module).build()
|
|
600
|
+
aligned_cfg = TopoGPT2ConfigAligner(
|
|
601
|
+
self._settings, source_module, self._logger,
|
|
602
|
+
).build(n_kv_heads=n_kv, vocab_size=tokenizer.vocab_size)
|
|
603
|
+
|
|
604
|
+
model = ModelAssembler(
|
|
605
|
+
self._settings, source_module, self._logger,
|
|
606
|
+
).assemble(aligned_cfg, paths)
|
|
607
|
+
|
|
608
|
+
engine = GenerationEngine(self._settings, self._logger)
|
|
609
|
+
report = engine.run(
|
|
610
|
+
model, tokenizer, self._settings.prompt,
|
|
611
|
+
SamplingPolicy.from_settings(self._settings),
|
|
612
|
+
)
|
|
613
|
+
ResultRenderer(self._settings, self._logger).render(report)
|
|
614
|
+
return report
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class CliArgumentParser:
|
|
618
|
+
"""Translates command-line arguments into an InferenceSettings instance."""
|
|
619
|
+
|
|
620
|
+
@staticmethod
|
|
621
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
622
|
+
"""Return the configured argparse.ArgumentParser."""
|
|
623
|
+
defaults = InferenceSettings()
|
|
624
|
+
parser = argparse.ArgumentParser(
|
|
625
|
+
description=(
|
|
626
|
+
"TopoGPT3 inference engine "
|
|
627
|
+
"(Grassmannian-trained, Gauss complex-multiply enabled)."
|
|
628
|
+
),
|
|
629
|
+
)
|
|
630
|
+
parser.add_argument(
|
|
631
|
+
"--prompt", type=str, default=defaults.prompt,
|
|
632
|
+
help="Code prefix to complete.",
|
|
633
|
+
)
|
|
634
|
+
parser.add_argument(
|
|
635
|
+
"--scale", type=str, default=defaults.model_scale,
|
|
636
|
+
choices=sorted(InferenceSettings.scale_presets().keys()),
|
|
637
|
+
help="Model scale preset.",
|
|
638
|
+
)
|
|
639
|
+
parser.add_argument(
|
|
640
|
+
"--max-new", type=int, default=defaults.max_new_tokens,
|
|
641
|
+
help="Maximum number of new tokens to generate.",
|
|
642
|
+
)
|
|
643
|
+
parser.add_argument(
|
|
644
|
+
"--temp", type=float, default=defaults.temperature,
|
|
645
|
+
help="Sampling temperature (lower means more deterministic).",
|
|
646
|
+
)
|
|
647
|
+
parser.add_argument(
|
|
648
|
+
"--top-k", type=int, default=defaults.top_k,
|
|
649
|
+
help="Top-k filtering (0 disables it).",
|
|
650
|
+
)
|
|
651
|
+
parser.add_argument(
|
|
652
|
+
"--rep-penalty", type=float, default=defaults.repetition_penalty,
|
|
653
|
+
help="Repetition penalty (values above 1 reduce repeats).",
|
|
654
|
+
)
|
|
655
|
+
parser.add_argument(
|
|
656
|
+
"--device", type=str, default=defaults.device,
|
|
657
|
+
help="Compute device, e.g. cuda, cpu, cuda:0.",
|
|
658
|
+
)
|
|
659
|
+
parser.add_argument(
|
|
660
|
+
"--ckpt-dir", type=str, default=defaults.checkpoint_dir,
|
|
661
|
+
help="Root directory holding TopoGPT3 checkpoints.",
|
|
662
|
+
)
|
|
663
|
+
parser.add_argument(
|
|
664
|
+
"--ckpt-name", type=str, default=defaults.checkpoint_name,
|
|
665
|
+
help="Checkpoint slot to load (e.g. 'last').",
|
|
666
|
+
)
|
|
667
|
+
parser.add_argument(
|
|
668
|
+
"--source-dir", type=str, default=defaults.source_directory,
|
|
669
|
+
help="Directory containing train.py and model.py.",
|
|
670
|
+
)
|
|
671
|
+
parser.add_argument(
|
|
672
|
+
"--no-gauss", action="store_true",
|
|
673
|
+
help="Disable the Gauss complex-multiply patch at load.",
|
|
674
|
+
)
|
|
675
|
+
parser.add_argument(
|
|
676
|
+
"--strict-load", action="store_true",
|
|
677
|
+
help="Require strict state_dict matching when loading weights.",
|
|
678
|
+
)
|
|
679
|
+
parser.add_argument(
|
|
680
|
+
"--seed", type=int, default=defaults.seed,
|
|
681
|
+
help="Deterministic seed for sampling reproducibility.",
|
|
682
|
+
)
|
|
683
|
+
parser.add_argument(
|
|
684
|
+
"--auto-continue", "-C", action="store_true",
|
|
685
|
+
default=defaults.auto_continue,
|
|
686
|
+
help="Auto-continue truncated responses by feeding tail lines back.",
|
|
687
|
+
)
|
|
688
|
+
parser.add_argument(
|
|
689
|
+
"--max-continuations", type=int, default=defaults.max_continuations,
|
|
690
|
+
help="Max number of continuation rounds (requires --auto-continue).",
|
|
691
|
+
)
|
|
692
|
+
parser.add_argument(
|
|
693
|
+
"--log-level", type=str, default=defaults.log_level,
|
|
694
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
695
|
+
help="Logging verbosity.",
|
|
696
|
+
)
|
|
697
|
+
return parser
|
|
698
|
+
|
|
699
|
+
@staticmethod
|
|
700
|
+
def parse(argv: Optional[List[str]] = None) -> InferenceSettings:
|
|
701
|
+
"""Parse `argv` (or sys.argv) and return a populated InferenceSettings."""
|
|
702
|
+
namespace = CliArgumentParser.build_parser().parse_args(argv)
|
|
703
|
+
return InferenceSettings(
|
|
704
|
+
prompt=namespace.prompt,
|
|
705
|
+
model_scale=namespace.scale,
|
|
706
|
+
max_new_tokens=namespace.max_new,
|
|
707
|
+
temperature=namespace.temp,
|
|
708
|
+
top_k=namespace.top_k,
|
|
709
|
+
repetition_penalty=namespace.rep_penalty,
|
|
710
|
+
device=namespace.device,
|
|
711
|
+
checkpoint_dir=namespace.ckpt_dir,
|
|
712
|
+
checkpoint_name=namespace.ckpt_name,
|
|
713
|
+
source_directory=namespace.source_dir,
|
|
714
|
+
apply_gauss_patch_at_load=not namespace.no_gauss,
|
|
715
|
+
strict_state_dict=namespace.strict_load,
|
|
716
|
+
seed=namespace.seed,
|
|
717
|
+
auto_continue=namespace.auto_continue,
|
|
718
|
+
max_continuations=namespace.max_continuations,
|
|
719
|
+
log_level=namespace.log_level,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
724
|
+
"""CLI entry point. Returns a process exit code."""
|
|
725
|
+
settings = CliArgumentParser.parse(argv)
|
|
726
|
+
InferencePipeline(settings).execute()
|
|
727
|
+
return 0
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
if __name__ == "__main__":
|
|
731
|
+
sys.exit(main())
|