feat(bayesian): compare NUTS vs DEMetropolisZ for MCMC calibration - #58
Conversation
|
@gmgivi @CedricTravelletti this demonstrates the advantages of differentiability by running NUTS vs DEzMetropolis side by side. Furthermore, we also run a massive GPU-based multi-init MAP run. We shall include this in the paper. |
|
@gmgivi aa you can see here, we no longer fit DBH directly, but only the biomasses derived from it. |
|
I went through the entire PR and the results. In short, the results are correct. But the PR description was slightly misleading. Here are few conclusions:
|
a702fee to
ba15c69
Compare
DEMetropolisZ (derivative-free) barely mixes on the real 3PG posterior (observed ESS of 3-12 out of 5000 draws on an ICP plot), while the model's log-likelihood already exposes a JAX gradient via Run3PGLogLikeOp.grad (the same one MAP's L-BFGS and the vmap search use). Adds step_method="nuts" to run_pymc_inference/run_pymc_analysis, running pm.NUTS on the identical Uniform-prior model MAP already uses (as opposed to the separate NumPyro/HMC pipeline in parameter_estimation.py, which silently uses a different, peaked prior) so MAP and MCMC stay directly comparable. Verified on the synthetic stand: r_hat ~1.00-1.01 and ESS 111-311/500 draws, vs. DEMetropolisZ's near-total lack of mixing on real data.
--step-method now defaults to nuts (DEMetropolisZ showed near-zero ESS on real 3PG posteriors, see prior commit). --skip-map lets a site whose MAP+Laplace already finished reuse it instead of redoing the expensive vmap search just to redo the MCMC arm with a different sampler.
Benchmarked NUTS as clearly faster on CPU than GPU for this workload (a 21-param site: ~28min on CPU vs. still unfinished after 50+min on GPU) — NUTS only parallelizes ~4x across chains, so GPU's batch throughput design doesn't help the way it does for MAP's 2000-way vmapped restart search. Drops --gres/GPU and forces JAX onto CPU. Also switches from a single array job to one job per site with an explicit --nodelist, since chacha and disco have independent filesystems and each site's map/ only exists on the node it was computed on — the array's implicit node assignment couldn't guarantee that pairing.
run_pymc_analysis loaded priors without the species-derived bound_overrides that run_map_analysis already applied, so MAP and MCMC could silently fit different Tmax/MaxAge bounds on the same site (confirmed divergent for Solling). Also documents the wSx1000/thinPower identifiability finding from the MAP-vs-NUTS comparison as a TODO, with next-step options.
run_pymc_inference silently overwrote its checkpoint_every argument with max(500, num_samples // 10) on every call, so callers could never request finer-grained checkpoints. This left the 12h solling NUTS job (57 params) with zero checkpoints: the first chunk needed the full tuning phase plus 500 draws across 4 chains to finish before anything was saved. Expose --checkpoint-every on run_comparison_site.py and give solling a much smaller value in the sbatch job so a long CPU run banks progress incrementally instead of all-or-nothing.
Add RESULTS.md summarizing GPU-vmapped MAP search wall-clock, DEMetropolisZ vs NUTS convergence at default and matched wall-clock budgets, and the completed Solling (57-parameter) NUTS run, with supporting plots.
Job 16564 completed: DEMetropolisZ reached its 200,000-draw ceiling in 1h29m (well under its 38h10m budget) but still failed to converge (R-hat 1.25-3.10, min ESS 3), matching the non-convergence already seen on the 3 ICP sites at a larger 57-parameter scale.
f05982c to
610357c
Compare
Summary
Stacked on #52 (MAP+Laplace uncertainty). Adds
NUTSas an alternative to PyMC's defaultDEMetropolisZsampler, switches the MAP-vs-MCMC comparison job to use it, and reports the resulting comparison inRESULTS.md.NUTSas an alternative MCMC step method (pymc_param_est.run_pymc_inference(..., step_method="nuts")), alongside the existingDEMetropolisZ— see Results below for why this replaced DEz as the comparison's MCMC arm.--step-method nuts, add--skip-mapso a site with an existing MAP+Laplace run doesn't redo it.run_pymc_analysisbuilt its priors without the species-derivedTmax/MaxAgebound widening thatrun_map_analysisalready applied (load_files.literature_bound_overrides), so MAP and MCMC could silently fit against different bounds on the same site (confirmed divergent for Solling; a no-op on the 3 ICP sites below, where the file's own bound already matched literature). Now both paths pass the samebound_overrides.run_pymc_inferencesilently overwriting itscheckpoint_everyargument withmax(500, num_samples // 10)on every call, so callers could never request finer-grained checkpoints — this had left the Solling job (57 params) with zero checkpoints over a 12h run. Expose--checkpoint-everyonrun_comparison_site.py.RESULTS.md, a consolidated write-up of every comparison run (GPU-vmapped MAP search timing, DEMetropolisZ vs. NUTS at default and matched wall-clock budgets, and Solling's 57-parameter NUTS and matched-budget DEMetropolisZ runs), with supporting plots underassets/results/.Verifying MAP and NUTS actually share priors and likelihood
Before trusting the comparison below, I checked whether MAP and NUTS are really scoring the same objective. They are, with one bug (now fixed, see Summary):
pymc_model/Run3PGLogLikeOp/build_loglikelihood_fnthat MCMC uses. Both popDIAGNOSTIC_ONLY_ERROR_NAMESfrompriorsbefore model construction, so DBH/BA/Height are excluded from the scored likelihood in both paths, leaving only WS/WF/WR — confirmed both by code inspection and by each run's loggedSkipping observations with no matching sigma prior: [...].wSx1000,thinPower, allerr_*sigmas, and — after the fix above —Tmax). The one place they could previously diverge (Tmax/MaxAge's literature-widened bounds) happened to be a no-op for these 3 sites but was real for Solling; now fixed for future runs of any site.Results: MAP+Laplace vs. full MCMC, 3 sites
Ran the comparison on 3 ICP plots (04.1605, 14.0003, 14.0012). Both methods fit only WS/WF/WR (biomass) — DBH/BA/Height are simulated for reference but never scored, confirmed in each run's logs (
Skipping observations with no matching sigma prior: [...]). SeeRESULTS.mdfor the full write-up, including the GPU-vmapped MAP search timing, a matched-wall-clock-budget rerun of the DEMetropolisZ comparison below (does increasing its draw count by 27-40x change the outcome? no), and the Solling (57-parameter) NUTS run.DEMetropolisZ (the prior default sampler) does not converge on this problem. With 3 chains × 5000 draws:
r_hat should be ≈1.0 and ess_bulk a healthy fraction of total draws; this is a near-total mixing failure (effectively 3-23 independent samples out of 15,000). NUTS, run on the same model/priors/likelihood, converges cleanly with 4 chains × 1000 draws:
Results: Solling, 57 parameters
Same comparison at a larger scale — Solling has 57 calibrated parameters vs. 21 on the ICP sites above. NUTS ran first here, so
DEMetropolisZwas sized to a matched wall-clock budget (RESULTS.md§3) rather than a fixed draw count:DEMetropolisZreached its 200,000-draw ceiling — 40x its default-budget draw count — in a fraction of its 38h10m walltime cap, and still didn't converge (worst parameterfracBB0), matching the pattern on the smaller ICP sites. NUTS converged, but not as cleanly as at 21 parameters (max r_hat 1.11 vs. ≈1.00, worst parameterk), consistent with per-iteration cost and warmup difficulty scaling faster than linearly with dimension; seeRESULTS.md's Open runs section for discussion.Test plan
uv run pytest tests/test_pymc_inference.pyuv run ruff check/uv run ruff format/uvx ty checkon all touched filesRESULTS.md's Open runs section for final convergence diagnosticstrunx-demz-matchedondisco) to completion — reached its 200,000-draw ceiling in 1h29m (well under its 38h10m budget) with R-hat 1.25–3.10 and min ESS(bulk) 3, confirming the same non-convergence pattern seen on the 3 ICP sites, at a larger 57-parameter scale; seeRESULTS.md§3