Skip to content

Numpyro Converter refactor#142

Open
kylejcaron wants to merge 29 commits intoarviz-devs:mainfrom
kylejcaron:numpyro-converter-with-adapter
Open

Numpyro Converter refactor#142
kylejcaron wants to merge 29 commits intoarviz-devs:mainfrom
kylejcaron:numpyro-converter-with-adapter

Conversation

@kylejcaron
Copy link
Contributor

@kylejcaron kylejcaron commented Jan 31, 2026

Addresses: #106

Background

Refactors Numpyro Converters to use an Inference Object Adapter - this should be more extensible long term and makes it easier to support new numpyro inference methods (such as Nested Sampling)

Changes

  • Breaking: SVIWrapper was renamed to SVIAdapter
  • Breaking: from_numpyro backwards compatibility is broken in some cases, such as when a posterior isn't provided
  • Added NumPyroInferenceAdapter abstract base class to standardize NumPyro inference objects
  • All adapters now follow a consistent interface: sample_dims, sample_shape, get_samples(), get_extra_fields()
  • Added MCMCAdapter for MCMC inference with consistent interface
  • NumPyroConverter works uniformly with all adapter types
  • Changed log_likelihood parameter default from None to False in all from_numpyro* functions for clarity
  • Implemented lazy import caching for JAX and NumPyro external converters
  • Updated the numpyro conversion guide

Open Questions (with resolution notes)

1) Proposal: Use NumPyroInferenceAdapter Pattern vs. Separate NumPyroConverter for Each Inference Method

One design question is whether we should use the NumPyroInferenceAdapter pattern or instead create a separate NumPyroConverter subclass for each inference method.

While having both an Adapter class and a Converter class might seem awkward, the Adapter pattern offers several advantages:

  1. Standardization: Attributes and methods are unified across different NumPyro inference objects, which reduces complex control flow downstream and simplifies tests.
  2. Future-proofing: NumPyro inference objects may evolve independently; using an adapter likely reduces the need for long-term changes if NumPyro introduces new standards or patterns.
  3. Simplicity of implementation: The Adapter pattern originally required fewer code changes compared to the separate converter class approach as explored in this draft PR (atleast before I added additional test coverage, NestedSampler support, and more docstring coverage). I think this overall approach remains more maintainable.

My Opinion: I preferred this adapter pattern to the abstract base converter approach in my other draft pr

Resolution Note: We went with the adapter pattern


2) Proposal: Remove from_nested_sampler() (experimental, contrib module in numpyro) in favor of a generic from_numpyro_adapter()?

Currently, from_nested_sampler() would support numpyro's NestedSampler, which lives in the contrib module and is marked as experimental. Given its experimental status, it may not be worth adding explicit support in ArviZ.

Instead, I'm wondering if we should adopt a generic from_numpyro_adapter() pattern:

  • Users can implement their own adapter by subclassing NumPyroInferenceAdapter (e.g., NestedSamplerAdapter).
  • This adapter can then be used with the generic from_numpyro_adapter() function.

Advantages of this approach:

  1. Future-proof: Any new inference method in NumPyro can be supported without changes to ArviZ core.
  2. Minimal maintenance: No need to track experimental contrib modules or add special-case code.

My Opinion: I think I should probably remove the nested sampler implementation since its experiment and in numpyro's contrib module. The from_numpyro_adapter() method gives users the flexibility to support it on their end pretty easily

Resolution Note: We removed the nested sampler


3) Question: Should we fold from_numpyro_adapter into from_numpyro()? Do we need to maintain backwards compatibility?

Problem: When a posterior isn't provided, we still need sample_dims to reshape input prior/posterior predictive data (e.g., for MCMC chains). Adding sample_dims as an argument to from_numpyro() would break backwards compatibility. To avoid this, the current implementation uses separate from_numpyro*() functions, which can infer sample_dims automatically for each inference type.

Alternative (breaks backwards compatibility, simplifies code):

  • Use from_numpyro() function to support all inference objects with an optional sample_dims argument.
    • posterior can be MCMC (default) or any NumPyroInferenceAdapter.
    • If posterior is None, sample_dims must be provided, otherwise it fails.
    • This removes the need for from_numpyro_adapter() entirely.
    • Would keep from_numpyro_svi for ease of use

Compromise for backwards compatibility:

  • Assume sample_dims = ["chain", "draw"] if not provided, but issue a warning.
  • This reduces duplication while preserving existing behavior for most users.
  • Keep from_numpyro_svi for each of use

My Opinion: I'm unsure which approach is best here, open to feedback!

Resolution Note: We removed from_numpyro_adapter in favor of from_numpyro working on either MCMC or the adapter classes

@codecov-commenter
Copy link

codecov-commenter commented Jan 31, 2026

Codecov Report

❌ Patch coverage is 3.19149% with 91 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.90%. Comparing base (ca9a28f) to head (7a4b072).

