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/continuation.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Auto-continuation engine: detects truncated responses and feeds the last
|
|
4
|
+
incomplete lines back so the model can resume where it left off.
|
|
5
|
+
|
|
6
|
+
Used by both the standard inference pipeline and the HRM "thinking" mode.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import List, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── Fence / block structures ────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_OPENING = {"{": "}", "(": ")", "[": "]", "<": ">"}
|
|
18
|
+
_CLOSING = {v: k for k, v in _OPENING.items()}
|
|
19
|
+
_OPENERS = frozenset(_OPENING)
|
|
20
|
+
_CLOSERS = frozenset(_CLOSING)
|
|
21
|
+
|
|
22
|
+
_CODE_FENCE_RE = re.compile(r"^```(\w*)$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _count_unclosed_brackets(text: str) -> int:
|
|
26
|
+
stack: List[str] = []
|
|
27
|
+
for ch in text:
|
|
28
|
+
if ch in _OPENERS:
|
|
29
|
+
stack.append(ch)
|
|
30
|
+
elif ch in _CLOSERS:
|
|
31
|
+
if stack and _CLOSING.get(ch) == stack[-1]:
|
|
32
|
+
stack.pop()
|
|
33
|
+
return len(stack)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _count_unclosed_fences(text: str) -> int:
|
|
37
|
+
fences = 0
|
|
38
|
+
for line in text.splitlines():
|
|
39
|
+
m = _CODE_FENCE_RE.match(line.strip())
|
|
40
|
+
if m:
|
|
41
|
+
fences += 1
|
|
42
|
+
return fences % 2 # 0 = balanced, 1 = unclosed
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_response_complete(text: str, min_chars: int = 10) -> bool:
|
|
46
|
+
"""Heuristic to decide whether a model response looks finished.
|
|
47
|
+
|
|
48
|
+
Returns True when the response seems naturally complete (no need to
|
|
49
|
+
continue), False when it appears truncated and continuation may help.
|
|
50
|
+
"""
|
|
51
|
+
stripped = text.rstrip()
|
|
52
|
+
|
|
53
|
+
if len(stripped) < min_chars:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
if _count_unclosed_fences(stripped):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
if _count_unclosed_brackets(stripped) > 0:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
if not stripped:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
last_ch = stripped[-1]
|
|
66
|
+
if last_ch in {":", ",", ";", "\\"}:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
if last_ch in {"\n", ".", "!", "?", ")", "}", "]", "`", '"', "'"}:
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_tail_for_continuation(text: str, tail_lines: int = 2,
|
|
76
|
+
tail_chars: int = 512) -> str:
|
|
77
|
+
"""Return the last N lines (or up to tail_chars) of `text` as a
|
|
78
|
+
continuation prefix to feed back into the model.
|
|
79
|
+
|
|
80
|
+
The returned string can be prepended as context for the model's next
|
|
81
|
+
generation call so it continues naturally from that point.
|
|
82
|
+
"""
|
|
83
|
+
stripped = text.rstrip()
|
|
84
|
+
if not stripped:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
lines: List[str] = text.splitlines()
|
|
88
|
+
result: List[str] = []
|
|
89
|
+
total_chars = 0
|
|
90
|
+
|
|
91
|
+
for line in reversed(lines):
|
|
92
|
+
candidate = line
|
|
93
|
+
new_total = total_chars + len(candidate) + 1
|
|
94
|
+
if len(result) >= tail_lines:
|
|
95
|
+
break
|
|
96
|
+
if new_total > tail_chars:
|
|
97
|
+
break
|
|
98
|
+
result.append(candidate)
|
|
99
|
+
total_chars = new_total
|
|
100
|
+
|
|
101
|
+
result.reverse()
|
|
102
|
+
return "\n".join(result)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def split_at_last_newline(text: str) -> Tuple[str, str]:
|
|
106
|
+
"""Split `text` at the last newline.
|
|
107
|
+
|
|
108
|
+
Returns (prefix_without_last_line, last_line).
|
|
109
|
+
Useful for discarding a trailing incomplete line before continuation.
|
|
110
|
+
"""
|
|
111
|
+
if "\n" not in text:
|
|
112
|
+
return "", text
|
|
113
|
+
idx = text.rfind("\n")
|
|
114
|
+
return text[:idx], text[idx + 1:]
|
topogpt3/ewc.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Elastic Weight Consolidation (EWC) and Experience Replay for TopoGPT3.
|
|
4
|
+
|
|
5
|
+
Provides EWCRegularizer for regularizing against catastrophic forgetting
|
|
6
|
+
across curriculum tiers, and ReplayBuffer for interleaving data from
|
|
7
|
+
previous tiers during training.
|
|
8
|
+
|
|
9
|
+
Author: Gris Iscomeback
|
|
10
|
+
License: GPL v3
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
import torch.nn as nn
|
|
20
|
+
import torch.nn.functional as F
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EWCRegularizer:
|
|
26
|
+
"""
|
|
27
|
+
Elastic Weight Consolidation regularizer.
|
|
28
|
+
|
|
29
|
+
Computes the diagonal Fisher information matrix over a dataset and uses
|
|
30
|
+
it to penalize large deviations from the parameters that were optimal
|
|
31
|
+
for previous tasks (tiers). The penalty is:
|
|
32
|
+
|
|
33
|
+
L_EWC = lambda * sum_i F_i * (theta_i - theta_opt_i)^2
|
|
34
|
+
|
|
35
|
+
where F_i is the Fisher information for parameter i and theta_opt_i
|
|
36
|
+
is the parameter value at the end of the previous task.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
ewc = EWCRegularizer(model, lambda_ewc=5000.0)
|
|
40
|
+
ewc.compute_fisher(dataloader, vocab_size, device)
|
|
41
|
+
# ... train on new tier ...
|
|
42
|
+
loss = task_loss + ewc.penalty(model)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
model: nn.Module,
|
|
48
|
+
lambda_ewc: float = 5000.0,
|
|
49
|
+
fisher_num_samples: int = 50,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Args:
|
|
53
|
+
model: The model to regularize.
|
|
54
|
+
lambda_ewc: Penalty strength scaling the Fisher-weighted
|
|
55
|
+
parameter deviation loss.
|
|
56
|
+
fisher_num_samples: Number of batches used when computing the
|
|
57
|
+
Fisher information matrix (stored for reference).
|
|
58
|
+
"""
|
|
59
|
+
self.model = model
|
|
60
|
+
self.lambda_ewc = lambda_ewc
|
|
61
|
+
self.fisher_num_samples = fisher_num_samples
|
|
62
|
+
|
|
63
|
+
# {param_name: Fisher information tensor}
|
|
64
|
+
self.fisher: Dict[str, torch.Tensor] = {}
|
|
65
|
+
# {param_name: parameter snapshot tensor}
|
|
66
|
+
self.old_params: Dict[str, torch.Tensor] = {}
|
|
67
|
+
|
|
68
|
+
def compute_fisher(
|
|
69
|
+
self,
|
|
70
|
+
dataloader: Iterable,
|
|
71
|
+
vocab_size: int,
|
|
72
|
+
device: torch.device,
|
|
73
|
+
max_batches: int = 50,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Compute the diagonal Fisher information matrix and snapshot the
|
|
77
|
+
current parameters as the reference point for the EWC penalty.
|
|
78
|
+
|
|
79
|
+
For each batch: forward pass -> per-sample NLL -> backprop ->
|
|
80
|
+
accumulate squared gradients. Handles AMP autocast.
|
|
81
|
+
|
|
82
|
+
After calling this method, ``self.fisher`` and ``self.old_params``
|
|
83
|
+
are populated and ready for ``penalty()``.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
dataloader: Iterable yielding ``(bx, by)`` batches.
|
|
87
|
+
vocab_size: Size of the vocabulary (for reshaping logits).
|
|
88
|
+
device: Device where the model lives.
|
|
89
|
+
max_batches: Maximum number of batches to use for estimation.
|
|
90
|
+
"""
|
|
91
|
+
was_training = self.model.training
|
|
92
|
+
self.model.train()
|
|
93
|
+
|
|
94
|
+
# Ensure all parameters participate in gradient computation
|
|
95
|
+
prev_requires_grad: Dict[str, bool] = {}
|
|
96
|
+
for name, p in self.model.named_parameters():
|
|
97
|
+
prev_requires_grad[name] = p.requires_grad
|
|
98
|
+
p.requires_grad_(True)
|
|
99
|
+
|
|
100
|
+
# Move model to CPU to avoid GPU OOM during backward
|
|
101
|
+
original_device = device
|
|
102
|
+
self.model.to("cpu")
|
|
103
|
+
cpu_device = torch.device("cpu")
|
|
104
|
+
|
|
105
|
+
fisher: Dict[str, torch.Tensor] = {}
|
|
106
|
+
|
|
107
|
+
batch_count = 0
|
|
108
|
+
for batch_idx, (bx, by) in enumerate(dataloader):
|
|
109
|
+
if batch_idx >= max_batches:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
bx = bx.to(cpu_device, non_blocking=True)
|
|
113
|
+
by = by.to(cpu_device, non_blocking=True)
|
|
114
|
+
|
|
115
|
+
self.model.zero_grad(set_to_none=True)
|
|
116
|
+
|
|
117
|
+
# No AMP on CPU — plain float32 forward + backward
|
|
118
|
+
logits, _recon_loss, _ = self.model(bx)
|
|
119
|
+
shift_logits = logits[:, :-1, :].contiguous()
|
|
120
|
+
shift_labels = by[:, 1:].contiguous()
|
|
121
|
+
loss = F.cross_entropy(
|
|
122
|
+
shift_logits.view(-1, vocab_size),
|
|
123
|
+
shift_labels.view(-1),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
loss.backward()
|
|
127
|
+
|
|
128
|
+
for name, p in self.model.named_parameters():
|
|
129
|
+
if p.grad is not None:
|
|
130
|
+
if name not in fisher:
|
|
131
|
+
fisher[name] = torch.zeros_like(p.data)
|
|
132
|
+
fisher[name] += p.grad.data.pow(2)
|
|
133
|
+
|
|
134
|
+
self.model.zero_grad(set_to_none=True)
|
|
135
|
+
batch_count += 1
|
|
136
|
+
|
|
137
|
+
# Average over batches
|
|
138
|
+
if batch_count > 0:
|
|
139
|
+
for name in fisher:
|
|
140
|
+
fisher[name] /= batch_count
|
|
141
|
+
|
|
142
|
+
# Move model back to original device
|
|
143
|
+
self.model.to(original_device)
|
|
144
|
+
|
|
145
|
+
# Move fisher tensors to original device for penalty computation
|
|
146
|
+
self.fisher = {k: v.to(original_device) for k, v in fisher.items()}
|
|
147
|
+
|
|
148
|
+
# Snapshot current parameters as the reference point
|
|
149
|
+
self.old_params = {}
|
|
150
|
+
for name, p in self.model.named_parameters():
|
|
151
|
+
if name in self.fisher:
|
|
152
|
+
self.old_params[name] = p.data.detach().clone()
|
|
153
|
+
|
|
154
|
+
# Restore requires_grad flags
|
|
155
|
+
for name, p in self.model.named_parameters():
|
|
156
|
+
if name in prev_requires_grad:
|
|
157
|
+
p.requires_grad_(prev_requires_grad[name])
|
|
158
|
+
|
|
159
|
+
# Restore training mode
|
|
160
|
+
if not was_training:
|
|
161
|
+
self.model.eval()
|
|
162
|
+
|
|
163
|
+
logger.info(
|
|
164
|
+
f"EWC fisher computed over {batch_count} batches, "
|
|
165
|
+
f"{len(self.fisher)} parameters tracked."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def penalty(self, model: nn.Module) -> torch.Tensor:
|
|
169
|
+
"""
|
|
170
|
+
Compute the EWC regularization loss.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Scalar tensor: lambda * sum_i F_i * (theta_i - theta_opt_i)^2
|
|
174
|
+
"""
|
|
175
|
+
if not self.fisher or not self.old_params:
|
|
176
|
+
return torch.tensor(0.0, device=next(model.parameters()).device)
|
|
177
|
+
|
|
178
|
+
loss = torch.tensor(0.0, device=next(model.parameters()).device)
|
|
179
|
+
for name, p in model.named_parameters():
|
|
180
|
+
if name in self.fisher and name in self.old_params:
|
|
181
|
+
loss = loss + (
|
|
182
|
+
self.fisher[name] * (p - self.old_params[name]).pow(2)
|
|
183
|
+
).sum()
|
|
184
|
+
return self.lambda_ewc * loss
|
|
185
|
+
|
|
186
|
+
def save_state(self, path: str) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Persist the Fisher information matrix and old parameter snapshots
|
|
189
|
+
to a single ``.pt`` file.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path: Destination file path (should end in ``.pt``).
|
|
193
|
+
"""
|
|
194
|
+
state = {
|
|
195
|
+
"fisher": self.fisher,
|
|
196
|
+
"old_params": self.old_params,
|
|
197
|
+
"lambda_ewc": self.lambda_ewc,
|
|
198
|
+
"fisher_num_samples": self.fisher_num_samples,
|
|
199
|
+
}
|
|
200
|
+
torch.save(state, path)
|
|
201
|
+
logger.info(f"EWC state saved to {path}")
|
|
202
|
+
|
|
203
|
+
def load_state(self, path: str) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Load a previously saved EWC state.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
path: Path to the ``.pt`` file created by ``save_state``.
|
|
209
|
+
"""
|
|
210
|
+
state = torch.load(path, map_location="cpu", weights_only=True)
|
|
211
|
+
self.fisher = state["fisher"]
|
|
212
|
+
self.old_params = state["old_params"]
|
|
213
|
+
self.lambda_ewc = state.get("lambda_ewc", self.lambda_ewc)
|
|
214
|
+
self.fisher_num_samples = state.get(
|
|
215
|
+
"fisher_num_samples", self.fisher_num_samples
|
|
216
|
+
)
|
|
217
|
+
logger.info(
|
|
218
|
+
f"EWC state loaded from {path}: "
|
|
219
|
+
f"{len(self.fisher)} params, lambda={self.lambda_ewc}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ReplayBuffer:
|
|
224
|
+
"""
|
|
225
|
+
Experience Replay buffer for interleaving data from previous curriculum
|
|
226
|
+
tiers during training.
|
|
227
|
+
|
|
228
|
+
Maintains references to the training dataloaders of previously completed
|
|
229
|
+
tiers and mixes their batches into the current tier's training loop at a
|
|
230
|
+
configurable ratio.
|
|
231
|
+
|
|
232
|
+
Usage:
|
|
233
|
+
replay = ReplayBuffer(max_tiers=3, replay_ratio=0.25)
|
|
234
|
+
replay.add_tier(0, tier0_train_dl)
|
|
235
|
+
replay.add_tier(1, tier1_train_dl)
|
|
236
|
+
# During tier-2 training, for each step:
|
|
237
|
+
bx, by = replay.sample_batch(current_batch_size=4)
|
|
238
|
+
# bx, by contains replay data from tiers 0,1
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(self, max_tiers: int = 3, replay_ratio: float = 0.25):
|
|
242
|
+
"""
|
|
243
|
+
Args:
|
|
244
|
+
max_tiers: Maximum number of previous tiers to remember.
|
|
245
|
+
When exceeded, the oldest tier is dropped.
|
|
246
|
+
replay_ratio: Fraction of the returned batch that comes from
|
|
247
|
+
replayed tiers (0.0 = no replay, 1.0 = all replay).
|
|
248
|
+
"""
|
|
249
|
+
self.max_tiers = max_tiers
|
|
250
|
+
self.replay_ratio = replay_ratio
|
|
251
|
+
|
|
252
|
+
# List of (tier_index, dataloader) tuples
|
|
253
|
+
self._tiers: List[Tuple[int, Iterable]] = []
|
|
254
|
+
|
|
255
|
+
# Cyclic iteration state per tier: stores the iterator and whether
|
|
256
|
+
# it has been exhausted (we reset it on exhaustion).
|
|
257
|
+
self._iterators: Dict[int, object] = {}
|
|
258
|
+
|
|
259
|
+
def add_tier(self, tier_index: int, dataloader: Iterable) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Register a tier's training dataloader for future replay.
|
|
262
|
+
|
|
263
|
+
If the buffer is full (``max_tiers`` tiers stored), the oldest
|
|
264
|
+
entry is evicted.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
tier_index: Integer tier identifier (e.g. 0, 1, 2, ...).
|
|
268
|
+
dataloader: The training dataloader for that tier.
|
|
269
|
+
"""
|
|
270
|
+
# Evict oldest if at capacity
|
|
271
|
+
if len(self._tiers) >= self.max_tiers:
|
|
272
|
+
evicted_idx, _ = self._tiers.pop(0)
|
|
273
|
+
self._iterators.pop(evicted_idx, None)
|
|
274
|
+
logger.debug(f"ReplayBuffer: evicted tier {evicted_idx}")
|
|
275
|
+
|
|
276
|
+
self._tiers.append((tier_index, dataloader))
|
|
277
|
+
logger.debug(
|
|
278
|
+
f"ReplayBuffer: added tier {tier_index}, "
|
|
279
|
+
f"total stored tiers = {len(self._tiers)}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _get_next_batch(
|
|
283
|
+
self, tier_index: int, dataloader: Iterable
|
|
284
|
+
) -> Optional[Tuple[torch.Tensor, torch.Tensor]]:
|
|
285
|
+
"""
|
|
286
|
+
Get the next batch from a tier's dataloader, cycling if necessary.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
tier_index: The tier identifier.
|
|
290
|
+
dataloader: The tier's dataloader.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
``(bx, by)`` tensors or ``None`` if the dataloader is empty.
|
|
294
|
+
"""
|
|
295
|
+
if tier_index not in self._iterators:
|
|
296
|
+
self._iterators[tier_index] = iter(dataloader)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
batch = next(self._iterators[tier_index])
|
|
300
|
+
except StopIteration:
|
|
301
|
+
# Reset iterator and try once more
|
|
302
|
+
self._iterators[tier_index] = iter(dataloader)
|
|
303
|
+
try:
|
|
304
|
+
batch = next(self._iterators[tier_index])
|
|
305
|
+
except StopIteration:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
if isinstance(batch, (list, tuple)) and len(batch) >= 2:
|
|
309
|
+
return batch[0], batch[1]
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def sample_batch(
|
|
313
|
+
self, current_batch_size: int
|
|
314
|
+
) -> Optional[Tuple[torch.Tensor, torch.Tensor]]:
|
|
315
|
+
"""
|
|
316
|
+
Sample a mixed replay batch from ALL stored previous tiers.
|
|
317
|
+
|
|
318
|
+
The returned batch has size approximately
|
|
319
|
+
``replay_ratio * current_batch_size`` and is drawn uniformly from
|
|
320
|
+
all stored tiers, cycling through their dataloaders.
|
|
321
|
+
|
|
322
|
+
The caller is responsible for concatenating this with the current
|
|
323
|
+
tier's batch::
|
|
324
|
+
|
|
325
|
+
current_batch = next(current_dataloader)
|
|
326
|
+
replay = replay_buffer.sample_batch(current_batch_size)
|
|
327
|
+
if replay is not None:
|
|
328
|
+
bx = torch.cat([current_batch[0], replay[0]], dim=0)
|
|
329
|
+
by = torch.cat([current_batch[1], replay[1]], dim=0)
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
current_batch_size: The batch size of the current tier.
|
|
333
|
+
Used to compute the target replay batch size as
|
|
334
|
+
``max(1, int(replay_ratio * current_batch_size))``.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
``(bx, by)`` tensors on the same device as the dataloader
|
|
338
|
+
yields them, or ``None`` if no replay tiers are available.
|
|
339
|
+
"""
|
|
340
|
+
if not self._tiers:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
target_replay = max(1, int(self.replay_ratio * current_batch_size))
|
|
344
|
+
collected_x: List[torch.Tensor] = []
|
|
345
|
+
collected_y: List[torch.Tensor] = []
|
|
346
|
+
|
|
347
|
+
# Round-robin through all stored tiers until we reach target size
|
|
348
|
+
attempts = 0
|
|
349
|
+
while len(collected_x) < target_replay and attempts < len(self._tiers) * 3:
|
|
350
|
+
tier_idx, dl = self._tiers[attempts % len(self._tiers)]
|
|
351
|
+
batch = self._get_next_batch(tier_idx, dl)
|
|
352
|
+
if batch is not None:
|
|
353
|
+
bx, by = batch
|
|
354
|
+
collected_x.append(bx)
|
|
355
|
+
collected_y.append(by)
|
|
356
|
+
attempts += 1
|
|
357
|
+
|
|
358
|
+
if not collected_x:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
# Concatenate and trim/pad to target size
|
|
362
|
+
all_x = torch.cat(collected_x, dim=0)
|
|
363
|
+
all_y = torch.cat(collected_y, dim=0)
|
|
364
|
+
|
|
365
|
+
if all_x.shape[0] > target_replay:
|
|
366
|
+
all_x = all_x[:target_replay]
|
|
367
|
+
all_y = all_y[:target_replay]
|
|
368
|
+
|
|
369
|
+
return all_x, all_y
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TopoExploit Training Configuration
|
|
4
|
+
|
|
5
|
+
122M parameter model trained on ExploitGym vulnerability/exploit data.
|
|
6
|
+
|
|
7
|
+
Tier structure:
|
|
8
|
+
Tier 1: vulnerability_analysis (256 seq, 3 epochs)
|
|
9
|
+
Tier 2: patch_analysis (512 seq, 4 epochs)
|
|
10
|
+
Tier 3: exploit_development (1024 seq, 5 epochs)
|
|
11
|
+
|
|
12
|
+
Author: Gris Iscomeback
|
|
13
|
+
License: GPL v3
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from .model import TopoGPT2Config
|
|
22
|
+
from .exploitgym_loader import ExploitGymLoaderConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TopoExploitConfig:
|
|
27
|
+
"""Configuration for TopoExploit: 122M params on ExploitGym data."""
|
|
28
|
+
|
|
29
|
+
# --- Model: 122M parameters ---
|
|
30
|
+
SCALE: str = "large"
|
|
31
|
+
DEVICE: str = "cuda"
|
|
32
|
+
RANDOM_SEED: int = 42
|
|
33
|
+
USE_AMP: bool = True
|
|
34
|
+
GRADIENT_CHECKPOINTING: bool = True
|
|
35
|
+
|
|
36
|
+
# --- Training hyperparams (tuned for 122M) ---
|
|
37
|
+
BATCH_SIZE: int = 2
|
|
38
|
+
GRAD_ACCUM_STEPS: int = 16
|
|
39
|
+
LEARNING_RATE: float = 2e-4
|
|
40
|
+
WEIGHT_DECAY: float = 0.1
|
|
41
|
+
GRADIENT_CLIP_NORM: float = 1.0
|
|
42
|
+
WARMUP_RATIO: float = 0.05
|
|
43
|
+
|
|
44
|
+
# --- Regularizacion (anti-overfitting) ---
|
|
45
|
+
LABEL_SMOOTHING: float = 0.05
|
|
46
|
+
EARLY_STOP_PATIENCE: int = 1
|
|
47
|
+
EARLY_STOP_MIN_DELTA: float = 0.0
|
|
48
|
+
BEST_CKPT_RESTORE: bool = True
|
|
49
|
+
|
|
50
|
+
# --- Curriculum: 3 tiers over ExploitGym ---
|
|
51
|
+
# Tier 1 gets more epochs (weak tier, small data), Tier 2 capped (easy tier, huge data)
|
|
52
|
+
TIER_EPOCHS: Tuple[int, int, int] = (6, 3, 4)
|
|
53
|
+
TIER_SEQ_LEN: Tuple[int, int, int] = (256, 512, 768)
|
|
54
|
+
TIER_NAMES: Tuple[str, str, str] = (
|
|
55
|
+
"vulnerability_analysis",
|
|
56
|
+
"patch_analysis",
|
|
57
|
+
"exploit_development",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# --- Progressive window sizes ---
|
|
61
|
+
PROGRESSIVE_WINDOW: Tuple[int, int, int] = (128, 256, 256)
|
|
62
|
+
|
|
63
|
+
# --- Grassmannian tracking ---
|
|
64
|
+
GRASS_TRACK_EVERY: int = 200
|
|
65
|
+
GRASS_MAX_RANK: int = 16
|
|
66
|
+
GRASS_ELBOW_RATIO: float = 0.05
|
|
67
|
+
GRASS_FISHER_BATCHES: int = 40
|
|
68
|
+
GRASS_FISHER_GRADS: int = 64
|
|
69
|
+
|
|
70
|
+
# --- ExploitGym data ---
|
|
71
|
+
DATA_CACHE: str = "data_exploitgym"
|
|
72
|
+
REPO_CACHE: str = ".exploitgym_repo"
|
|
73
|
+
MAX_TOKENS_PER_TIER: Tuple[int, int, int] = (50_000_000, 80_000_000, 100_000_000)
|
|
74
|
+
VAL_FRACTION: float = 0.10
|
|
75
|
+
HOLDOUT_FRACTION: float = 0.10
|
|
76
|
+
|
|
77
|
+
# --- Checkpointing ---
|
|
78
|
+
CHECKPOINT_DIR: str = "checkpoints_topoexploit"
|
|
79
|
+
LOG_FILE: str = "topoexploit_history.jsonl"
|
|
80
|
+
LOG_INTERVAL_STEPS: int = 50
|
|
81
|
+
CHECKPOINT_EVERY_MIN: int = 15
|
|
82
|
+
MAX_CHECKPOINTS: int = 5
|
|
83
|
+
|
|
84
|
+
# --- Gauss optimization ---
|
|
85
|
+
USE_GAUSS_COMPLEX_MULT: bool = True
|
|
86
|
+
|
|
87
|
+
# --- Limits ---
|
|
88
|
+
MAX_TRAIN_BATCHES_PER_EPOCH: int = 200 # cap per-tier to balance 18:1 data ratio
|
|
89
|
+
MAX_EVAL_BATCHES: int = 200
|
|
90
|
+
EFFICIENCY_THROUGHPUT_BATCHES: int = 20
|
|
91
|
+
|
|
92
|
+
# --- Anti-forgetting: EWC + Replay ---
|
|
93
|
+
USE_EWC: bool = True
|
|
94
|
+
EWC_LAMBDA: float = 5000.0
|
|
95
|
+
EWC_FISHER_BATCHES: int = 50
|
|
96
|
+
USE_REPLAY: bool = True
|
|
97
|
+
REPLAY_RATIO: float = 0.25
|
|
98
|
+
REPLAY_MAX_TIERS: int = 3
|
|
99
|
+
|
|
100
|
+
def build_topogpt2_config(self, max_seq_len: int,
|
|
101
|
+
attn_window: Optional[int] = None) -> TopoGPT2Config:
|
|
102
|
+
cfg = TopoGPT2Config(
|
|
103
|
+
DEVICE=self.DEVICE,
|
|
104
|
+
RANDOM_SEED=self.RANDOM_SEED,
|
|
105
|
+
USE_AMP=self.USE_AMP,
|
|
106
|
+
SCALE=self.SCALE,
|
|
107
|
+
BATCH_SIZE=self.BATCH_SIZE,
|
|
108
|
+
GRAD_ACCUM_STEPS=self.GRAD_ACCUM_STEPS,
|
|
109
|
+
LEARNING_RATE=self.LEARNING_RATE,
|
|
110
|
+
WEIGHT_DECAY=self.WEIGHT_DECAY,
|
|
111
|
+
GRADIENT_CLIP_NORM=self.GRADIENT_CLIP_NORM,
|
|
112
|
+
WARMUP_RATIO=self.WARMUP_RATIO,
|
|
113
|
+
GRADIENT_CHECKPOINTING=self.GRADIENT_CHECKPOINTING,
|
|
114
|
+
CHECKPOINT_DIR=self.CHECKPOINT_DIR,
|
|
115
|
+
DATA_DIR=self.DATA_CACHE,
|
|
116
|
+
)
|
|
117
|
+
cfg.MAX_SEQ_LEN = max_seq_len
|
|
118
|
+
if attn_window is not None and attn_window > 0:
|
|
119
|
+
cfg.ATTN_WINDOW = attn_window
|
|
120
|
+
return cfg
|
|
121
|
+
|
|
122
|
+
def build_loader_config(self) -> ExploitGymLoaderConfig:
|
|
123
|
+
return ExploitGymLoaderConfig(
|
|
124
|
+
data_cache=self.DATA_CACHE,
|
|
125
|
+
repo_cache=self.REPO_CACHE,
|
|
126
|
+
max_tokens_per_tier=self.MAX_TOKENS_PER_TIER,
|
|
127
|
+
val_fraction=self.VAL_FRACTION,
|
|
128
|
+
holdout_fraction=self.HOLDOUT_FRACTION,
|
|
129
|
+
split_seed=self.RANDOM_SEED,
|
|
130
|
+
)
|