Summary
DiscreteFactoredLogLikelihoodViaArrayVectorNoLoop offsets its time integral by the
batch maximum instead of the per-extrinsic-sample maximum:
# RIFT/likelihood/factored_likelihood.py
lnLmax = xpy.max(lnL_t) # lnL_t is (npts_extrinsic, npts_time) -- NO axis
L_t = xpy.exp(lnL_t - lnLmax, out=lnL_t)
L = simps(L_t, dx=deltaT, axis=-1)
lnL = lnLmax + xpy.log(L, out=L)
Every row is shifted by the batch peak, so any extrinsic sample whose own peak lnL
sits more than ~745 nats below the loudest sample in the batch has exp() underflow to
0 across the whole time axis, L = 0, and log(L) = -inf. The likelihood returns
-inf for a sample at which it is perfectly finite.
Since a typical prior draw has lnL ~ 0 and the peak has lnL ~ rho^2/2, the defect
switches on once max lnL exceeds the float64 underflow budget (~745 nats), i.e. around
rho ~ 40, and then applies to the bulk of the prior.
This is not device-dependent. The path is selected by opts.gpu, and --force-xpy
keeps opts.gpu true even when cupy is absent, so --gpu --force-xpy on a machine with
no CUDA reproduces it exactly. cupy is not implicated. It is also not the CPU/GPU
Simpson divergence (#204), which is bounded to [ln(2/3), ln(4/3)] nats and cannot
produce -inf.
Present at tag 0.0.17.4 (line 2033), on rift_O4d (2055), and at head.
The one line, measured
Real S250114ax H1/L1 strain, SEOBNRv5PHM lmax=4, srate 4096, rho ~ 82,
--sampler-method AV --n-max 4e6 --n-eff 100, seeds 3001/3002/3003, one container
(rift_o4d_cc90-120_cuda128_20260717.sif), one RIFT tree (bd5c6fa), one host
(ldas-pcdev11), single-CPU. Backend proved in-process in every arm
(mcsamplerGPU.xpy_default is cupy asserted; no cupy / Override --gpu counted).
Finite likelihood values in the first 10 000 draws:
| arm |
backend |
finite / 10000 |
no --gpu |
numpy |
9929 |
--gpu --force-xpy, cupy loaded, device 0 |
cupy |
6 |
--gpu --force-xpy, cupy not loaded |
numpy |
11 |
Same tree, same seed. The flag, not the device.
Full budget (4e6), paired, only that one line differing between arms:
| arm |
chi-square ESS |
Pareto k-hat |
reported lnL |
--gpu, as shipped |
1.0 / 1.0 / 1.0 |
21.8 / 16.2 / 23.6 |
3365.57 / 3346.74 / 3381.48 |
--gpu, per-row offset |
146.6 / 152.9 / 102.5 |
0.44 / 0.34 / 0.51 |
3370.95 / 3371.32 / 3370.83 |
CPU reference (no --gpu) |
102.6 / 104.0 / 123.0 |
0.51 / 0.64 / 0.47 |
3371.48 / 3370.71 / 3371.11 |
The shipped --gpu arm scatters over 34.7 nats across three seeds while reporting
sigma = 1.03 — understating its own error by more than an order of magnitude (k-hat
16-24 says as much). With the per-row offset it agrees with the CPU reference to 0.07
nats, inside the 0.26-0.38 nat replicate spread.
Production blast radius
The O4b campaign test_o4b_eventlist_6 is 141 runs through one container
(Reviewed-GWTC5-RIFT-20250919.sif) with an identical flag set
(--gpu --force-xpy --force-gpu-only --sampler-method AV --n-eff 10 --n-max 4e6 --time-marginalization, distance sampled). Reading each run's own
sqrt(var)/res column out of all.net (that column is 1/sqrt(chi-square ESS) in
this code):
|
median chi-square ESS |
fraction of points that exhausted --n-max |
| S250114ax / rift-v5PHM-calmarg |
1.73 |
98.2 % |
| S250114ax / rift-NRSur7dq4 |
1.83 |
100 % |
| the other 138 runs |
77 (q05 59, q95 90) |
0.31 % (q95 2.9 %) |
Exactly two runs in the campaign have max lnL > 745, and they are these two.
Nothing in the campaign sits between max lnL 666 and 2905, so the campaign brackets
the onset rather than resolving it — but it contains no counterexample in either
direction.
--force-gpu-only is sys.exit(35) when cupy fails to load, so every row of the
delivered S250114ax all.net was produced with cupy on a real device: this is not the
silent-numpy-fallback failure mode.
For scale: the delivered S250114ax intrinsic lnL surface spans 23 nats across the
top 10 % of its 5327 points — the surface CIP fits — against the per-point scatter
measured above. (That scatter was measured at srate 4096 with --internal-use-lnL, not
at production's srate 1024, so treat it as an order of magnitude, not a transfer.)
Grep the class, do not fix one line
The correct idiom xpy.max(..., axis=-1, keepdims=True) is already used at five sites
(factored_likelihood.py:3127, factored_likelihood_freqresponse.py:457,
factored_likelihood_with_rotation.py:948, time_marginalization_quadrature.py:764,
study_stencil_lnL_sensitivity.py:396). Three sites lack it:
| site |
expression |
status |
factored_likelihood.py:2953 |
lnLmax = xpy.max(lnL_t) |
confirmed live (this issue) |
factored_likelihood.py:2181 |
lnLmax = np.max(lnL_t_accum), lnL_t_accum is (npts_extrinsic, npts) |
same shape, same exposure — not exercised here |
factored_likelihood.py:3132 |
m_c = xpy.max(lnL_t_c) feeding a global running_max for the calmarg sum |
in-loop calmarg only — not exercised here |
The two unexercised ones are the same construction, not the same diagnosis: each needs
its own reproduction before anything is claimed about it.
What I am not proposing
Per standing policy I am not submitting a patch. Changing this moves production
numbers on the --gpu path for every loud event, and a change of that class needs
end-to-end known-answer runs, not a diff. The one-line per-row offset above is a
diagnostic intervention run in a private snapshot to confirm the mechanism, and it
is reported as evidence, not as a proposal.
Two consequences worth deciding on separately:
- Whether any delivered
--gpu result above rho ~ 40 needs re-running.
- Whether
mcsamplerAV should refuse to report a result at k-hat > 10 / ESS < 2 rather
than returning a finite lnL with a sigma that understates its error 17-fold. The
[AV COLLAPSE] diagnostic already prints; it does not gate.
Evidence, harnesses and logs: ~/av_gpu_ownership on ldas-pcdev11/CIT shared home
(run_arm.sh carries the in-process backend assertion; Code/ and Code_fix/ are the
pinned snapshots). Records:
RIFT_roboto_paper:development/OPEN_gpu_adaptive_volume_collapse.md.
Summary
DiscreteFactoredLogLikelihoodViaArrayVectorNoLoopoffsets its time integral by thebatch maximum instead of the per-extrinsic-sample maximum:
Every row is shifted by the batch peak, so any extrinsic sample whose own peak
lnLsits more than ~745 nats below the loudest sample in the batch has
exp()underflow to0 across the whole time axis,
L = 0, andlog(L) = -inf. The likelihood returns-inffor a sample at which it is perfectly finite.Since a typical prior draw has
lnL ~ 0and the peak haslnL ~ rho^2/2, the defectswitches on once
max lnLexceeds the float64 underflow budget (~745 nats), i.e. aroundrho ~ 40, and then applies to the bulk of the prior.This is not device-dependent. The path is selected by
opts.gpu, and--force-xpykeeps
opts.gputrue even when cupy is absent, so--gpu --force-xpyon a machine withno CUDA reproduces it exactly. cupy is not implicated. It is also not the CPU/GPU
Simpson divergence (#204), which is bounded to
[ln(2/3), ln(4/3)]nats and cannotproduce
-inf.Present at tag
0.0.17.4(line 2033), onrift_O4d(2055), and at head.The one line, measured
Real S250114ax H1/L1 strain, SEOBNRv5PHM
lmax=4, srate 4096,rho ~ 82,--sampler-method AV --n-max 4e6 --n-eff 100, seeds 3001/3002/3003, one container(
rift_o4d_cc90-120_cuda128_20260717.sif), one RIFT tree (bd5c6fa), one host(
ldas-pcdev11), single-CPU. Backend proved in-process in every arm(
mcsamplerGPU.xpy_default is cupyasserted;no cupy/Override --gpucounted).Finite likelihood values in the first 10 000 draws:
--gpu--gpu --force-xpy, cupy loaded, device 0--gpu --force-xpy, cupy not loadedSame tree, same seed. The flag, not the device.
Full budget (4e6), paired, only that one line differing between arms:
lnL--gpu, as shipped--gpu, per-row offset--gpu)The shipped
--gpuarm scatters over 34.7 nats across three seeds while reportingsigma = 1.03— understating its own error by more than an order of magnitude (k-hat16-24 says as much). With the per-row offset it agrees with the CPU reference to 0.07
nats, inside the 0.26-0.38 nat replicate spread.
Production blast radius
The O4b campaign
test_o4b_eventlist_6is 141 runs through one container(
Reviewed-GWTC5-RIFT-20250919.sif) with an identical flag set(
--gpu --force-xpy --force-gpu-only --sampler-method AV --n-eff 10 --n-max 4e6 --time-marginalization, distance sampled). Reading each run's ownsqrt(var)/rescolumn out ofall.net(that column is1/sqrt(chi-square ESS)inthis code):
--n-maxExactly two runs in the campaign have
max lnL > 745, and they are these two.Nothing in the campaign sits between
max lnL666 and 2905, so the campaign bracketsthe onset rather than resolving it — but it contains no counterexample in either
direction.
--force-gpu-onlyissys.exit(35)when cupy fails to load, so every row of thedelivered S250114ax
all.netwas produced with cupy on a real device: this is not thesilent-numpy-fallback failure mode.
For scale: the delivered S250114ax intrinsic
lnLsurface spans 23 nats across thetop 10 % of its 5327 points — the surface CIP fits — against the per-point scatter
measured above. (That scatter was measured at srate 4096 with
--internal-use-lnL, notat production's srate 1024, so treat it as an order of magnitude, not a transfer.)
Grep the class, do not fix one line
The correct idiom
xpy.max(..., axis=-1, keepdims=True)is already used at five sites(
factored_likelihood.py:3127,factored_likelihood_freqresponse.py:457,factored_likelihood_with_rotation.py:948,time_marginalization_quadrature.py:764,study_stencil_lnL_sensitivity.py:396). Three sites lack it:factored_likelihood.py:2953lnLmax = xpy.max(lnL_t)factored_likelihood.py:2181lnLmax = np.max(lnL_t_accum),lnL_t_accumis(npts_extrinsic, npts)factored_likelihood.py:3132m_c = xpy.max(lnL_t_c)feeding a globalrunning_maxfor the calmarg sumThe two unexercised ones are the same construction, not the same diagnosis: each needs
its own reproduction before anything is claimed about it.
What I am not proposing
Per standing policy I am not submitting a patch. Changing this moves production
numbers on the
--gpupath for every loud event, and a change of that class needsend-to-end known-answer runs, not a diff. The one-line per-row offset above is a
diagnostic intervention run in a private snapshot to confirm the mechanism, and it
is reported as evidence, not as a proposal.
Two consequences worth deciding on separately:
--gpuresult aboverho ~ 40needs re-running.mcsamplerAVshould refuse to report a result at k-hat > 10 / ESS < 2 ratherthan returning a finite
lnLwith asigmathat understates its error 17-fold. The[AV COLLAPSE]diagnostic already prints; it does not gate.Evidence, harnesses and logs:
~/av_gpu_ownershiponldas-pcdev11/CIT shared home(
run_arm.shcarries the in-process backend assertion;Code/andCode_fix/are thepinned snapshots). Records:
RIFT_roboto_paper:development/OPEN_gpu_adaptive_volume_collapse.md.