Three ways to break a density
A density reported -log q of 4.489e9, bit-identical across independent runs, from the very first step. Fixing it took three separate changes, and what found all three was a counter rather than a bound.
Applies to the normalising-flow density head, as built for run 2. The three causes are properties of that head's construction and recur wherever it is reused.
What the guard was for
The first response was defensive: bound the loss to the envelope the flow's
own construction implies. The coupling scale is tanh-bounded to
±2.5 and to_unconstrained clamps before atanh, so
there is a computable ceiling on -log q. Anything past it is a
pathological batch, not a hard example.
The important half was not the bound. It was this:
npe_rejected: int = 0
npe_seen_max: float = 0.0
A guard that rejects and reports nothing is a mask. A guard that rejects and reports a rate is a diagnostic. The first diagnosis — "a single degenerate batch" — was wrong, and the counter is what disproved it:
step 1 npe 0 rejected 1
step 400 npe 0 rejected 400
400 rejections in 400 steps is not a rare event. Without the counter, the posterior loss would have been silently zeroed for an entire run and the model would have shipped with an amortized posterior that never trained, under a green log.
Cause one: the conditioning, not the input
An instrumented dump on the first real rejection:
REJ#0 all=True kept=0/64 per=[1.5e7, 1.3e8, 3.3e7, ...]
y|max=19.34 c|max=13.18 u|max=3.962 u_fin=True th|max=6.144
u is bounded and finite, so the flow's input was never
the problem. The conditioning was: |c|max ≈ 13–18 in training
against ~0.4 for the same network on unmasked corpus data. A coupling's
scale is tanh-bounded; its translation is linear
in its input far from the origin. A conditioning vector of 13 drives
z far enough that -½z² reaches −4.5e9.
A LayerNorm on the conditioning, applied in
log_prob and sample alike so density and samples
cannot disagree, bounds -log q at 17.04 across a 200× input
range.
Cause two: a point mass
The loss became finite and stayed absurd — around 4000 for an eight-dimensional
flow. The configuration said nuisance_dim: 2, which accurately
describes an intent to estimate EEG gain and sensor noise. The trainer passed
torch.zeros.
So two of the flow's eight target dimensions were a constant at the origin. No normalizing flow can represent a point mass with finite density, and the attempt dominated everything else.
| configuration | final −log q | rejected |
|---|---|---|
nuisance_dim = 0 | 5.3034 | 0 / 120 |
nuisance_dim = 2 | does not converge | continuous |
Nothing in the config was wrong. The defect was that the intent had never been implemented and the placeholder standing in for it was silently pathological.
Cause three: one learning rate for two different objects
step 80 npe 8.449 rejected 0
step 100 npe 110.1
step 140 npe 1229
Stable through the warmup, climbing at exactly the point the one-cycle schedule reached its maximum — while the forecast head kept improving throughout. A step size that is merely large for a residual block is destabilising for a density, for the same reason as cause one: the translation is linear in its input. The posterior now trains in its own parameter group at 0.1× the stage rate.
A fourth claim, withdrawn
The obvious next move was to bound the translation the way the scale already is. That was committed as "the root cause, not a delay" — before the comparison finished. It finished:
UNBOUNDED final 7.3007 worst 9.2648 rejected 0/600
T_BOUND=12.0 final 7.3006 worst 9.2643 rejected 0/600
The unbounded arm did not diverge, so the test cannot confirm the bound fixes anything. It establishes only that the bound costs nothing measurable, so it stays as free insurance against a mechanism that is real in principle and unproven here.
The honest summary: three fixes moved the onset from step 1 to step 800, the fourth is free, and the training run remains the only test that has ever reproduced the failure.
What generalises
Each cause was invisible to the others' tests. The conditioning bug passed every isolation test because the summary network is scale-robust on unmasked data. The nuisance bug needed a converging flow before it became the dominant term. The learning-rate bug could not be reached until the first two were fixed.
Three sequential failures, each masking the next — and a rate, rather than a boolean, is what made them separable at all.
Follow-up: run 4 repaired the rate, and overshot
The learning rate above was the last of the three, and SC-WBD-004 is the run that carried the fix into production. A four-seed sweep chose the rate before launch, and on that sweep the coupling-gain posterior recovered R² 0.674–0.766 against roughly zero in run 3.
The repair worked. Run 3's posterior ignored its conditioning entirely: every posterior standard deviation sat within 3% of the prior's and its mean moved by 9% of a prior standard deviation as the data changed. Run 4's coupling-gain posterior is eight times narrower than the prior and its mean now moves by 1.10 prior standard deviations with the data. The flow reads its conditioning, which three runs could not achieve.
And it is not usable, because it is confidently wrong. The standardised residual has a spread of 59.25 where a calibrated posterior sits near 1.0. Simulation-based calibration returns a Kolmogorov–Smirnov p of 1.0 × 10−147; expected-coverage error is 0.203 against run 3's 0.021. It narrowed by eight times and earned about a quarter of that. Four of the six parameters still explain no variance.
So the failure mode inverted rather than resolving. Run 3's posterior was uninformative and honest — a distribution that returns the prior is perfectly calibrated by construction, which is why the calibration block certified it. Run 4's is partly informative and overconfident. Neither supports inference, and they are not two points on a path from worse to better; they are two ways to be unusable. The claim publishes as unsupported, where run 3's identical claim published as partial.
One number is worth stating because it constrains what to do next: the one-stage sweep predicted 0.674–0.766 and the full six-stage curriculum returned 0.284, under half. Why is unmeasured. The natural reading is that the two measured-return stages, which follow the founding stage at reduced and then zero posterior weight, give part of it back — but that is a hypothesis, and the experiment that would settle it (scoring the founding stage's own checkpoint through the same calibration block) has not been run. The lesson the page above already carries applies to its own sequel: a fix validated on one stage is not thereby validated on six.