Skip to content

portfolio: make the driver fail loudly instead of silently substituting a sampler (driver-side complement to #168) - #172

Open
oshaughnessy-junior wants to merge 1 commit into
oshaughn:rift_O4cfrom
oshaughnessy-junior:rift_O4c_portfolio_driver_selectable
Open

portfolio: make the driver fail loudly instead of silently substituting a sampler (driver-side complement to #168)#172
oshaughnessy-junior wants to merge 1 commit into
oshaughn:rift_O4cfrom
oshaughnessy-junior:rift_O4c_portfolio_driver_selectable

Conversation

@oshaughnessy-junior

@oshaughnessy-junior oshaughnessy-junior commented Aug 13, 2026

Copy link
Copy Markdown

Driver-side half of making --sampler-method portfolio usable on rift_O4c. One file,
+26/-5, no new dependencies, no behaviour change for any sampler other than portfolio.

Complementary to #168, not overlapping it. #168 fixes the module side
(mcsamplerPortfolio.py): the unguarded entry-point plugin load, and the fair-draw backend.
This PR fixes the driver side (integrate_likelihood_extrinsic_batchmode), which #168 does not
touch. Neither is sufficient alone — see the measurements below. I have deliberately not
duplicated #168's plugin guard here, even though I had written an identical one, so the two
branches do not conflict; 3201b000 cherry-picks onto 4ddf41de cleanly.

Four defects, one kind

Every one of them answers a request the driver cannot honour with something other than an error.

1. A dead raise, and a silent sampler substitution.

elif opts.sampler_method == "portfolio" and mcsampler_Portfolio_ok:
    if not(mcsampler_Portfolio_ok):
      raise Exception(" Portfolio integrator requested but not available")

The guard in the elif makes the very next statement unreachable, and routes an unavailable
portfolio past the whole chain to the terminal else, which prints ILE: **original sampler**
and proceeds with the plain mcsampler.MCSampler constructed before the chain. A run that asked
for the portfolio would have integrated with a different sampler and reported nothing unusual.
Dropping the flag from the test makes the existing raise reachable and correct.

2. The else diagnostic itself raises. It formats mcsamplerPortfolio.known_pipelines, but
that name is only bound when the import succeeded. So the branch that exists to explain a failed
import was the one that raised NameError: name 'mcsamplerPortfolio' is not defined — which is
what --sampler-method portfolio actually produced on any torch-free container.

3. The plugin-pipeline elif has the same unguarded dereference in its test. Gated on the
ok-flag.

4. The documented spelling of --sampler-portfolio silently built the wrong portfolio. The
option is action='append' while its help documents "comma-separated strings", and the member
loop had no else. So --sampler-portfolio AV,GMM arrived as the single member name "AV,GMM",
matched no branch, and appended whatever sampler happened to hold — the plain MCSampler from
before the chain, or on later iterations the previous member. Measured on 4ddf41de (#168 head),
rho_net 146.8:

 PORTFOLIO  ['AV,GMM']
PORTFOLIO: adding AV,GMM
  ===> FAILED ANALYSIS <====
'MCSampler' object has no attribute 'draw_simplified'

One member, wrong type, and an error that names neither the option nor the bad value. With this
PR the same invocation gives PORTFOLIO: adding AV / PORTFOLIO: adding GMM, and a bad name is
refused up front:

Exception:  --sampler-portfolio: unknown member 'BOGUS'.
            Known: AV, GMM, AC/adaptive_cartesian_gpu, ['AC', 'AV', 'GMM']

Both spellings (and a mix) are now accepted, so the help text stops lying rather than the
behaviour changing under anyone. rift_O4d carries the same defect 4; it is fixed here rather
than backported.

Note the committed demo harness has been splitting on commas in shell before invoking the
driver (extrinsic_collapse_demo/run_demo.sh:58) — a workaround for exactly this, which is why
only the append spelling had ever been exercised.

Measurements

Same host (ldas-pcdev2, A100), same container, same injection, run_snr140 (rho_net 146.8),
--sampler-portfolio AV,GMM:

tree result
4ddf41de (#168 head) 1 member "AV,GMM", dies no attribute 'draw_simplified'
4ddf41de + this PR 2 members AV, GMM, portfolio constructs, reaches the integration loop
4ddf41de + this PR, member BOGUS refused immediately, exit 1, message names the known members

Every run logs its tree and commit; all three read dirty=0.

What this does NOT fix

Reaching the integration loop is where this PR's scope ends, and the portfolio still does not
complete on rift_O4c.
With #168 and this PR both applied it now fails one layer deeper, in
the AV member's prior evaluation:

  File ".../integrators/mcsamplerPortfolio.py", line 286, in draw
    joint_p_s_here, joint_p_prior_here, rv_here = member.draw_simplified(...)
  File ".../integrators/mcsamplerAdaptiveVolume.py", line 286, in prior_prod
    p_out *= self.prior_pdf[param](x[:,indx])
TypeError: Unsupported type <class 'numpy.ndarray'>

Same class of defect as #168's fair-draw fix — allocate on one backend, operate with the other.
I chased it two sites further, and it is a chain, not a last mile:

# site
1 mcsamplerAdaptiveVolume.prior_prod:286 AV member
2 mcsamplerEnsemble.calc_pdf:202 GMM member
3 mcsamplerPortfolio.integrate_log:471 the portfolio itself

all the same TypeError, each only visible once the previous one is fixed. Standalone AV and
GMM escape 1 and 2 for different reasons (the driver rebinds mcsampler = mcsamplerAdaptiveVolume in the AV branch; self.xpy is numpy in standalone GMM), and a
portfolio breaks both assumptions at once. The O4c portfolio has evidently never run on a GPU host.

rift_O4d carries fixes for all three, and backporting the first one verbatim regresses the
production path — so I am not proposing it here.
With the O4d prior_prod fix applied,
standalone AV at rho=51.4:

tree seeds 9201-9204
4a8703f3 4/4 converged, fairdraw 8, eff_samp 8.00-8.72
+ O4d prior_prod 0/4 — all four TypeError: Unsupported type

because mcsamplerGPU.uniform_samp_psi(x, xpy=...) accepts xpy but its body is
xpy.ones(len(x))/(cupy_pi) with cupy_pi = cupy.array(np.pi) — the array honours the argument,
the constant does not. The O4c and O4d bodies of that helper are byte-identical, so the O4d fix
works upstream only in combination with the rest of O4d's backend hardening
(identity_convert* call sites: mcsamplerEnsemble 5 → 26, mcsamplerPortfolio 32 → 46,
mcsamplerAdaptiveVolume 38 → 52).

So: this PR makes the portfolio selectable and honestly diagnosed. It does not make it
runnable on rift_O4c, and that remainder is its own scoped job with its own inertness gate
on standalone AV and standalone GMM — not a follow-on commit. Full measurements and the
reproduce commands: extrinsic_collapse_demo/run/O4C_PORTFOLIO_STATUS_2026-08-13.md.

…ng a sampler

Driver-side half of making --sampler-method portfolio usable on rift_O4c.  The
module-side half -- guarding the unguarded entry-point plugin load, so one missing
optional dependency does not make mcsamplerPortfolio unimportable -- is PR oshaughn#168's
first commit (aaa8a6e) and is deliberately NOT duplicated here.  Neither half is
sufficient alone; oshaughn#168 does not touch this file.

Four defects, all in the sampler-construction chain, all of the same kind: a request
the driver cannot honour is answered with something other than an error.

1. `elif opts.sampler_method == "portfolio" and mcsampler_Portfolio_ok:` made the
   very next statement -- `if not(mcsampler_Portfolio_ok): raise` -- unreachable, and
   sent an unavailable portfolio down the chain to the terminal `else`, which prints
   " ILE: **original sampler** " and proceeds with the plain mcsampler.MCSampler
   constructed before the chain.  A run that asked for the portfolio would have
   integrated with a different sampler.  Drop the ok-flag from the test so the
   existing raise becomes reachable and does its job.

2. That terminal `else` then dereferences `mcsamplerPortfolio.known_pipelines` in its
   own diagnostic print -- but the name is only bound if the import succeeded, so the
   diagnostic for a failed import raised NameError itself.  This is what
   '--sampler-method portfolio' actually produced on a torch-free container:

       NameError: name 'mcsamplerPortfolio' is not defined

3. The plugin-pipeline branch had the same unguarded dereference in its `elif` test.
   Gate it on mcsampler_Portfolio_ok.

4. --sampler-portfolio is action='append' while its help documents a comma-separated
   list, and the member loop had no else clause.  So the documented invocation
   '--sampler-portfolio AV,GMM' arrived as the single member name "AV,GMM", matched
   no branch, and appended whatever `sampler` happened to hold -- the plain MCSampler
   from before the chain, or on later iterations the PREVIOUS member.  The portfolio
   then ran with a member nobody asked for and died later and elsewhere with a
   misleading "no attribute 'draw_simplified'".  Accept both spellings (and a mix),
   and make an unrecognized member name an error naming the known members.

   The committed demo harness already works around this by splitting on commas in
   shell before invoking the driver (extrinsic_collapse_demo/run_demo.sh:58), which
   is why the append form is the only one that had been exercised.

rift_O4d carries the same defect 4; it is fixed here rather than backported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant