Skip to content

fix: correct biased pymc HMC sampling (target_accept) - #1908

Merged
janfb merged 7 commits into
mainfrom
fix/pymc-c2st-flaky
Jul 27, 2026
Merged

fix: correct biased pymc HMC sampling (target_accept)#1908
janfb merged 7 commits into
mainfrom
fix/pymc-c2st-flaky

Conversation

@janfb

@janfb janfb commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1894 because CD is failing again. Once the lockfile fix let the pymc
tests run again in CD, test_c2st_pymc_sampler_on_Gaussian failed on the hmc
parametrizations with c2st around 0.62.

However, it's not flaky: pymc's HamiltonianMC defaults to target_accept=0.65,
which mixes poorly on this peaked Gaussian and biases the samples, regardless of
seed, warmup, or number of draws. NUTS works fine.

To fix this:

  • expose seed and target_accept on PyMCSampler
  • default hmc_pymc to target_accept=0.9 (_DEFAULT_HMC_TARGET_ACCEPT in
    pymc_wrapper.py) to get accurate HMC posteriors (c2st ~0.51 vs ~0.62);
    nuts_pymc keeps PyMC's backend default and slice_pymc is unchanged
  • make the new default overridable via MCMCPosterior(target_accept=...),
    MCMCPosterior.sample(target_accept=...) and
    MCMCPosteriorParameters.target_accept, validated to be in (0, 1)
  • seed the c2st test for reproducible sampling; it now relies on the new 0.9
    default rather than passing its own value

target_accept is PyMC-only: pymc.Slice does not take it, so it is forwarded
only for the hmc/nuts steps, and the Pyro samplers (hmc_pyro, nuts_pyro)
ignore it entirely.

Since this silently changes a default, there is a CHANGELOG entry under
v0.26.1 → Bug Fixes.

Note: the c2st test is marked slow, I will run the CD on this branch via
workflow dispatch.

janfb added 2 commits July 1, 2026 10:59
The hmc parametrizations of test_c2st_pymc_sampler_on_Gaussian were
failing in CD (c2st ~0.62). Root cause is PyMC's HamiltonianMC default
target_accept=0.65, which mixes poorly on this peaked target regardless
of seed, warmup, or number of draws (NUTS is unaffected).

Expose seed and target_accept on PyMCSampler and set target_accept=0.95
(c2st ~0.51-0.54 across seeds and chain counts) plus a fixed seed in the
test for reproducibility.
MCMCPosterior._pymc_mcmc left PyMC's HamiltonianMC at its default
target_accept=0.65, which produces biased hmc_pymc posteriors
(c2st ~0.62 on a Gaussian). Default it to 0.9 for the HMC step, leaving
nuts_pymc and slice_pymc unchanged. hmc_pymc now samples accurately
(c2st ~0.51).
@janfb
janfb requested a review from dgedon July 1, 2026 10:00
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.94%. Comparing base (5f227ac) to head (f40c42d).
⚠️ Report is 20 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1908      +/-   ##
==========================================
+ Coverage   87.81%   87.94%   +0.13%     
==========================================
  Files         143      143              
  Lines       13387    13657     +270     
==========================================
+ Hits        11756    12011     +255     
- Misses       1631     1646      +15     
Flag Coverage Δ
fast 81.36% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sbi/inference/posteriors/mcmc_posterior.py 84.17% <100.00%> (+0.34%) ⬆️
sbi/inference/posteriors/posterior_parameters.py 83.33% <100.00%> (+0.24%) ⬆️
sbi/samplers/mcmc/pymc_wrapper.py 81.57% <100.00%> (+2.16%) ⬆️
sbi/utils/typechecks.py 77.77% <100.00%> (+1.58%) ⬆️

... and 4 files with indirect coverage changes

Comment thread sbi/inference/posteriors/mcmc_posterior.py Outdated
Comment thread sbi/samplers/mcmc/pymc_wrapper.py Outdated
@janfb

janfb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

thanks for the review @dgedon

when addressing your comments I noticed there was an inconsistency in the default values (0.9 vs 0.95 in the tests), that's fixed now. And I noticed it would be better to have the new target_accept as an argument in the PyMCWrapper, and to not have it for Pyro (unrelated).

@janfb
janfb requested a review from dgedon July 21, 2026 17:34
@janfb janfb mentioned this pull request Jul 22, 2026
9 tasks

@dgedon dgedon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe update the original description of the PR, so it reflects the latest changes, e.g. the 0.9 default.

I approve for now since all remaining comments are smaller details and not functional really. In that way you can merge easier @janfb

Comment thread sbi/inference/posteriors/mcmc_posterior.py Outdated
Comment thread sbi/inference/posteriors/mcmc_posterior.py Outdated
Comment thread sbi/samplers/mcmc/pymc_wrapper.py Outdated
Comment thread sbi/inference/posteriors/mcmc_posterior.py Outdated
janfb added 3 commits July 27, 2026 15:55
Addresses review feedback from @dgedon on #1908:

- add `validate_target_accept()` to `sbi/utils/typechecks.py` and use it at all
  four validation sites, replacing the repeated `validate_float_range` block.
  The helper lives in `typechecks.py` because it has no sbi-domain imports, so
  `MCMCPosterior` and `MCMCPosteriorParameters` can use it without pulling in
  the optional `pymc` dependency.
- `MCMCPosterior.sample()` now validates only the caller's argument; the
  fallback to `self.target_accept` is already validated in `__init__`.
- name the HMC default `_DEFAULT_HMC_TARGET_ACCEPT` in `pymc_wrapper.py`
  instead of hardcoding `0.9`, with the rationale on the constant.
- state explicitly in all docstrings that `target_accept` is used only by
  `hmc_pymc` and `nuts_pymc`, and ignored by `slice_pymc`, the Pyro samplers
  and the numpy slice samplers.
- revert unrelated whitespace and variable churn in the Pyro/PyMC branches.
Validate the type and (0, 1) range directly instead of delegating to
validate_float_range. Error types and messages are unchanged.
Reverts 57f9716. `validate_float_range` predates this PR (added in #1668) and
stays for `validation_fraction` in the trainer contracts, so `target_accept`
should share it rather than reimplement the type check and range messages.
@janfb
janfb merged commit 93cf8b5 into main Jul 27, 2026
20 checks passed
@janfb
janfb deleted the fix/pymc-c2st-flaky branch July 27, 2026 14:19
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