Skip to content

Commit 857432e

Browse files
committed
Add end-to-end consistency stack stress test (all options + compile)
1 parent 5017aa8 commit 857432e

1 file changed

Lines changed: 301 additions & 0 deletions

File tree

tests/test_consistency_stress.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
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

Comments
 (0)