Walnutpie version 0.0.1 release

We are happy to announce the official release of Walnutpie version 0.0.1.

Walnutpie is an MCMC sampler for continuously differentiable densities coded in Python, accepting models coded in Stan, PyMC, NumPyro, JAX, and plain old Python.

Walnutpie is not an official Stan project

I thought this was worth saying up front. It may eventually migrate to Stan, but for now, we followed the Nutpie approach of building a standalone sampling package that worked with a range of packages for defining models.

pip installable

Walnutpie is on PyPI, so it’s pip installable. The documentation includes information on getting started, running models, and posterior analysis.

Stan through C++

Walnutpie runs Stan models through C++ using BridgeStan, so there is no Python dispatch overhead for Stan sampling. The basic architecture of the API is based on Adrian Seyboldt’s sampler Nutpie. We are working on doing that for other packages like NumPy and JAX to the extent that we can.

GitHub source

Development discussions and source code are managed through GitHub.

Features of Walnutpie

We are almost ready to release the paper on arXiv with all of the gory pseudocode details all of the algorithms used for Walnutpie. It will explain the following points in detail.

  1. Walnuts: The underlying Hamiltonian Monte Carlo sampler is Walnuts. Walnuts uses Nuts for choosing the number of steps per iteration. It further allows step sizes within the Hamiltonian dynamics simulation to be lowered when necessary to preserve simulation accuracy. This helps with robustness and with accuracy in multi-scale distributions (i.e., ones where the curvature as represented by the Hessian varies around the posterior). With a high tolerance threshold, Walnuts reverts to Nuts’s behavior.

  2. Mass-matrix warmup: The mass-matrix warmup strategy is an online form of Nutpie (links to: the paper and software). Nutpie minimizes Fisher divergence by estimating the inverse mass matrix as the midpoint (in the appropriate manifold) between an estimate based on the variance of the draws and the covariance of the scores (gradients of the log density). The target is better than Nuts’s variance of draws in both convergence speed and sampling efficiency. Walnutpie only supports diagonal mass matrices (Nuts supports dense matrices and Nutpie supports low-rank plus diagonal and even more general normalizing flow approaches). The approach is online in the sense that it is not blocked like warmup in Nuts or Nutpie—it updates every iteration by exponentially discounting the past to mimic Stan’s exponentially increasing history sizes. We also borrow Nutpie’s mass matrix initialization based on a regularized outer product of gradients at the initial point.

  3. Step-size warmup: The step size adaptation strategy has not changed, but the underlying stochastic gradient descent algorithm is Adam rather than dual averaging. We found Adam to be faster to converge and much more stable. Matt Hoffman included a hack in the original Nuts approach to stabilize dual averaging, but even with that it is not as stable as Adam.

  4. Concurrency and automatic stopping: The underlying sampler is multi-threaded (using C++11 threads) with shared data. On top of the multi-threading, we have layered a convergence monitor in a separate thread that communicates with the chains through lock-free, latest-only, single-producer/single-consumer (SPSC) buffers (specifically, a triple buffer). The monitor automatically stops warmup when the mass matrices and step sizes have converged within tolerance to their cross-chain averages. The monitor automatically stops sampling when a target (traditional, non-split, non-ranked) \widehat{R} threshold is satisfied for the unnormalized log density, which typically converges more slowly than any of the individual parameters. It can also be configured to run for a fixed number of warmup and/or sampling iterations. The link between the original \widehat{R} and effective sample size makes this essentially an unscaled ESS target.

  5. Ragged chain summaries: Asynchronous concurrent execution of chains with automatic stopping produces chains of different lengths. Because ArviZ does not accept ragged chain input of this kind, we have included posterior analysis tools for means, variances/standard deviations, quantiles, traditional R-hat, effective sample size, and Monte Carlo standard error that work with ragged chains.

  6. C++20: Walutpie is implemented in C++20. As a programming language type fanatic (I’ve written two books with “type” in the title!), I don’t know how I survived without C++ concepts before C++20.

  7. ctypes FFI:The foreign function interface in Python uses ctypes rather than a higher-level interface, which sidesteps the requirement of ABI compatibility of C++ binaries.

Developers

We would also like to welcome new developers who may want to get involved. There are already a stack of improvements we’d like to make, which we have enumerated on the GitHub issues.

Sources of algorithms

The Walnuts algorithm was a joint effort among Nawaf Bou-Rabee, Sifan Liu, Tore Kleppe, and Milo Marsden. Nutpie was developed by Adrian Seyboldt. Nuts, in the form used currently in Stan, was originally developed by Matt Hoffman and Andrew Gelman, then improved with multinomial sampling and mass matrix adaptation by Michael Betancourt. We haven’t yet added cross-chain adaptation as developed by Ben Bales, but the pieces are all in place to do so.