Files with missing lines Patch % Lines
src/arviz_base/io_numpyro.py 0.00% 91 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (ca9a28f) and HEAD (7a4b072). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (ca9a28f) HEAD (7a4b072)
5 2
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #142       +/-   ##
===========================================
- Coverage   72.30%   46.90%   -25.41%     
===========================================
  Files          19       19               
  Lines        1914     1936       +22     
===========================================
- Hits         1384      908      -476     
- Misses        530     1028      +498     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kylejcaron
Copy link
Contributor Author

@OriolAbril I have some big open questions in the PR description if you have time to take a look. Its almost ready to go once I get those questions answered

Copy link
Member

@OriolAbril OriolAbril left a comment

Choose a reason for hiding this comment

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

  1. Proposal: Use NumPyroInferenceAdapter Pattern vs. Separate NumPyroConverter for Each Inference Method

The adapter sounds good. It is also a somewhat similar pattern to what we use to interface with computational and plotting backends

2 & 3: nested_sampler and adapter or not

I think it makes sense to remove it, especially if it is easyish to get it to work in another way. Moreover, from the looks of it you'll be doing a large chunk of the maintenance on that so your opinion also gets extra weight.

We do not need to maintain backwards compatibility so having a single from_numpyro that can take MCMC or the custom adapter class sounds good, simplified code sounds even better (and a big reason for the refactor to begin with).

I think the main reason for having both things would be visibility but the conversion guide at the top level of the sidebar I think will be enough if it shows the different patterns including use of custom adapter.

@read-the-docs-community
Copy link

read-the-docs-community bot commented Feb 6, 2026

@kylejcaron kylejcaron marked this pull request as ready for review February 6, 2026 16:35
@kylejcaron kylejcaron marked this pull request as draft February 6, 2026 17:22
@kylejcaron
Copy link
Contributor Author

kylejcaron commented Feb 6, 2026

  1. Proposal: Use NumPyroInferenceAdapter Pattern vs. Separate NumPyroConverter for Each Inference Method

The adapter sounds good. It is also a somewhat similar pattern to what we use to interface with computational and plotting backends

2 & 3: nested_sampler and adapter or not

I think it makes sense to remove it, especially if it is easyish to get it to work in another way. Moreover, from the looks of it you'll be doing a large chunk of the maintenance on that so your opinion also gets extra weight.

We do not need to maintain backwards compatibility so having a single from_numpyro that can take MCMC or the custom adapter class sounds good, simplified code sounds even better (and a big reason for the refactor to begin with).

I think the main reason for having both things would be visibility but the conversion guide at the top level of the sidebar I think will be enough if it shows the different patterns including use of custom adapter.

Alright made these changes and tests are passing!

simplified code sounds even better

I'd say this PR is more "extensible to future changes" as opposed to "simplified".

I think the one thing that really adds code complexity is supporting 14 different input arguments, where many different combinations can be provided/withheld.

If we really want simplicity in the codebase, I wonder if the numpyro converter should be focused more on converting posteriors and the rest of the inputs could be provided by a user via idata.extend(prior=az.from_dict(...), ...) (io_emcee chooses not to handle other inference groups for example) or some other pattern that prevents everything from being provided in the same signature - its a tradeoff between simplicity in the codebase vs. simplicity for the user. edit: this also loses the ability to auto-infer dims from numpyro models, which is a huge convenience.

Curious if you have any thoughts or think I should take another pass at thinking of ways to simplify?

Constant data used for out-of-sample predictions.
log_likelihood : bool, default False
Whether to compute and include log likelihood in the output.
index_origin : int, optional
Copy link
Contributor Author

@kylejcaron kylejcaron Feb 6, 2026

Choose a reason for hiding this comment

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

could potentially get rid of index_origin if we want rcParams to handle it. it currently defaults to rcParams when not provided. Emcee and Pystan converters dont use these, but cmdstanpy does.

I'm leaning towards leaving it to introduce less changes - Thoughts?

# For MCMC from numpyro, we need to reshape the sample shape
# based on the number of chains provided
if self.nchains is not None:
ndraws = aelem.shape[0] // self.nchains
Copy link
Contributor Author

@kylejcaron kylejcaron Feb 6, 2026

Choose a reason for hiding this comment

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

this feels dangerous (its the previous logic we used, I just moved it out of init)

its for cases where a posterior isnt provided and numpyro has a flattened prior/predictions/posterior_predictive dictionary

I'm going to try and rethink this

import lazy_loader as _lazy


if TYPE_CHECKING:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this seemed to be the pattern necessary to get lazy_loader working when there is both lazy loading and regular module loading (I had trouble finding other patterns).

Most packages choose to go full lazy loading, but I wanted to keep the scoping of the PR to just numpyro, so I chose this partial lazy loading pattern

@kylejcaron kylejcaron marked this pull request as ready for review February 12, 2026 20:37
@kylejcaron
Copy link
Contributor Author

@OriolAbril ready for review! Apologies for the large notebook diff, I updated the conversion guide and stripped the output so that future changes wont have large diffs

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.

3 participants