Skip to content

feat(bayesian): compare NUTS vs DEMetropolisZ for MCMC calibration - #58

Merged
gmermoud merged 7 commits into
mainfrom
feat/nuts-vs-demetropolisz-comparison
Aug 18, 2026
Merged

feat(bayesian): compare NUTS vs DEMetropolisZ for MCMC calibration#58
gmermoud merged 7 commits into
mainfrom
feat/nuts-vs-demetropolisz-comparison

Conversation

@gmermoud

@gmermoud gmermoud commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #52 (MAP+Laplace uncertainty). Adds NUTS as an alternative to PyMC's default DEMetropolisZ sampler, switches the MAP-vs-MCMC comparison job to use it, and reports the resulting comparison in RESULTS.md.

  • Add NUTS as an alternative MCMC step method (pymc_param_est.run_pymc_inference(..., step_method="nuts")), alongside the existing DEMetropolisZ — see Results below for why this replaced DEz as the comparison's MCMC arm.
  • Switch the comparison job's default to --step-method nuts, add --skip-map so a site with an existing MAP+Laplace run doesn't redo it.
  • Run NUTS on CPU: benchmarked as clearly faster than GPU for this workload (a 21-param site: ~28min on CPU vs. still unfinished after 50+min on GPU) — NUTS only parallelizes across chains (4x here), so a GPU's batch-throughput design doesn't pay off the way it does for MAP's 2000-way vmapped restart search.
  • Fix a prior-parity bug: run_pymc_analysis built its priors without the species-derived Tmax/MaxAge bound widening that run_map_analysis already 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 same bound_overrides.
  • Fix run_pymc_inference silently overwriting its checkpoint_every argument with max(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-every on run_comparison_site.py.
  • Add 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 under assets/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):

  • Log-likelihood: identical by construction — MAP doesn't build its own PyTensor graph, it calls the same pymc_model/Run3PGLogLikeOp/build_loglikelihood_fn that MCMC uses. Both pop DIAGNOSTIC_ONLY_ERROR_NAMES from priors before 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 logged Skipping observations with no matching sigma prior: [...].
  • Prior bounds: identical for every parameter on all 3 ICP sites in the Results below (wSx1000, thinPower, all err_* 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: [...]). See RESULTS.md for 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:

Site r_hat range ess_bulk range (of 15,000 draws)
04.1605 1.18 – 3.53 3 – 12
14.0003 1.27 – 2.70 4 – 8
14.0012 1.10 – 2.88 3 – 23

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:

Site r_hat range ess_bulk range (of 4,000 draws)
04.1605 1.000 – 1.000 2,126 – 5,659
14.0003 1.000 – 1.010 1,813 – 6,201
14.0012 1.000 – 1.010 1,617 – 4,869

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 DEMetropolisZ was sized to a matched wall-clock budget (RESULTS.md §3) rather than a fixed draw count:

Method Wall-clock Draws r_hat range ess_bulk (min)
DEMetropolisZ 89.3 min 200,000 (ceiling) 1.25 – 3.10 3
NUTS 1d 14h04m 4,000 (4 × 1,000) 1.00 – 1.11 25

DEMetropolisZ reached 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 parameter fracBB0), 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 parameter k), consistent with per-iteration cost and warmup difficulty scaling faster than linearly with dimension; see RESULTS.md's Open runs section for discussion.

Test plan

  • uv run pytest tests/test_pymc_inference.py
  • uv run ruff check / uv run ruff format / uvx ty check on all touched files
  • Ran the full MAP+Laplace vs. NUTS comparison on 3 ICP sites end-to-end on the Dance SLURM cluster (see Results above)
  • Ran Solling (57 params) NUTS to completion on the Dance cluster; see RESULTS.md's Open runs section for final convergence diagnostics
  • Ran Solling's matched-wall-clock-budget DEMetropolisZ rerun (job trunx-demz-matched on disco) 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; see RESULTS.md §3

@gmermoud gmermoud mentioned this pull request Aug 17, 2026
3 tasks
@gmermoud
gmermoud requested a review from gmgivi August 17, 2026 06:58
@gmermoud

Copy link
Copy Markdown
Contributor Author

@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.

@gmermoud

Copy link
Copy Markdown
Contributor Author

@gmgivi aa you can see here, we no longer fit DBH directly, but only the biomasses derived from it.

@gmgivi

gmgivi commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@gmgivi aa you can see here, we no longer fit DBH directly, but only the biomasses derived from it.

Yes, thank you @gmermoud . I will review it, and let you know.

@gmgivi

gmgivi commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@gmermoud

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:

  • MAP using 2000 restarts would take ~30 mins for 21 params site, whereas ~ 1 hour on 57 param site (solling site). There are two reasons why its taking 1 hour in solling data, first the number of parameters are comparatively large (57 params), and other is solling data is also simulated for more years (30 years).

  • Run MAP on solling data simulation with 23 params, and report the results.

  • NUTS converges faster compared to DEMetropolitsz, demonstrating the value of differentiability

Method Data # params # monthly time step Time
DEMetropolitsZ solling data 23 360 17 hours
NUTS solling data 23 360 3.5 hours

@gmermoud
gmermoud force-pushed the feat/map-laplace-uncertainty branch from a702fee to ba15c69 Compare August 18, 2026 12:42
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.
@gmermoud
gmermoud force-pushed the feat/nuts-vs-demetropolisz-comparison branch from f05982c to 610357c Compare August 18, 2026 12:47
@gmermoud
gmermoud changed the base branch from feat/map-laplace-uncertainty to main August 18, 2026 12:47
@gmermoud
gmermoud merged commit 51adf55 into main Aug 18, 2026
6 checks passed
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.

2 participants