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,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
+ )