GMM: flexible (data-driven) component allocation for the extrinsic integrator - #27
Conversation
1d30d3e to
7a3ac63
Compare
Root cause (PR #27): a slow-starting VARAHA/AV member gets weight ~0.01 after chunk 1 (< freeze_wt 0.05) and is frozen from chunk 1, never contracts; the portfolio then rides the stalling GMM. Fix: GRACE (portfolio_grace_iters=25, no freezing early), REVIVE (portfolio_revive_period=8, periodic update of frozen members), freeze_wt overridable via setup; plus a NaN-weight guard (map non-finite log_integrand/log_weights to -inf in place -> zero weight, fixed length, no downstream NaN-mask size mismatch). Verified: cold portfolio(AV,GMM) on S250114ax runs with AV not frozen and no crash. Unblocks PR #27's default-vs-adaptive-GMM isolation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_match_components enumerated all k! permutations to align old->new mixture components in update(). Fine for k<=6 but explodes (12!~5e8, 16!~2e13), making any many-component proposal (e.g. a chain of Gaussians wrapping a curved degeneracy arc) impossible to refit. The objective is additive over matched pairs, so linear_sum_assignment returns the SAME optimum in O(k^3); verified identical to the permutation optimum on random models (k=2..6), and k=16 now matches in ~6ms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the hard-coded per-group GMM component counts (sky=4, dist-incl=2,
phase=wide) -- which target quadrupole/large-sky-ring binaries and do not adapt
to the actual posterior -- with a data-driven allocation, and fixes two bugs
that made a warm GMM proposal a no-op.
gaussian_mixture_model.py:
* fit_gmm_adaptive(): choose a group's component count from the (importance-
weighted) cloud by BIC over a k ladder, then prune dead components. More
components only where the cloud is genuinely non-Gaussian (e.g. a curved
degeneracy arc); k=1 for a single blob. Self-limiting via the ln(N_eff)
penalty, so it avoids the instability of a fixed over-allocated k.
* gmm.prune_components(): drop <floor-weight components (renormalize); cuts the
O(k) per-component mvnun cost in score() and removes spurious sharp comps.
* gmm.num_free_params(), _mixture_log_density_normalized(): BIC helpers.
* add_defensive_component(): optional broad box-covering component for tail
coverage (Hesterberg defensive IS).
MonteCarloEnsemble.integrator:
* new gmm_adaptive={group:k_max} (+ gmm_defensive_frac, gmm_inflate). A group
listed here picks its k by BIC at INITIALIZATION, then hands off to the
proven-stable merge adaptation (model.update()). Per-chunk refit-fresh was
tried and rejected: it makes the proposal wander (n_eff peaks then collapses)
because each fit sees a different elite cloud; the incremental merge smooths
that out.
mcsamplerEnsemble:
* WARM-START SURVIVAL: integrate() rebuilt a fresh integrator from the passed
gmm_dict (values None) and never referenced self.integrator, so the models
fitted by bootstrap_from_samples were SILENTLY DISCARDED -- a 'warm' run
started cold (measured: began at n_eff=1.0). Now transfer any fitted model
whose dim-group key matches; a mismatch falls back to cold (never biases).
* thread gmm_adaptive/defensive/inflate through integrate() and setup().
driver: --internal-gmm-adaptive-components [--internal-gmm-max-components N]
[--internal-gmm-defensive-frac F] [--internal-gmm-inflate X].
test/integrators/test_gmm_adaptive.py: BIC picks k=1 for one blob, grows for
separated modes, respects weights; prune renormalizes; Hungarian matching ==
permutation optimum. 5/5 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* driver: --internal-gmm-defensive-frac now defaults to 0 (OFF). On the wide extrinsic prior the broad box-covering defensive component draws physically extreme points where the likelihood is NaN, and it did not improve n_eff on the SNR~82 benchmark; keep it opt-in. * fix a stale constructor comment (BIC picks k at init, then the stable merge path adapts -- not a per-chunk refit). * add DESIGN_flexible_gmm.md: the design, the S250114ax + synthetic n_eff-vs-N measurements, and the honest conclusion -- pure GMM importance sampling stalls at n_eff~1-7 on this high-SNR degenerate event regardless of allocation, so the flexible GMM belongs in the portfolio (with AV) or on milder problems; the allocation itself is validated as robust and unbiased on the synthetic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Curved-arc + correlated-pair + tight-blob 6-D target (S250114ax analogue) with n_eff-vs-N crossings for correlate-all fixed k vs BIC-adaptive k. Reproduces the DESIGN_flexible_gmm.md synthetic numbers on CPU/GPU in seconds (no ILE data). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review: keep the stress-tested hard-coding as the default/fallback, not a replacement, and honor the flag in the portfolio. * SAFETY FLOOR: fit_gmm_adaptive gains k_min; BIC now chooses k in [k_min, k_max] and prune_components(min_keep=k_min) never drops below it. integrator._train and MCSampler.update_sampling_prior pass k_min = the group's stress-tested hard-coded n_comp, so opting into adaptive can only ADD components, never fewer than the validated layout. Protects broad multi-modal posteriors. Still fully OPT-IN: flag off => byte-identical hard-coded path. * gmm_adaptive may now be a SCALAR/bool (apply to every adapting group), not only a per-group dict -- needed for the portfolio, whose GMM member grouping is not built in the driver. * PORTFOLIO: --internal-gmm-adaptive-components injects gmm_adaptive (scalar cap) + defensive/inflate into the shared setup kwargs; the GMM member honors adaptive in update_sampling_prior (the path the portfolio drives). Default OFF leaves the stress-tested portfolio config unchanged. (A working AV,GMM portfolio is still blocked by a PRE-EXISTING unrelated issue: AV has no draw_simplified.) * test_gmm_adaptive: add k_min-floor test (6/6 pass); DESIGN doc updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h-update blocker) Correct the portfolio usage/wiring in DESIGN_flexible_gmm.md: - invoke members with the flag REPEATED (--sampler-portfolio AV --sampler-portfolio GMM), not comma-joined; when GMM is a member the driver runs the full GMM config section, so the per-group adaptive allocation applies to the pairing's adapting groups (sky, dist-incl), floored, phase excluded. - d44fe48 fixed draw_simplified (members instantiate + warm-start 2/2); the AV+GMM benchmark is still blocked by a separate pre-existing AV-member bug (empty selection -> xpy.max on empty allloglkl in update_sampling_prior_selfish), reproducible cold with the default GMM member and no adaptive flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AV freeze-out) After edf775c the portfolio runs past startup. Ran the requested default-vs- adaptive comparison (warm+cold, GPU, 4M): both stall (warm peak 3.55/1.72, cold 1.92/1.00) because the balance heuristic freezes the AV member (weight ~0.0099 < portfolio_freeze_wt 0.05) on chunk 1, so the portfolio rides the stalling GMM member instead of AV. Independent of the flexible allocation (same with the default GMM member); portfolio-dynamics territory (PR #26). Cold additionally crashes in the portfolio combine on nan samples from the frozen AV member. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-coded) After f2d51de (grace+revive) the portfolio runs to completion. Warm, GPU, 4M: default (hard-coded GMM member): peak n_eff 3.01 adaptive GMM member (BIC cap 8): peak n_eff 18.2 <- flexible allocation ~6x So the hands-free adaptive GMM member outperforms the hand-tuned layout inside the portfolio. Separate finding for the AV/portfolio side (PR #26): AV's balance- heuristic weight stays pinned at ~1/101 the whole run whether frozen or not, so AV never contributes; freeze-exemption does not help (it is worse: 4.16 vs 18.2, as active-but-tiny-weight AV injects nan/unproductive draws). Root question is the weight floor, not the freezing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a3ac63 to
ccfb087
Compare
|
Coordination from the portfolio freeze-policy work (#28, stacked on #26). Two things there directly unblock the default-vs-adaptive-GMM-component comparison, which was confounded by the AV freeze-out:
So the default-vs-adaptive-component isolation is now a re-run away on top of #28. Harness in |
Shape-recovery validation record (pre-merge gate + opt-in feature probe)1. Differential shape-recovery gate vs PR #26 (safety)Full
2. Targeted probe of the opt-in feature (
|
| run | checkout | n_comp | adaptive | n_eff | n_ESS | lnZ bias | max per-dim JS (floor) | max abs mean pull | max width dev | final k |
|---|---|---|---|---|---|---|---|---|---|---|
| d2 nc1 OFF | PR26 | 1 | off | 3064.6 | 26920 | +0.0036 | 4.9e-5 (3.4e-4) | 0.0031 | 0.34% | 1 |
| d2 nc1 OFF | PR27 | 1 | off | 3064.6 | 26920 | +0.0036 | 4.9e-5 (3.4e-4) | 0.0031 | 0.34% | 1 |
| d2 nc1 ON | PR27 | 1 | k_max=8 | 3045.1 | 26555 | -0.0028 | 6.2e-5 (3.4e-4) | 0.0034 | 0.44% | 2 (BIC) |
| d2 nc3 ctrl | PR27 | 3 | off | 3066.5 | 27239 | +0.0042 | 4.3e-5 (3.5e-4) | 0.0010 | 0.26% | 3 |
| d4 nc1 OFF | PR26 | 1 | off | 529.7 | 6149 | -0.0079 | 3.9e-4 (1.2e-3) | 0.0169 | 0.69% | 1 |
| d4 nc1 OFF | PR27 | 1 | off | 529.7 | 6149 | -0.0079 | 3.9e-4 (1.2e-3) | 0.0169 | 0.69% | 1 |
| d4 nc1 ON | PR27 | 1 | k_max=8 | 527.0 | 5960 | -0.0131 | 4.5e-4 (1.3e-3) | 0.0198 | 0.98% | 3 (BIC) |
| d4 nc3 ctrl | PR27 | 3 | off | 541.3 | 5944 | +0.0137 | 8.0e-4 (1.3e-3) | 0.0199 | 1.14% | 3 |
Readings:
- PR26 vs PR27 with feature OFF: identical to full precision (every metric), independently reconfirming the inert default path.
- Feature ON is statistically indistinguishable from OFF and breaks nothing: d2 n_eff 3065→3045, bias +0.004→−0.003; d4 n_eff 530→527, bias −0.008→−0.013. All per-dim JS below the matched-n_ESS floor, mean pulls < 0.02σ, widths within ~1%, zero training errors, zero crashes.
- BIC recovers real structure from a wrong floor: with
k_min=1it allocated k=2 (d2) / k=3 (d4, the true count) — the safety-floor + BIC design behaves as documented. - The default seed-101 targets are forgiving of wrong
n_comp(components overlap; a single tempered Gaussian still covers the support), so the matrix above demonstrates safety and correct component allocation. The rescue itself is demonstrated by the harsh pair below.
Harsh-separation pair (the rescue scenario)
Same d4 3-component target family but well-separated narrow modes (sigma_1d=0.35, scale_x0=9.0), wrong n_comp=1, same budget:
| run | n_eff | n_ESS | lnZ bias | max per-dim JS (floor) | max abs mean pull | corr diff | final k |
|---|---|---|---|---|---|---|---|
| OFF | 18.5 | 116 | +0.087 | 1.6e-2 (5.0e-2) | 0.105 | 0.077 | 1 |
| ON (k_max=8) | 85.8 | 743 | -0.037 | 4.6e-3 (9.1e-3) | 0.047 | 0.035 | 4 (BIC) |
Feature ON rescues the wrong-n_comp collapse: 4.6x n_eff, 6.4x n_ESS, lnZ |bias| halved, mean pulls 0.105σ → 0.047σ, with BIC allocating 4 components against the 3-mode truth (3 modes + tail capacity). Zero training errors, zero crashes.
- All probes ran CPU-mode with the runtime eigvals workaround for the GPU-port import bug in
gaussian_mixture_model.py(_xpy_eigvals/_xpy_eigbound tocupy.linalgat import with no device probe; bisected separately). That bug is now fixed onrift_O4dby PR integrators: probe for a CUDA device before selecting cupy; fail loudly when GMM refit never succeeds #32 (CPU device probe), so the workaround is no longer required on current heads (it is harmless where applied); these probes predate the fix and needed it to measure anything but uniform sampling.
3. Coverage summary
The gate suite exercised the true-n_comp default path (safety: bitwise-identical to PR #26); this probe exercised the wrong-n_comp opt-in path (the feature's intended scenario). Both dimensions of the change are tested.
Suite reference: MonteCarloMarginalizeCode/Code/test/expensive_before_merging/integrators (merged into rift_O4d via PR #31).
🤖 Generated with Claude Code
Replaces the hard-coded per-group GMM component layout (sky=4, dist-incl=2,
phase=wide) with an OPT-IN, data-driven allocation, and fixes two bugs that made a
warm GMM proposal a no-op. Hard-coding remains the default/fallback.
Changes
optimum, unblocks many-component proposals (k=16: ~6 ms vs infeasible).
GMM.fit_gmm_adaptive— pick a group's component count by weighted BIC overa k-ladder + prune dead components. BIC-at-init, then the stable merge path adapts
(per-chunk refit-fresh was tried and rejected — the proposal wanders).
hard-coded count; pruning never drops below it. Opting in can only add
components (protects broad multi-modal sky posteriors). Fully opt-in:
--internal-gmm-adaptive-components(default OFF) ⇒ byte-identical hard-coded path.integrate()was silently discarding the modelsfitted by
bootstrap_from_samples; now transfers them by dim-group key.update_sampling_prior, floored at itsn_comp).--internal-gmm-adaptive-components [--internal-gmm-max-components N][--internal-gmm-defensive-frac F] [--internal-gmm-inflate X].test/integrators/test_gmm_adaptive.py(6/6) + data-free syntheticbenchmark +
RIFT/integrators/DESIGN_flexible_gmm.md.Key measured finding
Pure GMM importance sampling stalls at n_eff≈1–7 on S250114ax (SNR≈82) regardless
of allocation, warm start, defensive coverage, or inflation (confirmed by ~12 runs
ESS ≈ 1. AV reaches ≈89 there. On a moderate-SNR synthetic (where importance
sampling is viable) the flexible allocation is validated: robust, unbiased,
hands-free. → Use the flexible GMM as a portfolio member with AV, not standalone on
high-SNR degenerate events. See
DESIGN_flexible_gmm.mdfor the full n_eff-vs-Ntables.
Base / merge note
Based on
rift_O4d_adaptive_integratorbefore992486cf(--internal-reparam-dl-incl);that commit touches the driver in a different region, so this should merge cleanly.
🤖 Generated with Claude Code