Brian Ward and I wrote all of the version 0.0.1 code with design advice and code review from Steve Bronder. Claude (the LLM) helped with code review and testing, but we didn’t use it to write the actual code (not out of principle, but because Brian and I both prefer the control of doing things manually).

Feedback

We would very much appreciate any feedback people have, including feedback on the documentation and ease of use, the source code, and performance.

We are happy to get feedback through issues on GitHub, through replies to this post, or through mail to one of the developers.

Cool, looking forward to trying it out. I’ve already run walnuts on a few models with bridgestan, but that was a bit tedious to configure and run each time, so I’m very glad to see this interface.

I think it makes sense to start this way. Eventually it would be great to get this into Stan itself so we can more easily expose it via the interfaces our users are already comfortable with. I know @WardBrian had expressed interest in this in the past. Brian, is that still something you’re interested in?

This is very cool.

We don’t support this in any of our posterior analysis packages in R either because there was never a need to. But this is good motivation to work on that. If anyone is interested in helping with this let me know!

It is! First I’d like to build the equivalent of what we have in Python in R (at least for Stan models, maybe not arbitrary R functions) and monitor what people discover in terms of bugs, difficult models, etc using the existing code base. So I would probably put the start date for such an effort at least six months out

I realized in a moment of panic after we did the release that we hadn’t tested on anything but a cherry-picked set of models for illustration purposes. So I went home last night and vibe-coded (Opus 4.8 hard) some posteriordb tests. Thankfully the AI could figure out the posteriordb interface in Python and our interface for Walnutpie just from pointers to the repositories. I couldn’t even find the API doc for posteriordb, and I tried before remembering Claude’s now better at this kind of stuff than I am.

Here’s a table of results from the posteriordb models we used to evaluate Pathfinder plus a few more. The initializations either with a unit matrix (Stan style) or with the outer product of gradients (Nutpie style). Not every pair shows up because some of the versions ran into what looks like a degeneracy for step size adaptation (they complained at the end of adaptation that the step size wasn’t positive and finite). So we have to look into that.

posterior                                       init chains  maxRhat   minESS warmup min warmup max sample min sample max  status
---------------------------------------------------------------------------------------------------------------------------------
arma-arma11                                  default     16   1.0001     7059        276        365        112        164    PASS
sblrc-blr                                    default     16   1.0502    19170       1000       1000       1000       1000    FLAG
sblrc-blr                                       unit     16   1.0472    19657       1000       1000       1000       1000    FLAG
sblri-blr                                    default     16   1.0529    19132       1000       1000       1000       1000    FLAG
sblri-blr                                       unit     16   1.0601    18519       1000       1000       1000       1000    FLAG
earnings-logearn_interaction                 default     16   1.0011    23548       1000       1000       1000       1000    PASS
earnings-logearn_height                      default     16   1.0002    10364       1000       1000        499        767    PASS
mesquite-logmesquite_logvolume               default     16   1.0000     2849        509        647        142        215    PASS
diamonds-diamonds                            default     16   1.0001     8702       1000       1000       1000       1000    PASS
diamonds-diamonds                               unit     16   1.0000    12729       1000       1000       1000       1000    PASS
bball_drive_event_0-hmm_drive_0              default     16   1.0001    16064       1000       1000       1000       1000    PASS
bball_drive_event_1-hmm_drive_1              default     16   1.0006     5448       1000       1000        290        495    PASS
hudson_lynx_hare-lotka_volterra              default     16   1.0029     6287       1000       1000       1000       1000    PASS
hudson_lynx_hare-lotka_volterra                 unit     16   1.0027     6328       1000       1000       1000       1000    PASS
low_dim_gauss_mix-low_dim_gauss_mix          default     16   1.0000     6204       1000       1000        103        135    PASS
gp_pois_regr-gp_regr                         default     16   1.0006     2208         68        105        120        158    PASS
gp_pois_regr-gp_regr                            unit     16   1.0010     1641         61         91         87        119    PASS
gp_pois_regr-gp_pois_regr                    default     16   1.0004     3450       1000       1000        639        799    PASS
dogs-dogs                                    default     16   2.6284       11        594        664        183        218    FLAG
dogs-dogs                                       unit     16   3.0295       10       1000       1000        134        179    FLAG
dogs-dogs_log                                default     16   3.5245       13        108        216         70        114    FLAG
one_comp_mm_elim_abs-one_comp_mm_elim_abs    default     16   1.0042      687        472        739        159        233    PASS
one_comp_mm_elim_abs-one_comp_mm_elim_abs       unit     16   1.0041      653         73        153        170        236    PASS
nes2000-nes                                  default     16   1.0012     1589       1000       1000        147        241    PASS
arK-arK                                      default     16   1.0001     1728        118        157        137        173    PASS
arK-arK                                         unit     16   1.0000     2424        124        158        170        246    PASS
hmm_example-hmm_example                      default     16   1.0005     2454        247        373        137        185    PASS
mcycle_gp-accel_gp                           default     16   1.0021    13517       1000       1000       1000       1000    PASS
garch-garch11                                default     16   1.0003     6250        147        244        181        224    PASS
garch-garch11                                   unit     16   1.0002     6721         70        134        190        251    PASS
eight_schools-eight_schools_noncentered      default     16   1.0248      722        122        207        144        205    FLAG
eight_schools-eight_schools_noncentered         unit     16   1.0212      704         51        104        136        204    FLAG
eight_schools-eight_schools_centered         default     16   1.0566      107       1000       1000       1000       1000    FLAG
eight_schools-eight_schools_centered            unit     16   1.0686      100       1000       1000       1000       1000    FLAG

