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.
topogpt3/lens_model.py ADDED
@@ -0,0 +1,389 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+ from typing import Any, Protocol
9
+
10
+ import torch
11
+ from torch import nn
12
+ from safetensors.torch import load_file
13
+
14
+
15
+ __all__ = [
16
+ "LensModel",
17
+ "TopoGPT3LensConfig",
18
+ "TopoGPT3LensModel",
19
+ "TinyDecoder",
20
+ ]
21
+
22
+
23
+ class LensModel(Protocol):
24
+ """What the lens needs from a model.
25
+
26
+ Attributes:
27
+ n_layers: Number of residual blocks.
28
+ d_model: Residual-stream width.
29
+ layers: The residual blocks, indexable by integer; what
30
+ ActivationRecorder hooks.
31
+ tokenizer: Tokenizer used by the visualisation helpers; must provide
32
+ ``decode(token_ids) -> str``. Fitting and apply() never touch it.
33
+ """
34
+
35
+ n_layers: int
36
+ d_model: int
37
+ layers: Sequence[nn.Module]
38
+ tokenizer: Any
39
+
40
+ def encode(self, text: str, *, max_length: int = ...) -> torch.Tensor:
41
+ """Tokenize ``text`` to ``input_ids`` of shape ``[1, seq_len]`` on the
42
+ model's input device."""
43
+ ...
44
+
45
+ def forward(self, input_ids: torch.Tensor) -> Any:
46
+ """Run the residual stack on ``input_ids`` (no LM head). Must build an
47
+ autograd graph through layers when grad is enabled, and must be
48
+ deterministic across batch elements (eval mode, dropout off) --- the
49
+ fitting estimator replicates the prompt along the batch axis."""
50
+ ...
51
+
52
+ def unembed(self, residual: torch.Tensor) -> torch.Tensor:
53
+ """Map a residual-stream tensor ``[..., d_model]`` to logits
54
+ ``[..., vocab_size]`` (final norm + LM head)."""
55
+ ...
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class TopoGPT3LensConfig:
60
+ """Centralized configuration for the TopoGPT3 lens model adapter.
61
+
62
+ Every value consumed downstream resides here. Adding a new tunable means
63
+ extending this class; no other module should embed literals.
64
+ """
65
+
66
+ d_model: int = 256
67
+ n_layers: int = 6
68
+ n_heads: int = 8
69
+ n_kv_heads: int = 0
70
+ vocab_size: int = 50257
71
+ max_seq_len: int = 256
72
+ dropout: float = 0.1
73
+ device: str = field(
74
+ default_factory=lambda: "cuda" if torch.cuda.is_available() else "cpu"
75
+ )
76
+ encoding: str = "gpt2"
77
+ scale: str = "small"
78
+ moe_enabled: bool = True
79
+ n_experts: int = 4
80
+ moe_top_k: int = 2
81
+ gradient_checkpointing: bool = True
82
+
83
+ @classmethod
84
+ def from_topogpt2_config(cls, cfg: Any) -> TopoGPT3LensConfig:
85
+ """Construct a lens config from a TopoGPT2Config dataclass."""
86
+ return cls(
87
+ d_model=cfg.D_MODEL,
88
+ n_layers=cfg.N_LAYERS,
89
+ n_heads=cfg.N_HEADS,
90
+ n_kv_heads=getattr(cfg, "N_KV_HEADS", 0),
91
+ vocab_size=cfg.VOCAB_SIZE,
92
+ max_seq_len=cfg.MAX_SEQ_LEN,
93
+ dropout=cfg.DROPOUT,
94
+ device=cfg.DEVICE,
95
+ encoding=getattr(cfg, "ENCODING", "gpt2"),
96
+ scale=cfg.SCALE,
97
+ moe_enabled=cfg.MOE_ENABLED,
98
+ n_experts=cfg.N_EXPERTS,
99
+ moe_top_k=cfg.MOE_TOP_K,
100
+ gradient_checkpointing=cfg.GRADIENT_CHECKPOINTING,
101
+ )
102
+
103
+ @classmethod
104
+ def probe_checkpoint(
105
+ cls,
106
+ checkpoint_dir: str = "checkpoints_topogpt3",
107
+ *,
108
+ state_filename: str = "state.json",
109
+ ) -> TopoGPT3LensConfig:
110
+ """Probe a checkpoint directory and infer lens config from state.json.
111
+
112
+ Args:
113
+ checkpoint_dir: Path to the checkpoint slot directory.
114
+ state_filename: JSON file containing training config.
115
+
116
+ Returns:
117
+ A TopoGPT3LensConfig matching the checkpoint.
118
+
119
+ Raises:
120
+ FileNotFoundError: If state.json is missing.
121
+ ValueError: If required fields are absent from the state.
122
+ """
123
+ state_path = Path(checkpoint_dir) / state_filename
124
+ if not state_path.exists():
125
+ raise FileNotFoundError(
126
+ f"State file not found at {state_path}. "
127
+ f"Provide a valid checkpoint directory."
128
+ )
129
+ with open(state_path) as f:
130
+ state = json.load(f)
131
+ raw = state.get("config", state)
132
+ scale = raw.get("SCALE", "small")
133
+ from .model import TopoGPT2Config
134
+ cfg = TopoGPT2Config(SCALE=scale, DEVICE="cpu")
135
+ for k, v in cfg.__dataclass_fields__.items():
136
+ if k in raw and isinstance(raw[k], type(getattr(cfg, k, None))):
137
+ setattr(cfg, k, raw[k])
138
+ cfg.__post_init__()
139
+ return cls.from_topogpt2_config(cfg)
140
+
141
+
142
+ class _TopoGPT3ResidualForward(nn.Module):
143
+ """Runs the residual block stack only (no final norm, no LM head).
144
+
145
+ This is the forward subgraph that ActivationRecorder hooks capture.
146
+ Extracted from TopoGPT2.forward() to expose the residual stream for
147
+ Jacobian lens fitting and application.
148
+ """
149
+
150
+ def __init__(self, model: nn.Module) -> None:
151
+ super().__init__()
152
+ self._model = model
153
+
154
+ def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
155
+ x = self._model.token_embed(input_ids)
156
+ for layer in self._model.layers:
157
+ x, _aux_loss, _kv_cache = layer(x, past_kv=None)
158
+ return x
159
+
160
+
161
+ class TopoGPT3LensModel(nn.Module):
162
+ """LensModel adapter over a loaded TopoGPT2 model.
163
+
164
+ Wraps a TopoGPT2 instance and implements the LensModel protocol for use
165
+ with ActivationRecorder, JacobianLens fitting, and apply().
166
+
167
+ The adapter owns no parameters --- all weights live in the wrapped model.
168
+ Call ``.eval()`` and set ``requires_grad_(False)`` on the wrapped model
169
+ before fitting.
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ model: nn.Module,
175
+ tokenizer: Any | None = None,
176
+ ) -> None:
177
+ super().__init__()
178
+ self._model = model
179
+ self._residual_forward = _TopoGPT3ResidualForward(model)
180
+ self._tokenizer = tokenizer
181
+ self._input_device: torch.device | None = None
182
+
183
+ @property
184
+ def n_layers(self) -> int:
185
+ return len(self._model.layers)
186
+
187
+ @property
188
+ def d_model(self) -> int:
189
+ return self._model.config.D_MODEL
190
+
191
+ @property
192
+ def layers(self) -> nn.ModuleList:
193
+ return self._model.layers
194
+
195
+ @property
196
+ def tokenizer(self) -> Any:
197
+ return self._tokenizer
198
+
199
+ @tokenizer.setter
200
+ def tokenizer(self, tok: Any) -> None:
201
+ self._tokenizer = tok
202
+
203
+ @property
204
+ def input_device(self) -> torch.device:
205
+ if self._input_device is not None:
206
+ return self._input_device
207
+ return self._model.token_embed.weight.device
208
+
209
+ @input_device.setter
210
+ def input_device(self, device: torch.device) -> None:
211
+ self._input_device = device
212
+
213
+ def encode(self, text: str, *, max_length: int = 512) -> torch.Tensor:
214
+ """Tokenize text to input_ids of shape ``[1, seq_len]``.
215
+
216
+ Uses BPETokenizer if available, otherwise falls back to a byte-level
217
+ encoding compatible with GPT-2 BPE tokenization.
218
+ """
219
+ if self._tokenizer is not None:
220
+ ids = self._tokenizer.encode(text)
221
+ if max_length and len(ids) > max_length:
222
+ ids = ids[:max_length]
223
+ return torch.tensor([ids], dtype=torch.long, device=self.input_device)
224
+ encoded = text.encode("utf-8")[: max_length - 1] if max_length else text.encode("utf-8")
225
+ ids = [0] + [1 + (b % 30) for b in encoded]
226
+ return torch.tensor([ids], dtype=torch.long, device=self.input_device)
227
+
228
+ def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
229
+ """Run the residual stack on ``input_ids``.
230
+
231
+ Returns hidden states of shape ``[batch, seq_len, d_model]``
232
+ (pre-final-norm, pre-LM-head). The autograd graph is retained through
233
+ all layers when grad is enabled.
234
+ """
235
+ return self._residual_forward(input_ids)
236
+
237
+ def unembed(self, residual: torch.Tensor) -> torch.Tensor:
238
+ """Map residual ``[..., d_model]`` to logits ``[..., vocab_size]``.
239
+
240
+ Applies the model's final norm and LM head projection.
241
+ """
242
+ x = self._model.final_norm(residual.to(dtype=self._model.lm_head.weight.dtype))
243
+ return self._model.lm_head(x.to(device=self._model.lm_head.weight.device))
244
+
245
+ @classmethod
246
+ def from_checkpoint(
247
+ cls,
248
+ checkpoint_dir: str = "checkpoints_topogpt3",
249
+ *,
250
+ device: str | None = None,
251
+ encoding: str = "gpt2",
252
+ strict: bool = False,
253
+ ) -> TopoGPT3LensModel:
254
+ """Build a TopoGPT3LensModel from a checkpoint directory.
255
+
256
+ Probes state.json for configuration, instantiates the model, loads
257
+ safetensors weights, and wraps the result.
258
+
259
+ Args:
260
+ checkpoint_dir: Path to the checkpoint slot directory.
261
+ device: Target device. Defaults to cuda if available else cpu.
262
+ encoding: Tokenizer encoding name (passed to BPETokenizer).
263
+ strict: Whether to enforce strict state dict loading.
264
+
265
+ Returns:
266
+ A TopoGPT3LensModel in eval mode with requires_grad_(False).
267
+
268
+ Raises:
269
+ FileNotFoundError: If model.safetensors or state.json is missing.
270
+ """
271
+ from .model import BPETokenizer, TopoGPT2, TopoGPT2Config
272
+
273
+ device = device or ("cuda" if torch.cuda.is_available() else "cpu")
274
+ resolved = Path(checkpoint_dir)
275
+ weights_path = resolved / "model.safetensors"
276
+
277
+ if not weights_path.exists():
278
+ raise FileNotFoundError(
279
+ f"Weights not found at {weights_path}. "
280
+ f"Download from https://huggingface.co/grisiscomeback/TopoGPT3"
281
+ )
282
+
283
+ lens_cfg = TopoGPT3LensConfig.probe_checkpoint(str(resolved))
284
+ model_cfg = TopoGPT2Config(
285
+ SCALE=lens_cfg.scale,
286
+ DEVICE=device,
287
+ )
288
+ model_cfg.__post_init__()
289
+
290
+ model = TopoGPT2(model_cfg)
291
+ state_dict = load_file(str(weights_path), device=device)
292
+ missing, unexpected = model.load_state_dict(state_dict, strict=strict)
293
+ model.to(device)
294
+ model.eval()
295
+
296
+ tokenizer = BPETokenizer(encoding=encoding)
297
+ instance = cls(model, tokenizer=tokenizer)
298
+ instance.input_device = torch.device(device)
299
+
300
+ for param in model.parameters():
301
+ param.requires_grad_(False)
302
+
303
+ return instance
304
+
305
+
306
+ class TinyDecoder(nn.Module):
307
+ """A tiny CPU-only decoder for end-to-end tests.
308
+
309
+ Implements the LensModel protocol indirectly (wrapped by
310
+ TopoGPT3LensModel). Residual blocks are ``h + 0.1 * linear(h)``:
311
+ the small gain keeps the Jacobian well-conditioned so the late-layer
312
+ ``diag(J) ~= 1`` property holds.
313
+ """
314
+
315
+ def __init__(
316
+ self, n_layers: int = 4, d_model: int = 8, vocab_size: int = 32, seed: int = 0
317
+ ) -> None:
318
+ super().__init__()
319
+ torch.manual_seed(seed)
320
+ config = SimpleNamespace(
321
+ D_MODEL=d_model,
322
+ N_LAYERS=n_layers,
323
+ N_HEADS=1,
324
+ N_KV_HEADS=1,
325
+ VOCAB_SIZE=vocab_size,
326
+ MAX_SEQ_LEN=128,
327
+ DROPOUT=0.0,
328
+ DEVICE="cpu",
329
+ SCALE="custom",
330
+ MOE_ENABLED=False,
331
+ N_EXPERTS=1,
332
+ MOE_TOP_K=1,
333
+ GRADIENT_CHECKPOINTING=False,
334
+ )
335
+ self.config = config
336
+ self.token_embed = nn.Embedding(vocab_size, d_model)
337
+ self.layers = nn.ModuleList(
338
+ [_ResidualBlock(d_model) for _ in range(n_layers)]
339
+ )
340
+ self.final_norm = nn.LayerNorm(d_model)
341
+ self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
342
+ self.lm_head.weight = self.token_embed.weight
343
+
344
+ def forward(
345
+ self, token_ids: torch.Tensor, past_kvs: Any = None
346
+ ) -> tuple[torch.Tensor, torch.Tensor, list]:
347
+ x = self.token_embed(token_ids)
348
+ new_kvs: list = []
349
+ total_aux = torch.tensor(0.0, device=x.device)
350
+ for layer in self.layers:
351
+ x, aux, _ = layer(x, past_kv=None)
352
+ total_aux = total_aux + aux
353
+ new_kvs.append(None)
354
+ x = self.final_norm(x)
355
+ logits = self.lm_head(x)
356
+ return logits, total_aux / len(self.layers), new_kvs
357
+
358
+
359
+ class _ResidualBlock(nn.Module):
360
+ def __init__(self, d_model: int) -> None:
361
+ super().__init__()
362
+ self.linear = nn.Linear(d_model, d_model, bias=False)
363
+ with torch.no_grad():
364
+ self.linear.weight.mul_(0.1)
365
+
366
+ def forward(
367
+ self, x: torch.Tensor, past_kv: Any = None
368
+ ) -> tuple[torch.Tensor, torch.Tensor, None]:
369
+ return x + self.linear(x), torch.tensor(0.0), None
370
+
371
+
372
+ if __name__ == "__main__":
373
+ import time
374
+ cfg = TopoGPT3LensConfig.probe_checkpoint("checkpoints_topogpt3/last")
375
+ device = "cpu"
376
+ import torch
377
+ t0 = time.perf_counter()
378
+ model = TopoGPT3LensModel.from_checkpoint(
379
+ "checkpoints_topogpt3/last", device=device
380
+ )
381
+ t1 = time.perf_counter()
382
+ n_params = sum(p.numel() for p in model._model.parameters())
383
+ print(f"TopoGPT3LensModel: n_layers={model.n_layers}, d_model={model.d_model}, params={n_params:,}, load={t1-t0:.1f}s")
384
+ ids = model.encode("def fibonacci(n):", max_length=32)
385
+ with torch.no_grad():
386
+ hidden = model.forward(ids)
387
+ logits = model.unembed(hidden)
388
+ print(f"encode: {ids.shape} forward: {hidden.shape} unembed: {logits.shape}")
389
+ print("Model is ready. Use: python3 -m topogpt3.jlens --prompt 'def hello('")
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TopoMerged: 6-tier curriculum merging TopoGPT3 code tasks with ExploitGym exploit tasks.
4
+
5
+ Tier structure:
6
+ Phase 1 — Code foundation (from TopoGPT3):
7
+ Tier 1: codealpaca (128 seq, 2 epochs) — short instructions
8
+ Tier 2: code_feedback (192 seq, 2 epochs) — step-by-step explanations
9
+ Tier 3: magicoder_evol (256 seq, 3 epochs) — complex problems
10
+
11
+ Phase 2 — Exploit specialization (from ExploitGym):
12
+ Tier 4: vulnerability_analysis (256 seq, 4 epochs) — analyze CVEs
13
+ Tier 5: patch_analysis (512 seq, 3 epochs) — understand patches
14
+ Tier 6: exploit_development (768 seq, 4 epochs) — write exploits
15
+
16
+ The model learns to code first, then applies coding skill to security.
17
+
18
+ Author: Gris Iscomeback
19
+ License: GPL v3
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import Optional, Tuple
26
+
27
+ from .model import TopoGPT2Config
28
+ from .exploitgym_loader import ExploitGymLoaderConfig
29
+
30
+
31
+ @dataclass
32
+ class TopoMergedConfig:
33
+ """Configuration for TopoMerged: 6-tier code+exploit curriculum."""
34
+
35
+ # --- Model: xlarge (~150M params) ---
36
+ SCALE: str = "xlarge"
37
+ DEVICE: str = "cuda"
38
+ RANDOM_SEED: int = 42
39
+ USE_AMP: bool = True
40
+ GRADIENT_CHECKPOINTING: bool = True
41
+
42
+ # --- Training hyperparams (tuned for ~150M on 6GB VRAM) ---
43
+ BATCH_SIZE: int = 2
44
+ GRAD_ACCUM_STEPS: int = 16
45
+ LEARNING_RATE: float = 2e-4
46
+ WEIGHT_DECAY: float = 0.1
47
+ GRADIENT_CLIP_NORM: float = 1.0
48
+ WARMUP_RATIO: float = 0.05
49
+
50
+ # --- 6-Tier Curriculum ---
51
+ # Phase 1 (code): shorter seq, fewer epochs — build coding foundation
52
+ # Phase 2 (exploit): longer seq, more epochs — apply to security
53
+ TIER_EPOCHS: Tuple[int, int, int, int, int, int] = (2, 2, 3, 4, 3, 4)
54
+ TIER_SEQ_LEN: Tuple[int, int, int, int, int, int] = (128, 192, 256, 256, 512, 768)
55
+ TIER_NAMES: Tuple[str, str, str, str, str, str] = (
56
+ "codealpaca",
57
+ "code_feedback",
58
+ "magicoder_evol",
59
+ "vulnerability_analysis",
60
+ "patch_analysis",
61
+ "exploit_development",
62
+ )
63
+ TIER_SOURCES: Tuple[str, str, str, str, str, str] = (
64
+ "topogpt3", # HuggingFace dataset
65
+ "topogpt3", # HuggingFace dataset
66
+ "topogpt3", # HuggingFace dataset
67
+ "exploitgym", # ExploitGym repo
68
+ "exploitgym", # ExploitGym repo
69
+ "exploitgym", # ExploitGym repo
70
+ )
71
+
72
+ # --- Progressive window sizes (sliding window attention) ---
73
+ PROGRESSIVE_WINDOW: Tuple[int, int, int, int, int, int] = (64, 96, 128, 128, 256, 256)
74
+
75
+ # --- Datasets (HuggingFace IDs for code tiers) ---
76
+ HF_CODEALPACA: str = "sahil2801/CodeAlpaca-20k"
77
+ HF_CODE_FEEDBACK: str = "m-a-p/CodeFeedback-Filtered-Instruction"
78
+ HF_MAGICODER: str = "ise-uiuc/Magicoder-Evol-Instruct-110K"
79
+
80
+ # --- ExploitGym data ---
81
+ DATA_CACHE_EXPLOIT: str = "data_exploitgym"
82
+ REPO_CACHE: str = ".exploitgym_repo"
83
+
84
+ # --- Splits ---
85
+ VAL_FRACTION: float = 0.05
86
+ HOLDOUT_FRACTION: float = 0.05
87
+ SPLIT_SEED: int = 1337
88
+
89
+ # --- Tokens per tier (limits) ---
90
+ MAX_TOKENS_PER_TIER: Tuple[int, int, int, int, int, int] = (
91
+ 20_000_000, # CodeAlpaca
92
+ 80_000_000, # CodeFeedback
93
+ 100_000_000, # Magicoder
94
+ 50_000_000, # vulnerability_analysis
95
+ 80_000_000, # patch_analysis
96
+ 100_000_000, # exploit_development
97
+ )
98
+
99
+ # --- Grassmannian tracking ---
100
+ GRASS_TRACK_EVERY: int = 200
101
+ GRASS_MAX_RANK: int = 16
102
+ GRASS_ELBOW_RATIO: float = 0.05
103
+ GRASS_FISHER_BATCHES: int = 40
104
+ GRASS_FISHER_GRADS: int = 64
105
+
106
+ # --- Gauss optimization ---
107
+ USE_GAUSS_COMPLEX_MULT: bool = True
108
+
109
+ # --- Limits ---
110
+ MAX_TRAIN_BATCHES_PER_EPOCH: int = 0 # 0 = no limit
111
+ MAX_EVAL_BATCHES: int = 200
112
+
113
+ # --- Anti-forgetting: EWC + Replay ---
114
+ USE_EWC: bool = True
115
+ EWC_LAMBDA: float = 5000.0
116
+ EWC_FISHER_BATCHES: int = 50
117
+ USE_REPLAY: bool = True
118
+ REPLAY_RATIO: float = 0.25
119
+ REPLAY_MAX_TIERS: int = 5 # remember all previous tiers
120
+
121
+ # --- Phase transition: stronger anti-forgetting at code→exploit boundary ---
122
+ PHASE_BOUNDARY: int = 3 # tier index where phase 2 starts (0-indexed)
123
+ EWC_PHASE2_SCALE: float = 0.3 # reduced EWC in phase 2 (allow more plasticity)
124
+
125
+ # --- Paths ---
126
+ DATA_CACHE_TOPOGPT3: str = "data_topogpt3"
127
+ DATA_CACHE_EXPLOITGYM: str = "data_exploitgym"
128
+ CHECKPOINT_DIR: str = "checkpoints_topomerged"
129
+ LOG_FILE: str = "topomerged_history.jsonl"
130
+
131
+ # --- Logging ---
132
+ LOG_INTERVAL_STEPS: int = 50
133
+ CHECKPOINT_EVERY_MIN: int = 15
134
+ MAX_CHECKPOINTS: int = 5
135
+
136
+ # --- Efficiency metrics ---
137
+ EFFICIENCY_THROUGHPUT_BATCHES: int = 20
138
+
139
+ def build_topogpt2_config(self, max_seq_len: int,
140
+ attn_window: Optional[int] = None) -> TopoGPT2Config:
141
+ cfg = TopoGPT2Config(
142
+ DEVICE=self.DEVICE,
143
+ RANDOM_SEED=self.RANDOM_SEED,
144
+ USE_AMP=self.USE_AMP,
145
+ SCALE=self.SCALE,
146
+ BATCH_SIZE=self.BATCH_SIZE,
147
+ GRAD_ACCUM_STEPS=self.GRAD_ACCUM_STEPS,
148
+ LEARNING_RATE=self.LEARNING_RATE,
149
+ WEIGHT_DECAY=self.WEIGHT_DECAY,
150
+ GRADIENT_CLIP_NORM=self.GRADIENT_CLIP_NORM,
151
+ WARMUP_RATIO=self.WARMUP_RATIO,
152
+ GRADIENT_CHECKPOINTING=self.GRADIENT_CHECKPOINTING,
153
+ CHECKPOINT_DIR=self.CHECKPOINT_DIR,
154
+ DATA_DIR=self.DATA_CACHE_TOPOGPT3,
155
+ )
156
+ cfg.MAX_SEQ_LEN = max_seq_len
157
+ if attn_window is not None and attn_window > 0:
158
+ cfg.ATTN_WINDOW = attn_window
159
+ return cfg
160
+
161
+ def build_exploit_loader_config(self) -> ExploitGymLoaderConfig:
162
+ return ExploitGymLoaderConfig(
163
+ data_cache=self.DATA_CACHE_EXPLOITGYM,
164
+ repo_cache=self.REPO_CACHE,
165
+ max_tokens_per_tier=(
166
+ self.MAX_TOKENS_PER_TIER[3],
167
+ self.MAX_TOKENS_PER_TIER[4],
168
+ self.MAX_TOKENS_PER_TIER[5],
169
+ ),
170
+ val_fraction=self.VAL_FRACTION,
171
+ holdout_fraction=self.HOLDOUT_FRACTION,
172
+ split_seed=self.RANDOM_SEED,
173
+ )
174
+
175
+ def is_code_tier(self, tier_index: int) -> bool:
176
+ """Returns True if the tier uses TopoGPT3 (HuggingFace) data."""
177
+ return self.TIER_SOURCES[tier_index] == "topogpt3"
178
+
179
+ def is_exploit_tier(self, tier_index: int) -> bool:
180
+ """Returns True if the tier uses ExploitGym data."""
181
+ return self.TIER_SOURCES[tier_index] == "exploitgym"
182
+
183
+ def exploit_tier_index(self, tier_index: int) -> Optional[int]:
184
+ """Convert global tier index to exploitgym-local index (0-2)."""
185
+ if not self.is_exploit_tier(tier_index):
186
+ return None
187
+ return tier_index - self.PHASE_BOUNDARY