aetherscan 1.0.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.
- aetherscan/__init__.py +45 -0
- aetherscan/benchmark.py +179 -0
- aetherscan/candidate_figures.py +328 -0
- aetherscan/cli.py +3032 -0
- aetherscan/config.py +1002 -0
- aetherscan/dashboard.py +709 -0
- aetherscan/dashboard_cli.py +51 -0
- aetherscan/dashboard_launcher.py +194 -0
- aetherscan/data_generation.py +1448 -0
- aetherscan/db/__init__.py +21 -0
- aetherscan/db/db.py +2789 -0
- aetherscan/hf_hub.py +547 -0
- aetherscan/inference.py +912 -0
- aetherscan/inference_viz.py +1494 -0
- aetherscan/latent_gif.py +512 -0
- aetherscan/latent_variants.py +352 -0
- aetherscan/logger/__init__.py +21 -0
- aetherscan/logger/logger.py +467 -0
- aetherscan/logger/slack_handler.py +760 -0
- aetherscan/main.py +1269 -0
- aetherscan/manager/__init__.py +21 -0
- aetherscan/manager/manager.py +781 -0
- aetherscan/models/__init__.py +21 -0
- aetherscan/models/random_forest.py +171 -0
- aetherscan/models/vae.py +849 -0
- aetherscan/monitor/__init__.py +19 -0
- aetherscan/monitor/monitor.py +935 -0
- aetherscan/pfb.py +161 -0
- aetherscan/preprocessing.py +2718 -0
- aetherscan/rf_metrics.py +100 -0
- aetherscan/round_data.py +932 -0
- aetherscan/run_state.py +277 -0
- aetherscan/seeding.py +160 -0
- aetherscan/shap_parallel.py +238 -0
- aetherscan/tag_guards.py +206 -0
- aetherscan/train.py +7681 -0
- aetherscan-1.0.0.dist-info/METADATA +1187 -0
- aetherscan-1.0.0.dist-info/RECORD +41 -0
- aetherscan-1.0.0.dist-info/WHEEL +4 -0
- aetherscan-1.0.0.dist-info/entry_points.txt +2 -0
- aetherscan-1.0.0.dist-info/licenses/LICENSE +13 -0
aetherscan/config.py
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
# Note, we avoid logging anything in config.py to prevent coupling with the logger module
|
|
2
|
+
"""
|
|
3
|
+
Configuration module for Aetherscan Pipeline
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from multiprocessing import cpu_count
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DBConfig:
|
|
16
|
+
"""SQLite database configuration"""
|
|
17
|
+
|
|
18
|
+
get_connection_timeout: float = 60.0 # seconds
|
|
19
|
+
stop_writer_timeout: float = 10.0 # seconds
|
|
20
|
+
write_interval: float = 5.0 # seconds
|
|
21
|
+
# Rows per foreground transaction. 100 meant a commit (and fsync) every 100 rows — one of
|
|
22
|
+
# the drivers of the ~590 rows/s writer that let a multi-hour backlog build up (#277)
|
|
23
|
+
write_buffer_max_size: int = 5000 # records
|
|
24
|
+
write_retry_delay: float = 1.0 # seconds
|
|
25
|
+
flush_timeout: float = 10.0 # seconds
|
|
26
|
+
# Bulk lane (#277): high-volume injection stats ride a separate bounded queue so a plot
|
|
27
|
+
# flush only has to drain the (small) foreground lane, and queue growth is capped —
|
|
28
|
+
# backpressure blocks the background enqueuer (the round-data drainer thread), never the
|
|
29
|
+
# training path. bulk_chunk_rows is both the enqueue granularity and the bulk transaction
|
|
30
|
+
# size; the cap is bulk_queue_max_items * bulk_chunk_rows rows in memory (~1.6M at defaults).
|
|
31
|
+
bulk_chunk_rows: int = 50_000 # rows per bulk queue item / transaction
|
|
32
|
+
bulk_queue_max_items: int = 32 # bounded bulk-lane depth (items)
|
|
33
|
+
# Shutdown drain (#277): stop() drains BOTH lanes to disk before the writer exits (the old
|
|
34
|
+
# behavior silently dropped everything still queued — up to ~26M rows on the release run).
|
|
35
|
+
# If the drain exceeds this cap the remaining rows are dropped with an exact ERROR count.
|
|
36
|
+
stop_drain_timeout: float = 600.0 # seconds
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ManagerConfig:
|
|
41
|
+
"""Resource manager configuration"""
|
|
42
|
+
|
|
43
|
+
n_processes: int = cpu_count() # use all available cores
|
|
44
|
+
# TODO: experiment with larger chunk sizes (how to track chunk processing efficiency)
|
|
45
|
+
# NOTE: should we move chunks_per_worker to DataConfig() or TrainingConfig() and make it specific to preproc/data_gen?
|
|
46
|
+
chunks_per_worker: int = 4 # for balancing overhead vs parallelism
|
|
47
|
+
pool_terminate_timeout: float = (
|
|
48
|
+
10.0 # seconds (actual timeout may be 2x this value -- from terminate + join threads)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MonitorConfig:
|
|
54
|
+
"""Resource monitor configuration"""
|
|
55
|
+
|
|
56
|
+
get_gpu_timeout: float = 5.0 # seconds
|
|
57
|
+
stop_monitor_timeout: float = 10.0 # seconds
|
|
58
|
+
monitor_interval: float = 1.0 # seconds
|
|
59
|
+
monitor_retry_delay: float = 1.0 # seconds
|
|
60
|
+
# Overlay top-level pipeline_stages spans (depth <= 2 dot-names, e.g. "train.round_03")
|
|
61
|
+
# as labeled translucent bands on the resource plot's CPU panel, so utilization
|
|
62
|
+
# plateaus are attributable to pipeline stages at a glance
|
|
63
|
+
annotate_stages: bool = True
|
|
64
|
+
# Live monitoring dashboard (aetherscan/dashboard.py) auto-launched by main.py at run start;
|
|
65
|
+
# --no-dashboard opts out. Served headless on dashboard_port (SSH-forward to reach it).
|
|
66
|
+
dashboard_enabled: bool = True
|
|
67
|
+
dashboard_port: int = 8501
|
|
68
|
+
# End-of-run benchmark report (utils/benchmark_report.py) rendered at the tail of
|
|
69
|
+
# train/inference and posted to Slack; --no-benchmark-report opts out.
|
|
70
|
+
benchmark_report_enabled: bool = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class LoggerConfig:
|
|
75
|
+
"""Logger configuration"""
|
|
76
|
+
|
|
77
|
+
# Set log levels
|
|
78
|
+
console_level: str = "INFO"
|
|
79
|
+
file_level: str = "INFO"
|
|
80
|
+
slack_level: str = "INFO"
|
|
81
|
+
|
|
82
|
+
# Slack configuration
|
|
83
|
+
slack_enabled: bool = True
|
|
84
|
+
slack_channel: str | None = None # Override with SLACK_CHANNEL env var
|
|
85
|
+
slack_username: str = "Aetherscan"
|
|
86
|
+
slack_timeout: float = 15.0
|
|
87
|
+
slack_retry_attempts: int = 3
|
|
88
|
+
slack_buffer_size: int = 100 # Max messages to buffer before flushing
|
|
89
|
+
slack_flush_interval: float = 60.0 # Seconds between automatic flushes
|
|
90
|
+
slack_broadcast_level: str = "ERROR" # Messages at this level+ are broadcast to main channel
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class BetaVAEConfig:
|
|
95
|
+
"""Beta-VAE model configuration"""
|
|
96
|
+
|
|
97
|
+
latent_dim: int = 8 # Bottleneck size
|
|
98
|
+
dense_layer_size: int = 512 # Should match num frequency bins after downsampling
|
|
99
|
+
kernel_size: tuple[int, int] = (3, 3) # For Conv2D & Conv2DTranspose layers
|
|
100
|
+
beta: float = 1.5 # KL divergence weight
|
|
101
|
+
alpha: float = 10.0 # Clustering loss weight
|
|
102
|
+
# Opt-in keras mixed_bfloat16 policy for the Beta-VAE (train.py sets the global policy
|
|
103
|
+
# before model build). A/B-gated: changes training numerics — bf16 needs no loss scaling,
|
|
104
|
+
# variables/optimizer state stay fp32 under keras mixed precision, and the numerically
|
|
105
|
+
# sensitive layers (z_mean/z_log_var/Sampling, decoder sigmoid output → loss math) are
|
|
106
|
+
# pinned fp32 in models/vae.py. Default False = fp32 end-to-end, byte-identical to before
|
|
107
|
+
# this flag existed (no policy call is made at all). STAYS OFF after the 7-seed A/B:
|
|
108
|
+
# 6 seeds clean, but seed 13 reproducibly fails (recall .8432 / val AUC .9807, far below
|
|
109
|
+
# the 6-seed control envelope, and the only runs to trip the ECE->calibrator gate were
|
|
110
|
+
# bf16-seed-13, twice, across configs) — a bf16-specific trajectory pathology the gate
|
|
111
|
+
# exists to catch. Revisit only with that pathology understood (+1.15x step throughput
|
|
112
|
+
# and ~halved activation VRAM are the upside on the table).
|
|
113
|
+
mixed_precision: bool = False
|
|
114
|
+
# Whether the conv layers' declared L1/L2 penalties are ADDED to the training objective.
|
|
115
|
+
# v1 default False: the declarations were dead code since inception (a custom loop only
|
|
116
|
+
# applies them by consuming model.losses, which nothing did), and activating them at the
|
|
117
|
+
# declared, never-calibrated coefficients measurably degraded the model in a 5-seed A/B
|
|
118
|
+
# (recall@0.01FPR median .984 -> .954, worst seed .72; 1-4 latent dims per seed pushed
|
|
119
|
+
# below the active-units threshold). reg_loss is computed and recorded regardless, so
|
|
120
|
+
# every run observes what the penalties WOULD be; coefficient calibration is a tracked
|
|
121
|
+
# follow-up issue. Flipping this changes training numerics — A/B-gate like the other
|
|
122
|
+
# numerics flags.
|
|
123
|
+
regularization_active: bool = False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ReproducibilityConfig:
|
|
128
|
+
"""Random-state configuration (#279): ONE root seed for the whole pipeline"""
|
|
129
|
+
|
|
130
|
+
# Root seed for every random stream in BOTH pipelines: synthetic data generation, dataset
|
|
131
|
+
# split/shuffles, TF weight init, the VAE sampling layer (training AND inference), the
|
|
132
|
+
# Random Forest, UMAP/KMeans plot fits, and plot subsampling — each consumer derives an
|
|
133
|
+
# independent stream (see aetherscan.seeding). Defaults to a concrete value so runs are
|
|
134
|
+
# reproducible out of the box (#279 flipped this from the historical None); None restores
|
|
135
|
+
# OS entropy (non-reproducible, warned once). Bit-exact GPU reproducibility additionally
|
|
136
|
+
# needs tf_deterministic_ops and identical hardware/software (GPU count, TF version).
|
|
137
|
+
seed: int | None = 11
|
|
138
|
+
# Force deterministic TF/cuDNN op implementations (tf.config.experimental
|
|
139
|
+
# .enable_op_determinism) at some speed cost. Only meaningful alongside `seed`.
|
|
140
|
+
# Default ON (#298, maintainer decision): without it, cuDNN autotune may select
|
|
141
|
+
# different conv algorithms run to run, and the resulting low-bit latent drift is
|
|
142
|
+
# enough to flip near-threshold candidates between otherwise identical inference runs
|
|
143
|
+
# (measured live: 3 candidate flips between two identical unflagged runs; two flagged
|
|
144
|
+
# runs were bit-identical). Opt out with --no-tf-deterministic-ops.
|
|
145
|
+
tf_deterministic_ops: bool = True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class RandomForestConfig:
|
|
150
|
+
"""Random Forest configuration"""
|
|
151
|
+
|
|
152
|
+
n_estimators: int = 1000 # Number of trees
|
|
153
|
+
bootstrap: bool = (
|
|
154
|
+
True # Whether to use bootstrap sampling when building each tree (True = bagging)
|
|
155
|
+
)
|
|
156
|
+
max_features: str = "sqrt" # Random feature selection (sqrt, log2, float)
|
|
157
|
+
n_jobs: int = -1 # Number of parallel jobs to run (-1 = use all available CPU cores)
|
|
158
|
+
# Explicit override for the RF random_state. None (the default) derives the seed from
|
|
159
|
+
# reproducibility.seed via STREAM_RF (#279); the deprecated --rf-seed flag sets this.
|
|
160
|
+
seed: int | None = None
|
|
161
|
+
|
|
162
|
+
# Latent-representation variant the RF consumes (#282). Training sweeps every variant on
|
|
163
|
+
# one shared dataset/split and records the empirical winner here before final_save, so
|
|
164
|
+
# config_{tag}.json tells inference exactly how to rebuild features (never hardcoded).
|
|
165
|
+
# See aetherscan.latent_variants.VARIANT_ORDER for the catalogue; "z" is the legacy baseline.
|
|
166
|
+
latent_variant: str = "z"
|
|
167
|
+
# ACTIVE latent dims measured at training (Burda et al.; None until a sweep ran) —
|
|
168
|
+
# persisted so inference rebuilds the z_mean_logvar_active layout identically
|
|
169
|
+
active_dims: list[int] | None = None
|
|
170
|
+
# Extra sampled-z draws per cadence for the z_aug variant's training rows
|
|
171
|
+
z_aug_draws: int = 4
|
|
172
|
+
# Variance floor for a latent dim to count as ACTIVE (Burda et al. AU with z_mean
|
|
173
|
+
# variance); gates the active-dims variant + the posterior-collapse guard (#282)
|
|
174
|
+
active_units_threshold: float = 0.01
|
|
175
|
+
# Variant selection (#282): primary metric is recall at this false-positive rate on the
|
|
176
|
+
# selection split; the winner must beat every simpler (fewer-feature) variant by more
|
|
177
|
+
# than a bootstrap CI of the recall difference, else the simpler variant wins the tie
|
|
178
|
+
selection_max_fpr: float = 0.01
|
|
179
|
+
selection_bootstrap_rounds: int = 500
|
|
180
|
+
# Probability calibration (#282): auto-fit a calibrator on the held-out calibration
|
|
181
|
+
# split when the winner's measured ECE exceeds max_ece; isotonic when the calibration
|
|
182
|
+
# split has at least calibration_min_isotonic rows, else sigmoid/Platt (isotonic
|
|
183
|
+
# overfits small sets); kept only if it improves ECE (and does not worsen Brier) on the
|
|
184
|
+
# held-out test split. calibration_active/method are RECORDED BY TRAINING for the saved
|
|
185
|
+
# config — inference reads them to decide whether to load rf_calibrator_{tag}.joblib.
|
|
186
|
+
max_ece: float = 0.05
|
|
187
|
+
calibration_min_isotonic: int = 1000
|
|
188
|
+
calibration_active: bool = False
|
|
189
|
+
calibration_method: str | None = None
|
|
190
|
+
# Fractions of the val split carved out for selection / calibration (remainder = the
|
|
191
|
+
# held-out test split that release metrics are reported on — best-of-N on the selection
|
|
192
|
+
# split alone would be optimistically biased)
|
|
193
|
+
val_selection_fraction: float = 0.5
|
|
194
|
+
val_calibration_fraction: float = 0.25
|
|
195
|
+
# Screening-threshold validation (#282): warn if the two-pass cascade loses more than
|
|
196
|
+
# this much recall vs MC-scoring everything at the science threshold on the test split
|
|
197
|
+
screen_recall_tolerance: float = 0.0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# NOTE: verify that our current GPU config gracefully handles cases where the node has a single GPU (vs multiple)
|
|
201
|
+
# TODO: add a way to specify (either number or name) the specific GPUs on a system we wish to use (currently defaults to all available). extend to cli.py too
|
|
202
|
+
# TODO: run performance benchmarks using different num_gpus on a single node (and in future, multi-node as well)
|
|
203
|
+
@dataclass
|
|
204
|
+
class GPUConfig:
|
|
205
|
+
"""GPU runtime configuration"""
|
|
206
|
+
|
|
207
|
+
# If None, the strategy uses every GPU visible to TF. If set to a positive int N, the
|
|
208
|
+
# strategy is restricted to the first N physical GPUs (the rest are left untouched for
|
|
209
|
+
# other workloads on the node). Validated against batch/sample divisibility in cli.py
|
|
210
|
+
# before being applied; runtime mismatch (N > available GPUs) aborts in
|
|
211
|
+
# setup_gpu_strategy rather than silently downgrading, so we never propagate batch
|
|
212
|
+
# sizes that were validated against a different replica count.
|
|
213
|
+
num_replicas: int | None = None
|
|
214
|
+
per_gpu_memory_limit_mb: int | None = None
|
|
215
|
+
nccl_num_packs: int = 2
|
|
216
|
+
use_async_allocator: bool = True
|
|
217
|
+
# TF_GPU_THREAD_MODE: "gpu_private" gives each GPU dedicated kernel-launch threads that
|
|
218
|
+
# tf.data host work cannot steal — the standard NGC lever for input-pipeline h2d/scheduling
|
|
219
|
+
# interference (measured at ~7.6% of step throughput on blpc3, benchmarks/README.md
|
|
220
|
+
# "Corrected ceiling decomposition"). "global" restores TF's default shared pool;
|
|
221
|
+
# "gpu_shared" is the third TF-supported value. Applied in setup_gpu_strategy before the
|
|
222
|
+
# GPU runtime initializes; inert in the producer tree (CUDA-blanked). NOTE: this config is
|
|
223
|
+
# the source of truth — "global" actively CLEARS any shell-set TF_GPU_THREAD_MODE/COUNT
|
|
224
|
+
# (same semantics as use_async_allocator vs TF_GPU_ALLOCATOR above), so an env-only
|
|
225
|
+
# override will not survive; set the config field instead.
|
|
226
|
+
gpu_thread_mode: str = "gpu_private"
|
|
227
|
+
# TF_GPU_THREAD_COUNT: threads per GPU when gpu_thread_mode="gpu_private" (TF default 2)
|
|
228
|
+
gpu_thread_count: int = 2
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# TODO: make sure the entire pipeline respects DataConfig() values, instead of hard coding
|
|
232
|
+
@dataclass
|
|
233
|
+
class DataConfig:
|
|
234
|
+
"""Data processing configuration"""
|
|
235
|
+
|
|
236
|
+
num_observations: int = 6 # Per cadence snippet (3 ON, 3 OFF)
|
|
237
|
+
width_bin: int = 4096 # Frequency bins per observation
|
|
238
|
+
downsample_factor: int = 8 # Frequency bins downsampling factor
|
|
239
|
+
time_bins: int = 16 # Time bins per observation
|
|
240
|
+
freq_resolution: float = 2.7939677238464355 # Hz
|
|
241
|
+
time_resolution: float = 18.25361108 # seconds
|
|
242
|
+
|
|
243
|
+
# TODO: experiment with larger chunk sizes (how to track chunk processing efficiency)
|
|
244
|
+
# Note the following heuristics/constraints, which only apply to training
|
|
245
|
+
# max_backgrounds_per_file = max_chunks_per_file * background_load_chunk_size
|
|
246
|
+
# max_backgrounds_total = min(max_backgrounds_per_file * num_files, num_target_backgrounds)
|
|
247
|
+
num_target_backgrounds: int = 45000 # Number of background cadences to load
|
|
248
|
+
background_load_chunk_size: int = (
|
|
249
|
+
15000 # Maximum cadences to process at once during load_train_data()
|
|
250
|
+
)
|
|
251
|
+
max_chunks_per_file: int = 1 # Maximum chunks to load from a single file
|
|
252
|
+
|
|
253
|
+
# NOTE: should this be in DataConfig, InferenceConfig, or a new dataclass entirely (e.g. PreprocessingConfig)
|
|
254
|
+
# NOTE: should be renamed test_background_load_chunk_size? or no because inference_files still generate .npy which uses this same chunk size?
|
|
255
|
+
# TODO: experiment with larger chunk sizes (how to track chunk processing efficiency)
|
|
256
|
+
# We only specify chunk size during inference, since we assume all backgrounds from all files must be loaded
|
|
257
|
+
inference_background_load_chunk_size: int = (
|
|
258
|
+
50000 # Maximum cadences to process at once during load_inference_data()
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Data files
|
|
262
|
+
# Note, Python dataclasses don't allow mutable objects (e.g. lists) to be used as defaults,
|
|
263
|
+
# since Python will create that object once when the class is defined, rather than each time
|
|
264
|
+
# a new object of that class is instantiated. This means that all instances of that class
|
|
265
|
+
# would share the same mutable object in memory (i.e. if we modified train_files in one
|
|
266
|
+
# instance, it would affect all other instances -- a dangerous bug).
|
|
267
|
+
# The default_factory parameter takes a callable (lambda function) that's called each time a
|
|
268
|
+
# new instance is created, ensuring each instance gets its own independent list, preventing
|
|
269
|
+
# the shared-state bug. Note that once created, the list behaves identical to any other list
|
|
270
|
+
train_files: list[str] = field(
|
|
271
|
+
default_factory=lambda: [
|
|
272
|
+
"real_filtered_LARGE_HIP110750.npy",
|
|
273
|
+
"real_filtered_LARGE_HIP13402.npy",
|
|
274
|
+
"real_filtered_LARGE_HIP8497.npy",
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
# TODO: add comment specifying test_files requirements & behavior (.npy instead of .csv containing individual .h5 files, in inference_files)
|
|
278
|
+
test_files: list[str] = field(default_factory=lambda: ["real_filtered_LARGE_test_HIP15638.npy"])
|
|
279
|
+
# Each entry is a CSV path (resolved via get_inference_file_path) whose rows
|
|
280
|
+
# describe individual .h5 observations to be grouped into cadences.
|
|
281
|
+
# If non-None, takes precedence over test_files during inference and triggers
|
|
282
|
+
# the energy detection preprocessing pipeline.
|
|
283
|
+
inference_files: list[str] | None = field(default_factory=lambda: None)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class TrainingConfig:
|
|
288
|
+
# Reproducibility now lives in ReproducibilityConfig (#279): one root seed shared by both
|
|
289
|
+
# pipelines instead of a training-only seed plus an independent rf.seed.
|
|
290
|
+
|
|
291
|
+
num_training_rounds: int = 20
|
|
292
|
+
epochs_per_round: int = 100
|
|
293
|
+
|
|
294
|
+
# Posterior-collapse guard (#282): a latent dim counts ACTIVE while its batch-mean KL
|
|
295
|
+
# exceeds kl_epsilon; a round alarms (advisory WARNING, never fatal) when the active
|
|
296
|
+
# fraction drops below min_active_units_fraction or any dim sits under epsilon for
|
|
297
|
+
# `patience` consecutive epochs
|
|
298
|
+
posterior_collapse_kl_epsilon: float = 0.01
|
|
299
|
+
min_active_units_fraction: float = 0.5
|
|
300
|
+
posterior_collapse_patience: int = 5
|
|
301
|
+
|
|
302
|
+
num_samples_beta_vae: int = 499200
|
|
303
|
+
num_samples_rf: int = 99840 # NOTE: come back to this later
|
|
304
|
+
train_val_split: float = 0.8
|
|
305
|
+
|
|
306
|
+
per_replica_batch_size: int = (
|
|
307
|
+
128 # Throughput-optimal per GPU (see benchmarks/README.md GPU sweep)
|
|
308
|
+
)
|
|
309
|
+
# Gradient-accumulation target. 7680 = lcm(per_replica_batch_size * {4,5,6}), so effective_batch_size
|
|
310
|
+
# is divisible by per_replica_batch_size * num_replicas on 4-, 5-, or 6-GPU hosts (and divides the
|
|
311
|
+
# 399360-sample train split); it makes the defaults valid on every supported GPU count, including
|
|
312
|
+
# the 5-GPU Blackwell cluster where the previous 3072 failed. On a fixed GPU count you can override
|
|
313
|
+
# with a smaller multiple (e.g. --effective-batch-size 3072 on 4 or 6 GPUs) to accumulate over fewer
|
|
314
|
+
# micro-batches; per-GPU throughput is unchanged (set by per_replica_batch_size).
|
|
315
|
+
effective_batch_size: int = 7680
|
|
316
|
+
# Validation / viz / RF-latent-generation batch per GPU. Its global size (64 * num_replicas) must
|
|
317
|
+
# divide the val split (99840), num_samples_rf (99840), and latent_viz_num_cadences_per_type * 4;
|
|
318
|
+
# 64 is paired with latent_viz_num_cadences_per_type=960 (below) so all three hold on 4-, 5-, or
|
|
319
|
+
# 6-GPU hosts. Runs off the training hot path, but 64 (vs the bare divisibility floor of 16 at
|
|
320
|
+
# latent_viz=240) keeps validation / latent-generation out of the small-batch launch-overhead
|
|
321
|
+
# regime (see the encode sweep in benchmarks/README.md) — a net win over a full run's ~2000
|
|
322
|
+
# validations. Raising V requires raising latent_viz in lockstep (V=128 needs latent_viz=1920).
|
|
323
|
+
per_replica_val_batch_size: int = 64
|
|
324
|
+
|
|
325
|
+
# Signal injection params
|
|
326
|
+
# TODO: experiment with larger chunk sizes (how to track chunk processing efficiency)
|
|
327
|
+
signal_injection_chunk_size: int = (
|
|
328
|
+
50000 # Maximum cadences to process at once during data generation
|
|
329
|
+
)
|
|
330
|
+
# Tuned to 64 via smoke-scale (n=8192) task-size sweeps on bla0 (96c) + blpc3 (32c):
|
|
331
|
+
# finer tasks load-balance the create_true_double straggler with negligible per-task
|
|
332
|
+
# overhead; 64 was near-optimal on both (256 ran ~2x slower on bla0).
|
|
333
|
+
# TODO: re-confirm at production sample sizes (~500k) before treating as final.
|
|
334
|
+
data_gen_task_size: int = 64 # Cadences per batched worker task (workers write results straight into the round memmap)
|
|
335
|
+
# On-disk dtype of the three round cadence arrays (main/true/false). "float16" halves the
|
|
336
|
+
# round footprint (~294.5 -> ~147 GB at full scale), the gather volume, and the page-cache
|
|
337
|
+
# working set — the lever that keeps overlapped epochs at page-cache speed once two rounds
|
|
338
|
+
# no longer fit in RAM — at a quantization cost of <= 2^-12 on the [0, 1] log-normalized
|
|
339
|
+
# inputs. DEFAULT FLIPPED to "float16" 2026-07-29 after passing the val-metric A/B gate:
|
|
340
|
+
# 4 seeds, every score metric inside the 6-seed control spread (val AUC >= .9979, recalls
|
|
341
|
+
# .9619-.9907 vs control .9449-.9907), AU within the controls' own 6-8 seed variation, no
|
|
342
|
+
# calibration trips. The gather map upcasts to float32 host-side, so the training graph
|
|
343
|
+
# and loss math are unchanged either way; labels + lognorm sidecars stay float32.
|
|
344
|
+
# Recorded in each round's .done manifest and validated on resume/reuse ("float32"
|
|
345
|
+
# restores the historical behavior byte-for-byte).
|
|
346
|
+
round_array_dtype: str = "float16"
|
|
347
|
+
|
|
348
|
+
# Round data pipeline params (disk-backed per-round datasets, see round_data.py)
|
|
349
|
+
round_data_dir: str | None = None # Defaults to get_training_file_path("round_data") at runtime
|
|
350
|
+
overlap_data_generation: bool = (
|
|
351
|
+
True # Generate round k+1's data in a background process while round k trains
|
|
352
|
+
)
|
|
353
|
+
keep_round_data: bool = (
|
|
354
|
+
False # Retain each round's on-disk data after its training completes (debugging)
|
|
355
|
+
)
|
|
356
|
+
# TODO: tune to include sufficient info without bottlenecking training
|
|
357
|
+
plot_injection_subsampling_count: int = (
|
|
358
|
+
100000 # Max points per series in injection_stats scatter plots
|
|
359
|
+
)
|
|
360
|
+
# TODO: tune to include sufficient info without bottlenecking training
|
|
361
|
+
plot_injection_outlier_percentile: float = (
|
|
362
|
+
99.0 # Always include injection_stats points beyond this percentile
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Latent space visualization params
|
|
366
|
+
# Cadences per signal type for the latent-space viz batch (total = 4×). 960 rather than a smaller
|
|
367
|
+
# value so latent_viz*4 (3840) is divisible by per_replica_val_batch_size * num_replicas on 4-, 5-,
|
|
368
|
+
# or 6-GPU hosts (this is the binding constraint that lets per_replica_val_batch_size be 64 instead
|
|
369
|
+
# of 16); it also yields denser latent-space plots. Must stay <= the val split (99840).
|
|
370
|
+
latent_viz_num_cadences_per_type: int = 960
|
|
371
|
+
latent_viz_step_interval: int = 10 # Capture snapshot every N training steps
|
|
372
|
+
latent_viz_umap_fit_max_samples: int = (
|
|
373
|
+
100_000 # Max pooled vectors for UMAP fit (rest are projected via .transform())
|
|
374
|
+
)
|
|
375
|
+
# Note, Python dataclasses don't allow mutable objects (e.g. lists) to be used as defaults,
|
|
376
|
+
# since Python will create that object once when the class is defined, rather than each time
|
|
377
|
+
# a new object of that class is instantiated. This means that all instances of that class
|
|
378
|
+
# would share the same mutable object in memory (i.e. if we modified latent_viz_umap_n_neighbors
|
|
379
|
+
# in one instance, it would affect all other instances -- a dangerous bug).
|
|
380
|
+
# The default_factory parameter takes a callable (lambda function) that's called each time a
|
|
381
|
+
# new instance is created, ensuring each instance gets its own independent list, preventing
|
|
382
|
+
# the shared-state bug. Note that once created, the list behaves identical to any other list
|
|
383
|
+
latent_viz_umap_n_neighbors: list[int] = field(
|
|
384
|
+
# UMAP n_neighbors values to sweep
|
|
385
|
+
# n_neighbors constrains the size of the local neighborhood UMAP will look at when attempting
|
|
386
|
+
# to learn the manifold structure of the data. Lower values lead to better local structure,
|
|
387
|
+
# whereas larger values yield better global structure
|
|
388
|
+
# - 5: fine-grained local structure
|
|
389
|
+
# - 15: UMAP default, good baseline
|
|
390
|
+
# - 50: global topology emphasis
|
|
391
|
+
# (30 was dropped from the default sweep 2026-07: it sat between 15 and 50 without
|
|
392
|
+
# adding a distinct view, and each nn value costs 3 min_dist x 2 level combos)
|
|
393
|
+
default_factory=lambda: [5, 15, 50]
|
|
394
|
+
)
|
|
395
|
+
latent_viz_umap_min_dist: list[float] = field(
|
|
396
|
+
# UMAP min_dist values to sweep
|
|
397
|
+
# min_dist controls how tightly points in the lower-dimensional representation can be packed.
|
|
398
|
+
# Lower values result in clumpier embeddings (useful for observing clusters), whereas larger
|
|
399
|
+
# values preserve more of the broad topological structure
|
|
400
|
+
# - 0.0: maximum cluster tightness
|
|
401
|
+
# - 0.1: UMAP default, slight breathing room
|
|
402
|
+
# - 0.5: spread out, reveals continuous gradients
|
|
403
|
+
default_factory=lambda: [0.0, 0.1, 0.5]
|
|
404
|
+
)
|
|
405
|
+
latent_viz_gif_max_frames: int = 500 # Max frames in output GIF (log-spaced subsampling)
|
|
406
|
+
latent_viz_gif_duration_ms: int = 100 # Milliseconds per frame in output GIF
|
|
407
|
+
|
|
408
|
+
# Latent traversal params (decoder-based interpretation of the latent dims; see
|
|
409
|
+
# TrainingPipeline.plot_latent_traversal)
|
|
410
|
+
latent_traversal_every_round: bool = (
|
|
411
|
+
False # Also render traversal figures at the end of every training round
|
|
412
|
+
)
|
|
413
|
+
latent_traversal_num_steps: int = (
|
|
414
|
+
7 # Steps per latent dim (odd & >= 3 so the center column is the unperturbed decode)
|
|
415
|
+
)
|
|
416
|
+
latent_traversal_max_sigma: float = (
|
|
417
|
+
3.0 # Traversal range in per-dim standard deviations: steps span ±max_sigma (> 0)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# RF visualization params
|
|
421
|
+
shap_max_samples_summary: int = 5000 # Samples for SHAP summary/dependence computation
|
|
422
|
+
shap_max_samples_interaction: int = (
|
|
423
|
+
1500 # Samples for SHAP interaction values (O(F^2) per sample)
|
|
424
|
+
)
|
|
425
|
+
shap_top_k_features_dependence: int = (
|
|
426
|
+
48 # Number of dependence plot panels (all 6 obs * 8 dims by default)
|
|
427
|
+
)
|
|
428
|
+
rf_decision_boundary_grid_size: int = (
|
|
429
|
+
150 # Grid resolution for decision boundary contour (grid_size x grid_size)
|
|
430
|
+
)
|
|
431
|
+
rf_decision_boundary_max_points: int = (
|
|
432
|
+
5000 # Subsample val points for decision boundary plot legibility
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Model-quality gate params (issue #139 Gate 1)
|
|
436
|
+
min_val_auc: float = 0.0 # Opt-in floor on the RF's validation ROC-AUC; 0.0 disables the check. When set and unmet after the RF fit, training logs a loud WARNING (reaches the Slack summary) rather than failing the run
|
|
437
|
+
|
|
438
|
+
# Curriculum learning params
|
|
439
|
+
snr_base: int = 10
|
|
440
|
+
initial_snr_range: int = 40
|
|
441
|
+
final_snr_range: int = 10
|
|
442
|
+
curriculum_schedule: str = "exponential" # "linear", "exponential", "step"
|
|
443
|
+
exponential_decay_rate: float = -3.0 # How quickly schedule should progress from easy to hard (must be <0) (more negative = less easy rounds & more hard rounds)
|
|
444
|
+
# TODO: generalize this to receive a step schedule (as a list/dict?) validate that len(list/dict) is divisible by num_training_rounds
|
|
445
|
+
step_easy_rounds: int = 5 # Number of rounds with easy signals
|
|
446
|
+
step_hard_rounds: int = 15 # Number of rounds with challenging signals
|
|
447
|
+
|
|
448
|
+
# Adaptive LR params
|
|
449
|
+
base_learning_rate: float = 0.001
|
|
450
|
+
min_learning_rate: float = 1e-6
|
|
451
|
+
min_pct_improvement: float = 0.001 # 0.1% val loss improvement
|
|
452
|
+
patience_threshold: int = 3 # consecutive epochs with no improvement
|
|
453
|
+
reduction_factor: float = 0.2 # 20% LR reduction
|
|
454
|
+
|
|
455
|
+
# NOTE: should we try an exponential backoff?
|
|
456
|
+
# Fault tolerance params
|
|
457
|
+
max_retries: int = 3
|
|
458
|
+
retry_delay: int = 60 # seconds
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass
|
|
462
|
+
class InferenceConfig:
|
|
463
|
+
"""Inference configuration"""
|
|
464
|
+
|
|
465
|
+
# TODO: have a startup function that sets encoder_path, rf_path, and config_path using load_dir & load_tag if either/both are None
|
|
466
|
+
encoder_path: str = None
|
|
467
|
+
rf_path: str = None
|
|
468
|
+
config_path: str = None
|
|
469
|
+
# Per-replica encode batch in SNIPPETS (each snippet = num_observations independent
|
|
470
|
+
# encoder inputs, so 256 snippets = 1,536 observation forwards — near the measured
|
|
471
|
+
# ~1024-obs single-GPU encode throughput peak; see benchmarks/README.md). The old 2048
|
|
472
|
+
# default was a units conflation (#298 I4): 2048 SNIPPETS = a 12,288-obs per-replica
|
|
473
|
+
# forward, past the throughput peak and above the bench's documented >~8192-obs int32
|
|
474
|
+
# launch-config abort. Also the cap of _distributed_encode's bucketed batch geometry.
|
|
475
|
+
per_replica_batch_size: int = 256
|
|
476
|
+
classification_threshold: float = 0.99
|
|
477
|
+
# Two-pass cascade (#282). Pass 1 scores EVERY snippet deterministically (z_mean-based
|
|
478
|
+
# features) against this permissive screening threshold, tuned for recall — its only job
|
|
479
|
+
# is "definitely not a candidate". Pass 2 re-scores the survivors with mc_draws seeded
|
|
480
|
+
# latent samples and carries the actual science threshold (classification_threshold
|
|
481
|
+
# above, applied to the MC mean). The two are a cascade, not two ANDed criteria.
|
|
482
|
+
screening_threshold: float = 0.5
|
|
483
|
+
mc_draws: int = 32
|
|
484
|
+
# Reference cloud (#282): pass 2 also MC-scores a seeded uniform reservoir subsample of
|
|
485
|
+
# the pass-1 REJECTS, persisted per run, so the candidate uncertainty plot compares each
|
|
486
|
+
# candidate against the survey population rather than against other candidates. 0
|
|
487
|
+
# disables the cloud (and the plot's background).
|
|
488
|
+
reference_cloud_size: int = 10000
|
|
489
|
+
# Streaming-loop prefetch depth (#298 N2): cadences preprocessed+loaded concurrently
|
|
490
|
+
# ahead of the GPU stage, overlapping disk-bound energy detection with
|
|
491
|
+
# decompression-bound extraction and the per-cadence serial sections, at the cost of
|
|
492
|
+
# one in-flight cadence of RAM per unit of depth (stamps + loaded array — up to
|
|
493
|
+
# ~65 GB each for RFI-dense C-band cadences, so depth 3 budgets ~4 in-flight worst
|
|
494
|
+
# case ≈ 260 GB on a 503 GB host). Default 3 per the #301 on-cluster A/B (the same 8
|
|
495
|
+
# fresh /datag cadences as the #298 depth-1→2 A/B: depth 3 = 4,071 s vs depth 2 =
|
|
496
|
+
# 5,118 s wall, ~-20%, identical candidates with 0.0 score deltas; caveat: the
|
|
497
|
+
# depth-3 leg ran last and warmest, so treat the honest win as ~10-20% — same
|
|
498
|
+
# confound structure the 1→2 flip carried). Per-cadence outputs are identical at any
|
|
499
|
+
# depth (results are consumed in catalog order and seeding keys on the catalog
|
|
500
|
+
# index); depth 1 restores the historical serial behavior.
|
|
501
|
+
prefetch_depth: int = 3
|
|
502
|
+
|
|
503
|
+
# NOTE: come back to this later (is this the optimal grouping?)
|
|
504
|
+
# Energy detection preprocessing
|
|
505
|
+
cadence_group_by_cols: list[str] = field(
|
|
506
|
+
default_factory=lambda: ["Target", "Session", "Band", "Cadence ID", "Frequency"]
|
|
507
|
+
)
|
|
508
|
+
cadence_h5_path_col: str = ".h5 path"
|
|
509
|
+
cadence_expected_obs: int = 6 # expected observations per cadence (ABACAD)
|
|
510
|
+
|
|
511
|
+
# NOTE: come back to this later (are these params correct?)
|
|
512
|
+
coarse_channel_width: int = 1048576
|
|
513
|
+
# Progress-logging cadence for energy detection. None (default) logs ~25% milestones
|
|
514
|
+
# per ON file (#301 — the per-channel lines were 62% of a run's log volume, all
|
|
515
|
+
# Slack-bound); an explicit N restores the historical every-N-channels lines.
|
|
516
|
+
# Parallelism itself comes from the persistent worker pool (one fused task per coarse
|
|
517
|
+
# channel), not from this knob.
|
|
518
|
+
coarse_channel_log_interval: int | None = None
|
|
519
|
+
# Bandpass flattening method for energy detection: "pfb" divides each coarse channel by
|
|
520
|
+
# the instrument's static polyphase-filterbank response (computed once per run); "spline"
|
|
521
|
+
# fits and subtracts a spline per coarse channel per file (the historical, data-driven
|
|
522
|
+
# method).
|
|
523
|
+
bandpass_method: str = "pfb"
|
|
524
|
+
# PFB prototype-filter taps per coarse channel. Instrument-dependent: 12 is the GBT /
|
|
525
|
+
# Breakthrough Listen backend default, and it must match the backend that produced the
|
|
526
|
+
# .h5 files being processed.
|
|
527
|
+
pfb_taps_per_channel: int = 12
|
|
528
|
+
# Opt-in debug artifact: save a raw-vs-flattened overlay plot (a few sampled coarse
|
|
529
|
+
# channels per cadence) under {output_path}/plots/inference/.
|
|
530
|
+
bandpass_debug_plot: bool = False
|
|
531
|
+
spline_order: int = 16
|
|
532
|
+
detection_window_size: int = 256
|
|
533
|
+
detection_step_size: int = 128
|
|
534
|
+
stat_threshold: float = 2048.0
|
|
535
|
+
stamp_width: int = 4096
|
|
536
|
+
# Downsample stamps along frequency at extraction time (by data.downsample_factor), so
|
|
537
|
+
# the per-cadence .npy stores stamp_width // downsample_factor bins (~8x smaller at
|
|
538
|
+
# defaults). Disable to archive raw-resolution stamps; loading handles both layouts.
|
|
539
|
+
store_downsampled_stamps: bool = True
|
|
540
|
+
|
|
541
|
+
# NOTE: come back to this later (is overlap search implemented correctly? redo analysis from peter forwards search paper)
|
|
542
|
+
overlap_search: bool = True
|
|
543
|
+
overlap_fraction: float = 0.5
|
|
544
|
+
|
|
545
|
+
# NOTE: come back to this later (placeholder for future drop logic — wired but inert)
|
|
546
|
+
discard_side_channels: bool = False
|
|
547
|
+
side_channel_count: int = 0
|
|
548
|
+
|
|
549
|
+
# Per-cadence .npy output directory for energy-detection preprocessing. None (default)
|
|
550
|
+
# resolves per CSV to {data_path}/inference/preprocessed/<csv_stem>_ed<hash12>/, keyed
|
|
551
|
+
# on the ED-config fingerprint (#298 I3): runs sharing an ED config share stamps (a
|
|
552
|
+
# threshold sweep or re-inference with new weights skips preprocessing entirely), while
|
|
553
|
+
# any ED-config change lands in a different directory by construction. Set explicitly
|
|
554
|
+
# to pin one directory across runs and CSVs (the resume guard still verifies each
|
|
555
|
+
# sidecar's h5_paths and recorded fingerprint before reuse).
|
|
556
|
+
preprocess_output_dir: str | None = None
|
|
557
|
+
# Stamp-cache pruning (#302): delete a cadence's stamp .npy right after its 'inferred'
|
|
558
|
+
# manifest row lands (metadata .json always kept; resume rides the DB row; each
|
|
559
|
+
# candidate's snippet is snapshotted into a ~196 KB .candidates.npz sidecar so the
|
|
560
|
+
# candidate figures survive, and a bounded top-K pixel pool — persisted across the
|
|
561
|
+
# in-process retry attempts, #305 — keeps the stamp gallery whole within one run).
|
|
562
|
+
# Without pruning a full catalog writes ~30-90 TB of stamps vs ~2.7 TB free on /datax —
|
|
563
|
+
# the run dies on disk after a few hundred cadences. None (default) = AUTO: ON for the
|
|
564
|
+
# fingerprint-scoped default cache dir, OFF when preprocess_output_dir is explicitly set
|
|
565
|
+
# (an operator-curated cache is never destroyed implicitly). Only stamps THIS run
|
|
566
|
+
# extracted are pruned (a resumed run never deletes a handed cache; a failed-then-retried
|
|
567
|
+
# cadence of the same run IS pruned). Same-run resume/retry is science-unaffected.
|
|
568
|
+
# Known limitations (viz-only, graceful): a cross-PROCESS relaunch cannot recover an
|
|
569
|
+
# earlier process's pruned pixels, so its stamp gallery may show blank columns for
|
|
570
|
+
# earlier-run cadences; and concurrent runs sharing the default cache dir with pruning
|
|
571
|
+
# ON can race (one deletes/overwrites a .npy or .candidates.npz another is mid-read of —
|
|
572
|
+
# self-heals via retry). Use a per-run --preprocess-output-dir or --no-prune-stamps to
|
|
573
|
+
# run concurrently in one dir. Trade: pruning forfeits the cross-run stamp-reuse rerun
|
|
574
|
+
# win for pruned cadences (re-extraction ~5-15 min each).
|
|
575
|
+
prune_stamps: bool | None = None
|
|
576
|
+
|
|
577
|
+
# Visualization suite (aetherscan.inference_viz): rendered at the end of a streaming
|
|
578
|
+
# CSV inference run, saved under {output_path}/plots/inference/{save_tag}/ and uploaded
|
|
579
|
+
# to Slack. Every figure is individually exception-guarded — a plot bug can never kill
|
|
580
|
+
# a science run.
|
|
581
|
+
inference_viz_enabled: bool = True
|
|
582
|
+
# Which cadences the metadata-driven figures cover: "full" (default) renders the whole
|
|
583
|
+
# accumulated tag every successful pass; "new" renders only cadences inferred THIS
|
|
584
|
+
# pass (#301 — a resumed catalog campaign re-paid the full O(catalog) viz tail on
|
|
585
|
+
# every pass). DB-sourced candidate figures always cover the full tag either way.
|
|
586
|
+
inference_viz_scope: str = "full"
|
|
587
|
+
# Number of top-statistic stamps shown in the stamp gallery (6-obs waterfall grids).
|
|
588
|
+
stamp_gallery_top_k: int = 12
|
|
589
|
+
# Cap on per-candidate figures (candidate_{i}_{tag}.png), highest confidence first.
|
|
590
|
+
max_candidate_plots: int = 50
|
|
591
|
+
|
|
592
|
+
# NOTE: come back to this later (is this implemented correctly?)
|
|
593
|
+
# Fault tolerance
|
|
594
|
+
max_retries: int = 3
|
|
595
|
+
retry_delay: int = 60 # seconds
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@dataclass
|
|
599
|
+
class HFConfig:
|
|
600
|
+
"""HuggingFace Hub integration configuration"""
|
|
601
|
+
|
|
602
|
+
# Target model repo for weight upload (train) and download (inference).
|
|
603
|
+
repo_id: str = "zachtheyek/aetherscan"
|
|
604
|
+
# Opt-in: publish the final artifacts + a generated model card after training completes
|
|
605
|
+
# (requires HF_TOKEN in the environment; default off = local-only).
|
|
606
|
+
upload_after_training: bool = False
|
|
607
|
+
# Inference pin: HF revision (tag/branch/commit) to download artifacts from. None
|
|
608
|
+
# resolves to the latest release tag (highest semver vX.Y.Z); a release tag is required.
|
|
609
|
+
revision: str | None = None
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@dataclass
|
|
613
|
+
class CheckpointConfig:
|
|
614
|
+
"""Checkpoint configuration"""
|
|
615
|
+
|
|
616
|
+
load_dir: str | None = None
|
|
617
|
+
load_tag: str | None = None
|
|
618
|
+
start_round: int = 1
|
|
619
|
+
# Resolved once at runtime by cli.resolve_save_tag (in main(), before init_logger) to
|
|
620
|
+
# {command}_{YYYYMMDD_HHMMSS}. None until then — the pipeline always runs through the CLI,
|
|
621
|
+
# which sets it before any stage reads it.
|
|
622
|
+
save_tag: str | None = None
|
|
623
|
+
# Override the fail-early save-tag dedup guards (local artifact/DB collisions and the
|
|
624
|
+
# HF-side tag check) for an explicitly-provided save_tag.
|
|
625
|
+
force_tag: bool = False
|
|
626
|
+
|
|
627
|
+
def infer_start_round(self):
|
|
628
|
+
"""Infer start_round from load_tag"""
|
|
629
|
+
if self.load_tag and self.load_tag.startswith("round_"):
|
|
630
|
+
self.start_round = (
|
|
631
|
+
int(self.load_tag.split("_", 1)[1]) + 1
|
|
632
|
+
) # Start from the round proceeding model checkpoint (round_XX + 1)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class Config:
|
|
636
|
+
"""Main configuration class"""
|
|
637
|
+
|
|
638
|
+
_instance = None # Stores singleton instance
|
|
639
|
+
_lock = threading.Lock() # Ensures thread safety on object initialization
|
|
640
|
+
|
|
641
|
+
# __new__ allocates the object in memory (constructor at the object-creation level)
|
|
642
|
+
# __init__ initializes the object's attributes after it's created
|
|
643
|
+
# since __new__ is called before __init__ every time we instantiate a class,
|
|
644
|
+
# by overriding __new__, we can short-circuit object creation entirely, and control whether a
|
|
645
|
+
# new instance is created, or just return the existing instance
|
|
646
|
+
def __new__(cls):
|
|
647
|
+
# Double-checked locking pattern:
|
|
648
|
+
# First check if _instance is None, without lock (for performance)
|
|
649
|
+
if cls._instance is None:
|
|
650
|
+
# If None, acquire the lock to serialize the initialization path,
|
|
651
|
+
# preventing race conditions (2 threads violating singleton semantics)
|
|
652
|
+
with cls._lock:
|
|
653
|
+
# Check if _instance is None again inside the lock
|
|
654
|
+
# (since multiple threads can be calling simultaneously)
|
|
655
|
+
if cls._instance is None:
|
|
656
|
+
# If still None, only then we construct the singleton instance
|
|
657
|
+
cls._instance = super().__new__(cls)
|
|
658
|
+
cls._instance._initialized = False # Mark as not initialized (for __init__)
|
|
659
|
+
# Return the same instance for all subsequent constructor calls
|
|
660
|
+
return cls._instance
|
|
661
|
+
|
|
662
|
+
def __init__(self):
|
|
663
|
+
"""Initialize configuration"""
|
|
664
|
+
# Note, __init__ is triggered every time the class's constructor is called,
|
|
665
|
+
# even if __new__ returned the existing singleton instance
|
|
666
|
+
# Hence, we use the _initialized flag to make sure __init__ only runs once
|
|
667
|
+
if self._initialized:
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
self._initialized = True
|
|
671
|
+
|
|
672
|
+
self.db = DBConfig()
|
|
673
|
+
self.manager = ManagerConfig()
|
|
674
|
+
self.monitor = MonitorConfig()
|
|
675
|
+
self.logger = LoggerConfig()
|
|
676
|
+
self.reproducibility = ReproducibilityConfig()
|
|
677
|
+
self.beta_vae = BetaVAEConfig()
|
|
678
|
+
self.rf = RandomForestConfig()
|
|
679
|
+
self.gpu = GPUConfig()
|
|
680
|
+
self.data = DataConfig()
|
|
681
|
+
self.training = TrainingConfig()
|
|
682
|
+
self.inference = InferenceConfig()
|
|
683
|
+
self.hf = HFConfig()
|
|
684
|
+
self.checkpoint = CheckpointConfig()
|
|
685
|
+
|
|
686
|
+
# Paths
|
|
687
|
+
self.data_path = os.environ.get(
|
|
688
|
+
"AETHERSCAN_DATA_PATH", "/datax/scratch/zachy/data/aetherscan"
|
|
689
|
+
)
|
|
690
|
+
self.model_path = os.environ.get(
|
|
691
|
+
"AETHERSCAN_MODEL_PATH", "/datax/scratch/zachy/models/aetherscan"
|
|
692
|
+
)
|
|
693
|
+
self.output_path = os.environ.get(
|
|
694
|
+
"AETHERSCAN_OUTPUT_PATH", "/datax/scratch/zachy/outputs/aetherscan"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Coupled-defaults guard: inference.stamp_width and data.width_bin are independent
|
|
698
|
+
# dataclass fields that must agree — energy-detection stamps are cut at stamp_width
|
|
699
|
+
# while every loader/model surface is sized off width_bin. A divergent source-level
|
|
700
|
+
# default edit would otherwise surface only at load time (load_inference_data fails
|
|
701
|
+
# safe, but late and per-file); fail here, at config init. CLI overrides are
|
|
702
|
+
# validated separately in cli.collect_validation_errors.
|
|
703
|
+
if self.inference.stamp_width != self.data.width_bin:
|
|
704
|
+
raise ValueError(
|
|
705
|
+
f"Config defaults diverged: inference.stamp_width "
|
|
706
|
+
f"({self.inference.stamp_width}) must equal data.width_bin "
|
|
707
|
+
f"({self.data.width_bin})"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def resolved_rf_seed(self) -> int:
|
|
711
|
+
"""
|
|
712
|
+
The Random Forest random_state actually used this run (#279): rf.seed when explicitly
|
|
713
|
+
overridden (deprecated --rf-seed), else derived from the root seed via STREAM_RF.
|
|
714
|
+
Always a concrete int — with an unseeded root the derived value is OS entropy, so the
|
|
715
|
+
RF still gets a set random_state (the #279 constraint: never drop one that exists).
|
|
716
|
+
"""
|
|
717
|
+
# Deferred import: config must stay importable before/without the rest of the package
|
|
718
|
+
from aetherscan.seeding import STREAM_RF, derive_seed # noqa: PLC0415
|
|
719
|
+
|
|
720
|
+
if self.rf.seed is not None:
|
|
721
|
+
return self.rf.seed
|
|
722
|
+
return derive_seed(self.reproducibility.seed, STREAM_RF)
|
|
723
|
+
|
|
724
|
+
@classmethod
|
|
725
|
+
def _reset(cls):
|
|
726
|
+
"""
|
|
727
|
+
Teardown hook for thread-safe singleton
|
|
728
|
+
Resets the config instance to None
|
|
729
|
+
|
|
730
|
+
WARNING: Only use for testing or restarting the application
|
|
731
|
+
Calling this while the config is active will cause issues.
|
|
732
|
+
Do NOT call this method unless you know what you're doing
|
|
733
|
+
"""
|
|
734
|
+
# Acquire lock to prevent race conditions
|
|
735
|
+
with cls._lock:
|
|
736
|
+
# Discard the singleton instance by removing the global reference
|
|
737
|
+
# Guarantees the next constructor call will produce a fresh instance
|
|
738
|
+
# Note, resources held by the old instance will remain alive unless explicitly closed beforehand
|
|
739
|
+
cls._instance = None
|
|
740
|
+
|
|
741
|
+
def get_training_file_path(self, filename: str, base_path: str | None = None) -> str:
|
|
742
|
+
"""Get full path for training data files. base_path overrides self.data_path (used by
|
|
743
|
+
validate_args, which runs before a --data-path override is applied to the singleton)."""
|
|
744
|
+
return os.path.join(
|
|
745
|
+
base_path if base_path is not None else self.data_path, "training", filename
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
def get_test_file_path(self, filename: str, base_path: str | None = None) -> str:
|
|
749
|
+
"""Get full path for testing data files. base_path overrides self.data_path (used by
|
|
750
|
+
validate_args, which runs before a --data-path override is applied to the singleton)."""
|
|
751
|
+
return os.path.join(
|
|
752
|
+
base_path if base_path is not None else self.data_path, "testing", filename
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
def get_inference_file_path(self, filename: str, base_path: str | None = None) -> str:
|
|
756
|
+
"""Get full path for inference CSV files. base_path overrides self.data_path (used by
|
|
757
|
+
validate_args, which runs before a --data-path override is applied to the singleton)."""
|
|
758
|
+
return os.path.join(
|
|
759
|
+
base_path if base_path is not None else self.data_path, "inference", filename
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
def get_file_subset(self, filename: str) -> tuple[int | None, int | None]:
|
|
763
|
+
"""Get subset parameters for a file (start, end indices)"""
|
|
764
|
+
# Option to define subsets for specific files to manage memory usage
|
|
765
|
+
subset_map = {
|
|
766
|
+
"real_filtered_LARGE_HIP110750.npy": (None, None), # Shape: (14567, 6, 16, 4096)
|
|
767
|
+
"real_filtered_LARGE_HIP13402.npy": (None, None), # Shape: (14567, 6, 16, 4096)
|
|
768
|
+
"real_filtered_LARGE_HIP8497.npy": (None, None), # Shape: (14567, 6, 16, 4096)
|
|
769
|
+
"real_filtered_LARGE_testHIP83043.npy": (None, None), # Shape: (14567, 6, 16, 4096)
|
|
770
|
+
}
|
|
771
|
+
return subset_map.get(filename, (None, None))
|
|
772
|
+
|
|
773
|
+
def to_dict(self) -> dict:
|
|
774
|
+
"""Convert config to dictionary for serialization"""
|
|
775
|
+
return {
|
|
776
|
+
"paths": {
|
|
777
|
+
"data_path": self.data_path,
|
|
778
|
+
"model_path": self.model_path,
|
|
779
|
+
"output_path": self.output_path,
|
|
780
|
+
},
|
|
781
|
+
"db": {
|
|
782
|
+
"get_connection_timeout": self.db.get_connection_timeout,
|
|
783
|
+
"stop_writer_timeout": self.db.stop_writer_timeout,
|
|
784
|
+
"write_interval": self.db.write_interval,
|
|
785
|
+
"write_buffer_max_size": self.db.write_buffer_max_size,
|
|
786
|
+
"write_retry_delay": self.db.write_retry_delay,
|
|
787
|
+
"flush_timeout": self.db.flush_timeout,
|
|
788
|
+
"bulk_chunk_rows": self.db.bulk_chunk_rows,
|
|
789
|
+
"bulk_queue_max_items": self.db.bulk_queue_max_items,
|
|
790
|
+
"stop_drain_timeout": self.db.stop_drain_timeout,
|
|
791
|
+
},
|
|
792
|
+
"manager": {
|
|
793
|
+
"n_processes": self.manager.n_processes,
|
|
794
|
+
"chunks_per_worker": self.manager.chunks_per_worker,
|
|
795
|
+
"pool_terminate_timeout": self.manager.pool_terminate_timeout,
|
|
796
|
+
},
|
|
797
|
+
"monitor": {
|
|
798
|
+
"get_gpu_timeout": self.monitor.get_gpu_timeout,
|
|
799
|
+
"stop_monitor_timeout": self.monitor.stop_monitor_timeout,
|
|
800
|
+
"monitor_interval": self.monitor.monitor_interval,
|
|
801
|
+
"monitor_retry_delay": self.monitor.monitor_retry_delay,
|
|
802
|
+
"annotate_stages": self.monitor.annotate_stages,
|
|
803
|
+
"dashboard_enabled": self.monitor.dashboard_enabled,
|
|
804
|
+
"dashboard_port": self.monitor.dashboard_port,
|
|
805
|
+
"benchmark_report_enabled": self.monitor.benchmark_report_enabled,
|
|
806
|
+
},
|
|
807
|
+
"logger": {
|
|
808
|
+
"console_level": self.logger.console_level,
|
|
809
|
+
"file_level": self.logger.file_level,
|
|
810
|
+
"slack_level": self.logger.slack_level,
|
|
811
|
+
"slack_enabled": self.logger.slack_enabled,
|
|
812
|
+
"slack_channel": self.logger.slack_channel,
|
|
813
|
+
"slack_username": self.logger.slack_username,
|
|
814
|
+
"slack_timeout": self.logger.slack_timeout,
|
|
815
|
+
"slack_retry_attempts": self.logger.slack_retry_attempts,
|
|
816
|
+
"slack_buffer_size": self.logger.slack_buffer_size,
|
|
817
|
+
"slack_flush_interval": self.logger.slack_flush_interval,
|
|
818
|
+
"slack_broadcast_level": self.logger.slack_broadcast_level,
|
|
819
|
+
},
|
|
820
|
+
"beta_vae": {
|
|
821
|
+
"latent_dim": self.beta_vae.latent_dim,
|
|
822
|
+
"dense_layer_size": self.beta_vae.dense_layer_size,
|
|
823
|
+
"kernel_size": self.beta_vae.kernel_size,
|
|
824
|
+
"beta": self.beta_vae.beta,
|
|
825
|
+
"alpha": self.beta_vae.alpha,
|
|
826
|
+
"mixed_precision": self.beta_vae.mixed_precision,
|
|
827
|
+
"regularization_active": self.beta_vae.regularization_active,
|
|
828
|
+
},
|
|
829
|
+
"reproducibility": {
|
|
830
|
+
"seed": self.reproducibility.seed,
|
|
831
|
+
"tf_deterministic_ops": self.reproducibility.tf_deterministic_ops,
|
|
832
|
+
# Provenance only (#279): the concrete values derived from the root this run.
|
|
833
|
+
# Not a settable field — apply_saved_config skips unknown sub-keys, so a
|
|
834
|
+
# restored config re-derives from the root instead of pinning these.
|
|
835
|
+
"derived_rf_seed": self.resolved_rf_seed(),
|
|
836
|
+
},
|
|
837
|
+
"rf": {
|
|
838
|
+
"n_estimators": self.rf.n_estimators,
|
|
839
|
+
"bootstrap": self.rf.bootstrap,
|
|
840
|
+
"max_features": self.rf.max_features,
|
|
841
|
+
"n_jobs": self.rf.n_jobs,
|
|
842
|
+
# The override field (None = derived from reproducibility.seed; see
|
|
843
|
+
# reproducibility.derived_rf_seed for the value actually used)
|
|
844
|
+
"seed": self.rf.seed,
|
|
845
|
+
"latent_variant": self.rf.latent_variant,
|
|
846
|
+
"active_dims": self.rf.active_dims,
|
|
847
|
+
"z_aug_draws": self.rf.z_aug_draws,
|
|
848
|
+
"active_units_threshold": self.rf.active_units_threshold,
|
|
849
|
+
"selection_max_fpr": self.rf.selection_max_fpr,
|
|
850
|
+
"selection_bootstrap_rounds": self.rf.selection_bootstrap_rounds,
|
|
851
|
+
"max_ece": self.rf.max_ece,
|
|
852
|
+
"calibration_min_isotonic": self.rf.calibration_min_isotonic,
|
|
853
|
+
"calibration_active": self.rf.calibration_active,
|
|
854
|
+
"calibration_method": self.rf.calibration_method,
|
|
855
|
+
"val_selection_fraction": self.rf.val_selection_fraction,
|
|
856
|
+
"val_calibration_fraction": self.rf.val_calibration_fraction,
|
|
857
|
+
"screen_recall_tolerance": self.rf.screen_recall_tolerance,
|
|
858
|
+
},
|
|
859
|
+
"gpu": {
|
|
860
|
+
"num_replicas": self.gpu.num_replicas,
|
|
861
|
+
"per_gpu_memory_limit_mb": self.gpu.per_gpu_memory_limit_mb,
|
|
862
|
+
"nccl_num_packs": self.gpu.nccl_num_packs,
|
|
863
|
+
"use_async_allocator": self.gpu.use_async_allocator,
|
|
864
|
+
"gpu_thread_mode": self.gpu.gpu_thread_mode,
|
|
865
|
+
"gpu_thread_count": self.gpu.gpu_thread_count,
|
|
866
|
+
},
|
|
867
|
+
"data": {
|
|
868
|
+
"num_observations": self.data.num_observations,
|
|
869
|
+
"width_bin": self.data.width_bin,
|
|
870
|
+
"downsample_factor": self.data.downsample_factor,
|
|
871
|
+
"time_bins": self.data.time_bins,
|
|
872
|
+
"freq_resolution": self.data.freq_resolution,
|
|
873
|
+
"time_resolution": self.data.time_resolution,
|
|
874
|
+
"num_target_backgrounds": self.data.num_target_backgrounds,
|
|
875
|
+
"background_load_chunk_size": self.data.background_load_chunk_size,
|
|
876
|
+
"max_chunks_per_file": self.data.max_chunks_per_file,
|
|
877
|
+
"inference_background_load_chunk_size": self.data.inference_background_load_chunk_size,
|
|
878
|
+
"train_files": self.data.train_files,
|
|
879
|
+
"test_files": self.data.test_files,
|
|
880
|
+
"inference_files": self.data.inference_files,
|
|
881
|
+
},
|
|
882
|
+
"training": {
|
|
883
|
+
"num_training_rounds": self.training.num_training_rounds,
|
|
884
|
+
"epochs_per_round": self.training.epochs_per_round,
|
|
885
|
+
"posterior_collapse_kl_epsilon": self.training.posterior_collapse_kl_epsilon,
|
|
886
|
+
"min_active_units_fraction": self.training.min_active_units_fraction,
|
|
887
|
+
"posterior_collapse_patience": self.training.posterior_collapse_patience,
|
|
888
|
+
"num_samples_beta_vae": self.training.num_samples_beta_vae,
|
|
889
|
+
"num_samples_rf": self.training.num_samples_rf,
|
|
890
|
+
"train_val_split": self.training.train_val_split,
|
|
891
|
+
"per_replica_batch_size": self.training.per_replica_batch_size,
|
|
892
|
+
"effective_batch_size": self.training.effective_batch_size,
|
|
893
|
+
"per_replica_val_batch_size": self.training.per_replica_val_batch_size,
|
|
894
|
+
"signal_injection_chunk_size": self.training.signal_injection_chunk_size,
|
|
895
|
+
"data_gen_task_size": self.training.data_gen_task_size,
|
|
896
|
+
"round_array_dtype": self.training.round_array_dtype,
|
|
897
|
+
"round_data_dir": self.training.round_data_dir,
|
|
898
|
+
"overlap_data_generation": self.training.overlap_data_generation,
|
|
899
|
+
"keep_round_data": self.training.keep_round_data,
|
|
900
|
+
"plot_injection_subsampling_count": self.training.plot_injection_subsampling_count,
|
|
901
|
+
"plot_injection_outlier_percentile": self.training.plot_injection_outlier_percentile,
|
|
902
|
+
"latent_viz_num_cadences_per_type": self.training.latent_viz_num_cadences_per_type,
|
|
903
|
+
"latent_viz_step_interval": self.training.latent_viz_step_interval,
|
|
904
|
+
"latent_viz_umap_fit_max_samples": self.training.latent_viz_umap_fit_max_samples,
|
|
905
|
+
"latent_viz_umap_n_neighbors": self.training.latent_viz_umap_n_neighbors,
|
|
906
|
+
"latent_viz_umap_min_dist": self.training.latent_viz_umap_min_dist,
|
|
907
|
+
"latent_viz_gif_max_frames": self.training.latent_viz_gif_max_frames,
|
|
908
|
+
"latent_viz_gif_duration_ms": self.training.latent_viz_gif_duration_ms,
|
|
909
|
+
"latent_traversal_every_round": self.training.latent_traversal_every_round,
|
|
910
|
+
"latent_traversal_num_steps": self.training.latent_traversal_num_steps,
|
|
911
|
+
"latent_traversal_max_sigma": self.training.latent_traversal_max_sigma,
|
|
912
|
+
"shap_max_samples_summary": self.training.shap_max_samples_summary,
|
|
913
|
+
"shap_max_samples_interaction": self.training.shap_max_samples_interaction,
|
|
914
|
+
"shap_top_k_features_dependence": self.training.shap_top_k_features_dependence,
|
|
915
|
+
"rf_decision_boundary_grid_size": self.training.rf_decision_boundary_grid_size,
|
|
916
|
+
"rf_decision_boundary_max_points": self.training.rf_decision_boundary_max_points,
|
|
917
|
+
"min_val_auc": self.training.min_val_auc,
|
|
918
|
+
"snr_base": self.training.snr_base,
|
|
919
|
+
"initial_snr_range": self.training.initial_snr_range,
|
|
920
|
+
"final_snr_range": self.training.final_snr_range,
|
|
921
|
+
"curriculum_schedule": self.training.curriculum_schedule,
|
|
922
|
+
"exponential_decay_rate": self.training.exponential_decay_rate,
|
|
923
|
+
"step_easy_rounds": self.training.step_easy_rounds,
|
|
924
|
+
"step_hard_rounds": self.training.step_hard_rounds,
|
|
925
|
+
"base_learning_rate": self.training.base_learning_rate,
|
|
926
|
+
"min_learning_rate": self.training.min_learning_rate,
|
|
927
|
+
"min_pct_improvement": self.training.min_pct_improvement,
|
|
928
|
+
"patience_threshold": self.training.patience_threshold,
|
|
929
|
+
"reduction_factor": self.training.reduction_factor,
|
|
930
|
+
"max_retries": self.training.max_retries,
|
|
931
|
+
"retry_delay": self.training.retry_delay,
|
|
932
|
+
},
|
|
933
|
+
"inference": {
|
|
934
|
+
"encoder_path": self.inference.encoder_path,
|
|
935
|
+
"rf_path": self.inference.rf_path,
|
|
936
|
+
"config_path": self.inference.config_path,
|
|
937
|
+
"per_replica_batch_size": self.inference.per_replica_batch_size,
|
|
938
|
+
"classification_threshold": self.inference.classification_threshold,
|
|
939
|
+
"screening_threshold": self.inference.screening_threshold,
|
|
940
|
+
"mc_draws": self.inference.mc_draws,
|
|
941
|
+
"reference_cloud_size": self.inference.reference_cloud_size,
|
|
942
|
+
"prefetch_depth": self.inference.prefetch_depth,
|
|
943
|
+
"cadence_group_by_cols": self.inference.cadence_group_by_cols,
|
|
944
|
+
"cadence_h5_path_col": self.inference.cadence_h5_path_col,
|
|
945
|
+
"cadence_expected_obs": self.inference.cadence_expected_obs,
|
|
946
|
+
"coarse_channel_width": self.inference.coarse_channel_width,
|
|
947
|
+
"coarse_channel_log_interval": self.inference.coarse_channel_log_interval,
|
|
948
|
+
"bandpass_method": self.inference.bandpass_method,
|
|
949
|
+
"pfb_taps_per_channel": self.inference.pfb_taps_per_channel,
|
|
950
|
+
"bandpass_debug_plot": self.inference.bandpass_debug_plot,
|
|
951
|
+
"spline_order": self.inference.spline_order,
|
|
952
|
+
"detection_window_size": self.inference.detection_window_size,
|
|
953
|
+
"detection_step_size": self.inference.detection_step_size,
|
|
954
|
+
"stat_threshold": self.inference.stat_threshold,
|
|
955
|
+
"stamp_width": self.inference.stamp_width,
|
|
956
|
+
"store_downsampled_stamps": self.inference.store_downsampled_stamps,
|
|
957
|
+
"overlap_search": self.inference.overlap_search,
|
|
958
|
+
"overlap_fraction": self.inference.overlap_fraction,
|
|
959
|
+
"discard_side_channels": self.inference.discard_side_channels,
|
|
960
|
+
"side_channel_count": self.inference.side_channel_count,
|
|
961
|
+
"preprocess_output_dir": self.inference.preprocess_output_dir,
|
|
962
|
+
# NOTE: any key added here also enters BOTH inference fingerprints unless
|
|
963
|
+
# it joins the run_state.py denylists — prune_stamps and
|
|
964
|
+
# inference_viz_scope are excluded there (#301/#302: retention/viz only)
|
|
965
|
+
"prune_stamps": self.inference.prune_stamps,
|
|
966
|
+
"inference_viz_enabled": self.inference.inference_viz_enabled,
|
|
967
|
+
"inference_viz_scope": self.inference.inference_viz_scope,
|
|
968
|
+
"stamp_gallery_top_k": self.inference.stamp_gallery_top_k,
|
|
969
|
+
"max_candidate_plots": self.inference.max_candidate_plots,
|
|
970
|
+
"max_retries": self.inference.max_retries,
|
|
971
|
+
"retry_delay": self.inference.retry_delay,
|
|
972
|
+
},
|
|
973
|
+
"hf": {
|
|
974
|
+
"repo_id": self.hf.repo_id,
|
|
975
|
+
"upload_after_training": self.hf.upload_after_training,
|
|
976
|
+
"revision": self.hf.revision,
|
|
977
|
+
},
|
|
978
|
+
"checkpoint": {
|
|
979
|
+
"load_dir": self.checkpoint.load_dir,
|
|
980
|
+
"load_tag": self.checkpoint.load_tag,
|
|
981
|
+
"start_round": self.checkpoint.start_round,
|
|
982
|
+
"save_tag": self.checkpoint.save_tag,
|
|
983
|
+
"force_tag": self.checkpoint.force_tag,
|
|
984
|
+
},
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def init_config() -> Config:
|
|
989
|
+
"""
|
|
990
|
+
Initialize global config instance (call once at startup)
|
|
991
|
+
"""
|
|
992
|
+
config = Config()
|
|
993
|
+
return config
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
# NOTE: suppress None return type for now to reduce pyright errors
|
|
997
|
+
# def get_config() -> Config | None:
|
|
998
|
+
def get_config() -> Config:
|
|
999
|
+
"""
|
|
1000
|
+
Get the global config instance
|
|
1001
|
+
"""
|
|
1002
|
+
return Config._instance
|