It’s flagging (a) dogs, (b) sblrc, (c) eight schools.

(a) The dogs models are misspecified in that they have unconstrained parameters with odd interval constrained distributions (@andrewgelman assures me these were not the priors he and Jennifer Hill used in their book).

parameters {
  vector[2] beta;
}
model {
  beta[1] ~ uniform(-100, 0);
  beta[2] ~ uniform(0, 100);
}

(b) For sblrc, (Stan model blr.stan) the minESS is fine, so I’m confused as to how the R-hat values can be so high. I think “blr” is for “Bayesian linear regression” (what I’d call “linear regression”). It’s just a simple linear regression in 5 dimensions with a somewhat wide prior. I couldn’t find the data easily so have to go back and have Claude find it for me.

(c) For eight schools, the centered parmeterizations fail as expected and the non-centered ones will be fine with a tighter R-hat tolerance. As is, it’s stopping after very few iterations and getting R-hat under 1.01 for the log density, but the R-hats on some of the model parameters are higher than the log density, which is unusual.

I consider this a very promising set of results. We’re going to have to look into why adaptation and sampling aren’t converging for some of these. I really don’t know what’s up with the ESS in sblrc yet. It also feels very fast running 16 chains on my Mac Studio, but then these are all fairly small models, so there’s not much data memory contention.

Hi Bob, on the 8 schools model I’m confused why walnuts fails with the centered parameterization. Nawaf shared this view of the adaptive step size with walnuts to capture the funnel. What am I missing here?

This is really cool work! I love the BridgeStan idea and how you use Walnutpie.

For fun, I’ve added Walnuts sampling (without Rhat early stopping) to the Stanli website and made it so you can watch it sample live at the same time as NUTS / dynamic HMC in your own browser: stanli: Stan Language Interpreter, browser edition

And when it’s done sampling you can compare param by param:

I also added a BridgeStan API interface to Stanli so you can do your own experiments quickly from Python, which is more in spirit with the unix philosophy and the way you’re building Walnutpie.

import stanli
import bridgestan as bs
from walnutpie import walnuts_stan

lib = stanli.bridgestan_lib(stan_file="model.stan")
model = bs.StanModel(lib, {"N": 10, "y": [0.1] * 10})
chains = walnuts_stan(model=model, num_chains=4, seed=42)
print(chains[0]["theta"])

I think the main bad thing is that right now when you make a model it copies the entire interpreter over. The one bad thing about the API I guess, there wasn’t a clear way to keep shared BridgeStan state.

The ‘sidecar manifest’ is a clever workaround. An alternative to consider to avoid the copy would be to have the bridgestan_lib() function instead be something like make_bs_model(data) -> bs.StanModel, which then stuffs whatever manifest is needed into an extra piece of the ‘data’ argument before calling the normal StanModel constructor, and updating the shared lib’s bs_construct function to read that out rather than do the lookup you mention

Nawaf was looking at a pure funnel. He reated a really great movie, but the forums will only let me upload static images. @WardBrian may have a web link.

The pure funnel is actually easier than the mess resulting from a handful of data points, which is worse in both centered and non-centered than they both are at a pure funnel. I have to do some more investigation to see what’s going wrong in the funnel cases. I put a pretty conservative upper limit of 5 halving of step size and a Nuts tree depth of 5 (we push more trajectory into micro steps, so this isn’t as bad as it sounds for Walnuts).

It turns out that the worst place to be for centered or non-centered parameterizations is a low data count like 4 or 8. That winds up not being amenable to being sampled by either. And it’s harder than centered or non-centered parameterizations of the funnel, which is what was shown in Nawaf’s animation.

oh sick. Done feat: embed the BridgeStan manifest in the data argument by seantalts · Pull Request #70 · seantalts/stanli · GitHub merging now, thank you!!

import stanli
from walnutpie import walnuts_stan

model = stanli.bridgestan_model(stan_file="model.stan", data={"N": 10, "y": [0.1] * 10})
chains = walnuts_stan(model=model, num_chains=4, seed=42)
print(chains[0]["theta"].mean())