ILE: make the --internal-use-lnL backends run, and return a lnZ that is right - #167
Open
oshaughnessy-junior wants to merge 3 commits into
Open
ILE: make the --internal-use-lnL backends run, and return a lnZ that is right#167oshaughnessy-junior wants to merge 3 commits into
oshaughnessy-junior wants to merge 3 commits into
Conversation
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
August 19, 2026 15:47 — with
GitHub Actions
Active
oshaughnessy-junior
had a problem deploying
to
private-review-dispatch-rift
August 19, 2026 16:00 — with
GitHub Actions
Error
oshaughnessy-junior
marked this pull request as ready for review
August 19, 2026 16:00
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
August 19, 2026 16:01 — with
GitHub Actions
Active
…is right Three defects reported from the ILE-GPU-Paper demo on an A100 (cupy 12.0.0, IGWN CVMFS python), all reproducing on an unmodified checkout, plus three more found while checking that the fixes produced NUMBERS rather than just exit code 0. Together they meant a requested .dgrid marginal-distance-grid came out on AV and GMM and silently did not on anything else. 1. mcsamplerGPU.integrate_log: HOST ARRAYS LEAKING INTO DEVICE EXPRESSIONS. `identity_convert` is cupy.asnumpy and `self.xpy.max` returns a 0-d DEVICE array, so `max(maxval, identity_convert(...))` made maxval a 0-d numpy.ndarray, which the next line mixes into a device expression: "TypeError: Unsupported type <class 'numpy.ndarray'>". That took down every --internal-use-lnL run on adaptive_cartesian_gpu -- the DEFAULT sampler. maxlnL has the identical bug one branch up, biting the verbose per-iteration report and the deltalnL cut, so fixing maxval alone left the demo failing at the same TypeError six lines later. Coerced both to python floats, which both backends accept. Invisible on CPU, where numpy.max returns a numpy SCALAR and cupy accepts those. 2. mcsampler.MCSampler had no identity_convert. Every other sampler defines the host/device converters in __init__; the driver only assigns them onto the samplers it recognizes by name, so anything landing on the "original sampler" fallback (e.g. adaptive_cartesian) reached `float(sampler.identity_convert(neff))` AFTER a completed integration and died with AttributeError, discarding the work. 3. Which exposed a silent wrong answer behind it: the post-processing asked `if not(opts.internal_use_lnL): log_res = numpy.log(res)`, reading the OPTION where it had to read the convention the sampler was actually handed. They differ for exactly one pairing -- adaptive_cartesian is in ok_lnL_methods so the option is accepted, but mcsampler.py has no use_lnL handling and returns a LINEAR integral. The logarithm was skipped: the demo reported lnZ = 1.3e29 (= e^67) with sigma = inf, and the .dgrid inherited it. Keyed off `return_lnL` instead -- the predicate the driver already derives and the integrand already uses, and whose divergence from the option the driver already documents for _rvs['integrand']. Same substitution in the replica arm and the zero-likelihood stand-in. The two agree for GMM / adaptive_cartesian_gpu / AV / portfolio, so nothing else moves. 4. mcsamplerPortfolio had no prior_pdf: it delegates the priors to its members and kept no copy, so `--sampler-method portfolio --export-marginal-distance-grid` completed the integration and then died in the .dgrid exporter on sampler.prior_pdf["distance"]. Mirrored from member 0, the designated full-support member, alongside llim/rlim. 5. The .dgrid gate itself also required --internal-use-lnL, which decides nothing this exporter reads: its weights go through ln_weights_for_posterior with the record's own convention, and log_res now comes through return_lnL. All it did was make --export-marginal-distance-grid silently do nothing unless a second, unrelated flag was set too. Dropped from the gate. The reported `--sampler-method portfolio` with no member (TypeError: 'NoneType' object is not iterable) was already fixed in-tree by a134af3; it is pinned here so it cannot come back. NOT FIXED, deliberately, and now recorded in the code: `maxval` in integrate_log is a LOG-scale accumulator initialized at 0, a linear-weight idiom, so eff_samp = sum(w)/max(w) is floored whenever the largest weight is < 1. Real GW lnL is large and positive so it does not bite in production, and the identical initializer is copied verbatim into mcsamplerPortfolio and mcsamplerNFlow -- changing one would split n_eff, and hence run lengths, across backends. Measured on ldas-pcdev12 (A100, cupy 12.0.0), demo as reported, same GPU, same seed, this branch against its own merge-base. Runs are reproducible here (3/3 identical repeats), so the identical columns are identity, not noise: config tip this branch adaptive_cartesian_gpu lnZ 58.3091 neff 3.4 IDENTICAL, + .dgrid adaptive_cartesian_gpu + lnL TypeError, no output lnZ 58.4381, .dgrid GMM lnZ 57.9598 neff 2.1 IDENTICAL, + .dgrid GMM + lnL lnZ 57.9598 + .dgrid IDENTICAL incl. .dgrid adaptive_cartesian + lnL AttributeError, no output lnZ 58.5209, .dgrid portfolio AV,GMM + lnL AttributeError (prior_pdf) lnZ 58.1610 IDENTICAL, + .dgrid AV + lnL lnZ 59.0983 + .dgrid IDENTICAL incl. .dgrid Every backend that already produced a number produces the same one. And where a backend can now export a .dgrid from BOTH record conventions, the two agree -- adaptive_cartesian_gpu peak 63.995 (linear record) vs 63.845 (lnL record), GMM 102.020 vs 102.550 -- which is the check that the exporter's linear branch is right, not merely reachable. test_ile_lnL_backend_defects.py covers each fix; every one was verified by reverting it and watching the matching test fail. The device-typing tests run twice: against a cupy stand-in that enforces the real operand rule (CPU lane), and against real cupy where a GPU is present. Wired into the CI job that already runs the sampler regression suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One finding, in the tests rather than the fix. _derive_return_lnL executes the driver's own
source from `return_lnL=False` down to `if use_gmm_args:`, which stops SHORT of a second,
unconditional `if opts.sampler_method == "AV": return_lnL=True` further down. The assertion
`_derive_return_lnL('AV', False)[0] is False` therefore passed against an incomplete model of
the derivation -- the real driver yields True for that input.
It is harmless today only because the AV branch of the sampler chain forces
opts.internal_use_lnL=True, so (AV, internal_use_lnL=False) is unreachable. But the test would
have gone on passing if that forcing were ever removed, which is precisely when AV would join
adaptive_cartesian as a place where the option and the sampler's actual convention part
company -- the whole subject of this branch.
Dropped the assertion and replaced it with test_the_av_override_cannot_disagree_with_the_option,
which pins BOTH halves of what makes AV safe: the override exists where the model expects it,
and the branch that forces the option on is still there. _derive_return_lnL now says in its
docstring that it covers the first block only.
Also re-ran the revert check over the final tree: each of the six fixes, reverted one at a
time, makes a specific test fail, and none of them makes only a source-scan test fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oshaughnessy-junior
force-pushed
the
claude/ile-lnL-backend-fixes
branch
from
August 20, 2026 09:46
61728ce to
d235f39
Compare
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
August 20, 2026 09:46 — with
GitHub Actions
Active
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
August 22, 2026 10:02 — with
GitHub Actions
Active
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Three defects reported from the ILE-GPU-Paper demo on an A100 (cupy 12.0.0, IGWN CVMFS
python), all reproducing on an unmodified checkout, plus three more found while checking
that the fixes produced numbers rather than just exit code 0. Together they meant a
requested
.dgridmarginal-distance-grid came out on AV and GMM and silently did not onanything else.
The defects
1.
mcsamplerGPU.integrate_log: host arrays leaking into device expressions.identity_convertiscupy.asnumpyandself.xpy.maxreturns a 0-d device array, somax(maxval, identity_convert(...))mademaxvala 0-dnumpy.ndarray, which the next linemixes into a device expression —
TypeError: Unsupported type <class 'numpy.ndarray'>. Thistook down every
--internal-use-lnLrun onadaptive_cartesian_gpu, the default sampler.maxlnLhas the identical bug one branch up, biting the verbose per-iteration report and thedeltalnL cut, so fixing
maxvalalone left the demo failing at the sameTypeErrorsix lineslater. Both coerced to python floats, which both backends accept. Invisible on CPU, where
numpy.maxreturns a numpy scalar and cupy accepts those.2.
mcsampler.MCSamplerhad noidentity_convert. Every other sampler defines thehost/device converters in
__init__; the driver only assigns them onto the samplers itrecognizes by name, so anything landing on the "original sampler" fallback (e.g.
adaptive_cartesian) reachedfloat(sampler.identity_convert(neff))after a completedintegration and died with
AttributeError, discarding the work.3. Which exposed a silent wrong answer behind it. The post-processing asked
if not(opts.internal_use_lnL): log_res = numpy.log(res)— reading the option where it hadto read the convention the sampler was actually handed. They differ for exactly one pairing:
adaptive_cartesianis inok_lnL_methodsso the option is accepted, butmcsampler.pyhasno
use_lnLhandling and returns a linear integral. The logarithm was skipped, and thedemo reported lnZ = 1.3e29 (= e^67) with sigma = inf, with the
.dgridinheriting it.Keyed off
return_lnLinstead — the predicate the driver already derives, the integrandalready uses, and whose divergence from the option the driver already documents for
_rvs['integrand']. Same substitution in the replica arm and the zero-likelihood stand-in.The two agree for GMM /
adaptive_cartesian_gpu/ AV / portfolio, so nothing else moves.4.
mcsamplerPortfoliohad noprior_pdf. It delegates the priors to its members and keptno copy, so
--sampler-method portfolio --export-marginal-distance-gridcompleted theintegration and then died in the exporter on
sampler.prior_pdf["distance"]. Mirrored frommember 0, the designated full-support member, alongside
llim/rlim.5. The
.dgridgate also required--internal-use-lnL, a flag that decides nothing thisexporter reads: its weights go through
ln_weights_for_posteriorwith the record's ownconvention, and
log_resnow comes throughreturn_lnL. All it did was make--export-marginal-distance-gridsilently do nothing unless a second, unrelated flag was settoo. Dropped from the gate.
The reported
--sampler-method portfoliowith no member (TypeError: 'NoneType' object is not iterable) was already fixed in-tree by a134af3 — the reporting worktree predated it. Itis pinned here so it cannot come back.
Deliberately not fixed
maxvalinintegrate_logis a LOG-scale accumulator initialized at0, a linear-weightidiom, so
eff_samp = sum(w)/max(w)is floored whenever the largest weight is < 1 (itunder-reports n_eff by 1/max·w and keeps drawing to nmax). Real GW lnL is large and positive so
it does not bite in production, and the identical initializer is copied verbatim into
mcsamplerPortfolioandmcsamplerNFlow— changing one would split n_eff, and hence runlengths, across backends mid-campaign. Recorded in the code where it is, so the next reader
meets a note rather than a landmine.
Measurements
ldas-pcdev12 (A100, cupy 12.0.0), the demo as reported, same GPU and same seed, this branch
against its own merge-base. Runs are reproducible on this host (3/3 identical repeats), so
the identical columns below are identity, not noise.
adaptive_cartesian_gpu.dgridadaptive_cartesian_gpu+ lnLTypeError, no output.dgridGMM.dgridGMM+ lnL.dgrid.dgridadaptive_cartesian+ lnLAttributeError, no output.dgridportfolio AV,GMM+ lnLAttributeError(prior_pdf).dgridAV+ lnL.dgrid.dgridEvery backend that already produced a number produces the same one. Where a backend can now
export a
.dgridfrom both record conventions, the two agree —adaptive_cartesian_gpupeak 63.995 (linear record) vs 63.845 (lnL record); GMM 102.020 vs 102.550 — which is the check
that the exporter's linear branch is right, not merely reachable.
(The demo grid was regenerated mid-session by another branch, so absolute lnZ here is ~58 where
older logs of the same demo show ~67. Both arms of every row above ran on the same grid.)
Tests
test/test_ile_lnL_backend_defects.py, 34 tests, wired into the CI job that already runs thesampler regression suites. Every fix was verified by reverting it and watching the matching
test fail — the first version of the defect-1 test passed with the bug present and was
replaced. The device-typing tests run twice: against a cupy stand-in that enforces cupy's real
operand rule (CPU lane), and against real cupy where a GPU is present. Where practical the
tests execute the driver's own source — the
_neff_valline, the portfolio flatteningcomprehension, the
return_lnLderivation, thelog_resblock and the.dgridgate arecompiled out of the file and run, rather than copied.
Also green on this branch:
test_av_empty_live_volume,test_l0_rescue_seed,test_seq_warmstart_seed,test_fairdraw_double_weighting,test_portfolio_fairdraw_backend,test_rvs_record,test_limit_cosine_samplers,test_distance_grid, and bothexpensive_before_mergingaudits (audit_rvs_fairdraw,audit_backend_contracts).Adversarial review
Run before un-drafting. One finding, and it was in the tests, not the fix:
_derive_return_lnLexecutes the driver's own
return_lnLderivation but stops short of a second, unconditionalif opts.sampler_method == "AV": return_lnL=Truefurther down, so an assertion about(AV, internal_use_lnL=False)was passing against an incomplete model. Unreachable today onlybecause the AV branch forces the option on — and the test would have gone on passing if that
forcing were removed, which is exactly when AV would join
adaptive_cartesianas a place thetwo predicates part company. Replaced with a test that pins both halves. Fixed in 61728ce.
Cleared without a finding, each checked rather than assumed:
self.prior_pdfon the portfolio is written only in__init__andadd_parameter— thepin/restore path never touches it, so a pinned pass cannot leave a delta function in the
prior the
.dgridexporter divides out.prior_pdfinadd_parameter, so widening the.dgridgatecannot
AttributeErroron any sampler that now reaches it.SamplerOutputMixin(new on the base from Give the retained set and the export resample separate names #94) defines onlysamples/set_samples— nocollision with the converters added here.
return_lnLis never bound insideanalyze_event(AST-checked), so the new reads are globalreads, not
UnboundLocalError; andanalyze_eventhas exactly one call site, well after themodule-level derivation.
zero_likereturns zeros andunit_likereturns ones, so the newif return_lnL:mapping isthe right way round.
float()on-infand onnanpreserves the previousmax()/isinfbehaviour exactly.Each of the six fixes was reverted individually against the final tree; every one makes a
specific test fail, and none of them makes only a source-scan test fail.
🤖 Generated with Claude Code