|
| 1 | +""" |
| 2 | +End-to-end STRESS + smoke test for the full multi-detector consistency stack, |
| 3 | +exercising every option we have built, together: |
| 4 | +
|
| 5 | + - per-detector consistency heads (tc + mchirp), GroupNorm, dropout, |
| 6 | + - the 4-class non-astrophysical masker (signal+signal / signal+noise / |
| 7 | + signal+signal' / noise+noise) at ``p_non_astrophysical``, |
| 8 | + - recolour noise augmentation (already in the o3b noise sampler), |
| 9 | + - the ``torch.compile`` production path (fullgraph + dynamic), |
| 10 | + - MC-dropout inference. |
| 11 | +
|
| 12 | +Stages |
| 13 | +------ |
| 14 | + 1. build the all-options graph + COMPILED consistency model, |
| 15 | + 2. shape + class-composition probe (sampler, masker, one compiled forward), |
| 16 | + 3. long compiled run: throughput, peak GPU memory, finite loss every iter, |
| 17 | + 4. ``p_non_astrophysical`` sweep {0.0, 0.5} (eager): both extremes run finite, |
| 18 | + 5. MC-dropout inference on the consistency model: stochastic under eval. |
| 19 | +
|
| 20 | +Needs CUDA + the local data release + rebuilt fiducial PSDs; auto-skips where |
| 21 | +that environment is absent. Runnable standalone under the sage conda python: |
| 22 | +
|
| 23 | + python3 tests/test_consistency_stress.py |
| 24 | +""" |
| 25 | + |
| 26 | +import os |
| 27 | +import sys |
| 28 | +import time |
| 29 | +from pathlib import Path |
| 30 | + |
| 31 | +import torch |
| 32 | + |
| 33 | +RUN_DIR = Path(__file__).resolve().parent.parent / "runs" / "o3b" |
| 34 | +DATA = Path("/local/scratch/igr/nnarenraju/data_release") |
| 35 | + |
| 36 | +_HAS_CUDA = torch.cuda.is_available() |
| 37 | +_HAS_NOISE = (DATA / "o3b_dataset" / "data_H1_O3b.bin").exists() |
| 38 | +_HAS_FID = (RUN_DIR / "run_export" / "fiducial_psds" / "fiducial_H1_psd.bin").exists() |
| 39 | +_READY = _HAS_CUDA and _HAS_NOISE and _HAS_FID |
| 40 | + |
| 41 | +N_LONG_ITERS = 40 |
| 42 | +N_SWEEP_ITERS = 3 |
| 43 | + |
| 44 | + |
| 45 | +def _stage(name): |
| 46 | + print(f"\n{'='*70}\n[STAGE] {name}\n{'='*70}", flush=True) |
| 47 | + |
| 48 | + |
| 49 | +def _shp(name, t): |
| 50 | + if isinstance(t, (tuple, list)): |
| 51 | + for i, e in enumerate(t): |
| 52 | + _shp(f"{name}[{i}]", e) |
| 53 | + elif hasattr(t, "shape"): |
| 54 | + print(f" {name:22s} shape={tuple(t.shape)} dtype={t.dtype} dev={t.device}") |
| 55 | + else: |
| 56 | + print(f" {name:22s} {type(t).__name__}={t}") |
| 57 | + |
| 58 | + |
| 59 | +def _enter_run_dir(): |
| 60 | + sys.path.insert(0, str(RUN_DIR)) |
| 61 | + os.chdir(RUN_DIR) |
| 62 | + for m in ("config", "train_consistency"): |
| 63 | + sys.modules.pop(m, None) |
| 64 | + |
| 65 | + |
| 66 | +def _build(p_non_astro, dropout, compile_model): |
| 67 | + """Register o3b config with the given options and build the full consistency |
| 68 | + run (graph + processor + model + masker + losses + optimiser).""" |
| 69 | + import importlib |
| 70 | + |
| 71 | + config = importlib.import_module("config") |
| 72 | + tcmod = importlib.import_module("train_consistency") |
| 73 | + importlib.reload(config) |
| 74 | + importlib.reload(tcmod) |
| 75 | + config.O3bCFG.p_non_astrophysical = float(p_non_astro) |
| 76 | + config.O3bCFG.dropout = float(dropout) |
| 77 | + config.set_configs() |
| 78 | + |
| 79 | + from sage.core.config import get_cfg, get_data_cfg |
| 80 | + from sage.architecture.network import MSCNN1D_2DResNetCBAM_Consistency |
| 81 | + from sage.architecture.custom_losses import BCEWithPEsigmaLoss, ConsistencyNLLLoss |
| 82 | + from sage.data.non_astrophysical import NonAstrophysicalMasker |
| 83 | + from sage.factory import SageConsistencyTraining |
| 84 | + import torch.optim as optim |
| 85 | + from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts |
| 86 | + |
| 87 | + cfg, data_cfg = get_cfg(), get_data_cfg() |
| 88 | + signal_sampler, noise_sampler, bounds = tcmod.make_training_graph() |
| 89 | + processor, t_grid = tcmod.make_processor(bounds) |
| 90 | + |
| 91 | + model = MSCNN1D_2DResNetCBAM_Consistency( |
| 92 | + t_grid, frontend_filters=32, frontend_kernel=64, |
| 93 | + backend_resnet_size=50, norm_type="groupnorm", dropout=cfg.dropout, |
| 94 | + ).to(dtype=cfg.dtype, device=cfg.device, memory_format=torch.channels_last) |
| 95 | + if compile_model: |
| 96 | + model = torch.compile(model, fullgraph=True, dynamic=True) |
| 97 | + |
| 98 | + merged = BCEWithPEsigmaLoss(regression_weight=0.005, coupling_weight=0.005) |
| 99 | + cons = ConsistencyNLLLoss(tc_weight=1.0, mc_weight=1.0) |
| 100 | + opt = optim.Adam(model.parameters(), lr=2e-4, weight_decay=1e-6, fused=True) |
| 101 | + sched = CosineAnnealingWarmRestarts(opt, T_0=5, T_mult=2, eta_min=1e-6) |
| 102 | + scaler = torch.amp.GradScaler(cfg.device, enabled=cfg.autocast) |
| 103 | + |
| 104 | + masker = NonAstrophysicalMasker( |
| 105 | + delta_f=signal_sampler.df, tc_bounds=bounds["tc"], |
| 106 | + analysis_length_s=data_cfg.sample_length_in_s, seed=150914, |
| 107 | + ) |
| 108 | + return dict( |
| 109 | + cfg=cfg, data_cfg=data_cfg, signal=signal_sampler, noise=noise_sampler, |
| 110 | + bounds=bounds, processor=processor, model=model, merged=merged, |
| 111 | + cons=cons, opt=opt, sched=sched, scaler=scaler, masker=masker, |
| 112 | + ) |
| 113 | + |
| 114 | + |
| 115 | +def _make_trainer(b, n_iters): |
| 116 | + from sage.factory import SageConsistencyTraining |
| 117 | + |
| 118 | + return SageConsistencyTraining( |
| 119 | + b["signal"], b["noise"], b["processor"], b["model"], b["merged"], |
| 120 | + b["cons"], b["opt"], b["sched"], b["scaler"], |
| 121 | + num_iterations=n_iters, num_epochs=1, |
| 122 | + consistency_weight=0.1, masker=b["masker"], |
| 123 | + ) |
| 124 | + |
| 125 | + |
| 126 | +def _probe(b): |
| 127 | + """One step, stage by stage: sampler -> masker -> assemble -> compiled fwd.""" |
| 128 | + cfg = b["cfg"] |
| 129 | + D = len(cfg.detectors) |
| 130 | + S = int(cfg.batch_size * cfg.class_balance) |
| 131 | + num_pe = len(cfg.do_point_estimate) |
| 132 | + mw = num_pe + 1 |
| 133 | + with torch.no_grad(): |
| 134 | + _stage("2. SHAPE + CLASS-COMPOSITION PROBE") |
| 135 | + sig, sig_t = b["signal"]() |
| 136 | + extra = sig.shape[0] - S |
| 137 | + print(f" signal sampler: S={S} coherent + extra={extra} pool " |
| 138 | + f"(= round(p * (B-S)))") |
| 139 | + _shp("signal_data", sig) |
| 140 | + _shp("signal_targets", sig_t) |
| 141 | + # coherent injections: per-det mchirp identical across detectors |
| 142 | + coh_mc = sig_t[:S, mw + D: mw + 2 * D] |
| 143 | + assert torch.allclose(coh_mc[:, 0], coh_mc[:, 1], atol=1e-5), \ |
| 144 | + "coherent per-det mchirp should match across detectors" |
| 145 | + print(" coherent per-det mchirp matches across detectors OK") |
| 146 | + |
| 147 | + if extra > 0: |
| 148 | + pool_tc = sig_t[S:, mw: mw + D] |
| 149 | + pool_mc = sig_t[S:, mw + D: mw + 2 * D] |
| 150 | + na_d, na_tc, na_mc, na_mask = b["masker"](sig[S:], pool_tc, pool_mc) |
| 151 | + _shp("na_data", na_d) |
| 152 | + _shp("na_mask", na_mask) |
| 153 | + both = int((na_mask.sum(1) == D).sum()) |
| 154 | + one = int((na_mask.sum(1) == 1).sum()) |
| 155 | + print(f" non-astro split: signal+signal'={both} (mask[1,1]), " |
| 156 | + f"signal+noise={one} (mask[1,0]) of {extra}") |
| 157 | + assert both + one == extra |
| 158 | + |
| 159 | + nd, nt = b["noise"]() |
| 160 | + _shp("noise_data", nd) |
| 161 | + net = _one_assembled_forward(b, sig, sig_t, nd, nt, S, D, num_pe, mw) |
| 162 | + print(f" class balance: class1={net['c1']} (== S), " |
| 163 | + f"non-astro={net['na']}, pure-noise={net['noise']} " |
| 164 | + f"(class0 total={net['na'] + net['noise']})") |
| 165 | + assert net["c1"] == S, "class-1 count must equal the signal budget S" |
| 166 | + assert net["c1"] == net["na"] + net["noise"], "class balance broken" |
| 167 | + print(" compiled forward + both losses finite " |
| 168 | + f"merged={net['merged']:.3f} cons={net['cons']:.3f}") |
| 169 | + |
| 170 | + |
| 171 | +def _one_assembled_forward(b, sig, sig_t, nd, nt, S, D, num_pe, mw): |
| 172 | + """Mirror SageConsistencyTraining batch assembly for one step and run the |
| 173 | + compiled model + both losses; return composition counts + loss values.""" |
| 174 | + from contextlib import nullcontext |
| 175 | + from sage.core.pipeline import GWBatch, Grid, ProcessingState |
| 176 | + |
| 177 | + cfg = b["cfg"] |
| 178 | + device = cfg.device |
| 179 | + fw = mw + 2 * D |
| 180 | + tc0, mc0 = mw, mw + D |
| 181 | + extra = sig.shape[0] - S |
| 182 | + coh_data, coh_tgt = sig[:S], sig_t[:S] |
| 183 | + |
| 184 | + na_n = 0 |
| 185 | + if extra > 0: |
| 186 | + na_d, na_tc, na_mc, na_mask = b["masker"]( |
| 187 | + sig[S:], sig_t[S:, tc0:mc0], sig_t[S:, mc0:mc0 + D]) |
| 188 | + na_n = na_d.shape[0] |
| 189 | + na_tgt = torch.zeros(na_n, fw, device=device, dtype=sig_t.dtype) |
| 190 | + na_tgt[:, tc0:mc0] = na_tc |
| 191 | + na_tgt[:, mc0:mc0 + D] = na_mc |
| 192 | + |
| 193 | + B = cfg.batch_size |
| 194 | + perm = torch.randperm(B, device=device) |
| 195 | + coh_slots, na_slots = perm[:S], perm[S:S + na_n] |
| 196 | + inj = torch.zeros_like(nd) |
| 197 | + targets = torch.zeros(B, fw, device=device, dtype=sig_t.dtype) |
| 198 | + mask = torch.zeros(B, D, device=device, dtype=sig_t.dtype) |
| 199 | + targets[:, num_pe:num_pe + 1] = nt |
| 200 | + inj[coh_slots] = coh_data |
| 201 | + targets[coh_slots] = coh_tgt |
| 202 | + mask[coh_slots] = 1.0 |
| 203 | + if na_n: |
| 204 | + inj[na_slots] = na_d |
| 205 | + targets[na_slots] = na_tgt |
| 206 | + mask[na_slots] = na_mask |
| 207 | + x = nd + inj |
| 208 | + |
| 209 | + batch = GWBatch(x, state=getattr(b["signal"], "output_state", |
| 210 | + ProcessingState(Grid.FD_UNIFORM))) |
| 211 | + net_input = b["processor"](batch).to_network_input() |
| 212 | + with (torch.autocast(device_type="cuda", dtype=torch.float16) |
| 213 | + if cfg.autocast else nullcontext()): |
| 214 | + out = b["model"](net_input) |
| 215 | + _shp("ConsistencyOutput", out) |
| 216 | + merged = b["merged"]((out.ranking_stat, out.point_estimates), targets[:, :mw]) |
| 217 | + cons = b["cons"](out.mu_tc, out.log_sigma_tc, out.mu_mc, out.log_sigma_mc, |
| 218 | + targets[:, tc0:mc0], targets[:, mc0:mc0 + D], mask) |
| 219 | + c1 = int((targets[:, num_pe] == 1).sum()) |
| 220 | + return dict(c1=c1, na=na_n, noise=B - S - na_n, |
| 221 | + merged=float(merged[0]), cons=float(cons[0])) |
| 222 | + |
| 223 | + |
| 224 | +def _long_run(b): |
| 225 | + _stage(f"3. LONG COMPILED RUN — {N_LONG_ITERS} iters (all options on)") |
| 226 | + torch.cuda.reset_peak_memory_stats() |
| 227 | + trainer = _make_trainer(b, N_LONG_ITERS) |
| 228 | + t = time.time() |
| 229 | + trainer(nepoch=0) |
| 230 | + dt = time.time() - t |
| 231 | + comps = trainer.loss_components[0] |
| 232 | + peak = torch.cuda.max_memory_allocated() / 1e9 |
| 233 | + print(f" {N_LONG_ITERS} iters in {dt:.1f}s = {N_LONG_ITERS/dt:.2f} it/s " |
| 234 | + f"({b['cfg'].batch_size*N_LONG_ITERS/dt:.0f} samples/s) incl. compile") |
| 235 | + print(f" peak GPU memory = {peak:.1f} GB") |
| 236 | + print(f" loss [total, merged, cons] = {comps.tolist()}") |
| 237 | + assert torch.isfinite(comps).all(), f"non-finite loss: {comps}" |
| 238 | + |
| 239 | + |
| 240 | +def _sweep(): |
| 241 | + _stage("4. p_non_astrophysical SWEEP {0.0, 0.5} (eager), dropout=0.05") |
| 242 | + for p in (0.0, 0.5): |
| 243 | + b = _build(p_non_astro=p, dropout=0.05, compile_model=False) |
| 244 | + extra = b["signal"].signal_batch_size - int( |
| 245 | + b["cfg"].batch_size * b["cfg"].class_balance) |
| 246 | + trainer = _make_trainer(b, N_SWEEP_ITERS) |
| 247 | + trainer(nepoch=0) |
| 248 | + comps = trainer.loss_components[0] |
| 249 | + print(f" p={p:<4} extra={extra:<3} loss={comps.tolist()}") |
| 250 | + assert torch.isfinite(comps).all(), f"non-finite loss at p={p}: {comps}" |
| 251 | + |
| 252 | + |
| 253 | +def _mc_dropout(): |
| 254 | + _stage("5. MC-DROPOUT inference on the consistency model") |
| 255 | + from sage.architecture.network import enable_mc_dropout |
| 256 | + |
| 257 | + b = _build(p_non_astro=0.0, dropout=0.05, compile_model=False) |
| 258 | + model = b["model"] |
| 259 | + # a real preprocessed batch to feed the model |
| 260 | + nd, _ = b["noise"]() |
| 261 | + from sage.core.pipeline import GWBatch, Grid, ProcessingState |
| 262 | + net_input = b["processor"]( |
| 263 | + GWBatch(nd, state=getattr(b["signal"], "output_state", |
| 264 | + ProcessingState(Grid.FD_UNIFORM))) |
| 265 | + ).to_network_input() |
| 266 | + |
| 267 | + model.eval() |
| 268 | + enable_mc_dropout(model) # dropout back ON under eval |
| 269 | + with torch.no_grad(): |
| 270 | + a = model(net_input).ranking_stat |
| 271 | + c = model(net_input).ranking_stat |
| 272 | + spread = float((a - c).abs().mean()) |
| 273 | + print(f" two MC passes differ: mean|Δ ranking_stat| = {spread:.3e}") |
| 274 | + assert spread > 0.0, "MC-dropout produced identical passes (dropout inactive)" |
| 275 | + |
| 276 | + |
| 277 | +def test_consistency_stack_stress(): |
| 278 | + if not _READY: |
| 279 | + print(f"SKIP: CUDA={_HAS_CUDA} noise={_HAS_NOISE} fiducial={_HAS_FID}") |
| 280 | + return |
| 281 | + prev = os.getcwd() |
| 282 | + _enter_run_dir() |
| 283 | + try: |
| 284 | + _stage("1. BUILD all-options graph + COMPILED model " |
| 285 | + "(groupnorm, dropout=0.05, p=0.5, recolour, compile)") |
| 286 | + b = _build(p_non_astro=0.5, dropout=0.05, compile_model=True) |
| 287 | + print(f" params: {sum(p.numel() for p in b['model'].parameters()):,}") |
| 288 | + _probe(b) |
| 289 | + _long_run(b) |
| 290 | + _sweep() |
| 291 | + _mc_dropout() |
| 292 | + _stage("CONSISTENCY STACK STRESS TEST PASSED") |
| 293 | + finally: |
| 294 | + os.chdir(prev) |
| 295 | + if str(RUN_DIR) in sys.path: |
| 296 | + sys.path.remove(str(RUN_DIR)) |
| 297 | + |
| 298 | + |
| 299 | +if __name__ == "__main__": |
| 300 | + test_consistency_stack_stress() |
| 301 | + print("\n>>> CONSISTENCY STACK STRESS TEST COMPLETE <<<") |
0 commit comments