Getting started#
The best way to get started with PyVBMC is via the tutorials and worked examples. In particular, start with PyVBMC Example 1: Basic usage and continue from there.
Optionally, we recommend running pyvbmc.calibrate() once to tune performance
for your machine (memory, processor, etc.); it takes tens of seconds and can
make PyVBMC run faster.
Saved calibration settings are reused automatically by future runs (see the
calibration guide).
If you are already familiar with approximate inference methods, you can find a summary usage below.
Summary usage#
The typical usage pipeline of PyVBMC follows four steps:
Define the model, which defines a target log density (i.e., an unnormalized log posterior density);
Set up the parameters (parameter bounds, starting point);
Initialize and run the inference;
Examine and visualize the results.
PyVBMC is not concerned with how you define your model in step 1, as long as you can provide an (unnormalized) target log density. Running the inference in step 3 only involves a couple of lines of code:
from pyvbmc import VBMC
# ...
vbmc = VBMC(target, x0, LB, UB, PLB, PUB)
vp, results = vbmc.optimize()
with input arguments:
target: the target (unnormalized) log density — often an unnormalized log posterior.targettakes as input a parameter vector and returns the log density at the point. The returned log density must be a finite real value, i.e. neitherNaNnor+/-inf. See the FAQ for more details;x0: the starting point of the inference in parameter space;LBandUB: hard lower and upper bounds for the parameters (can be-infandinf, or bounded);PLBandPUB: plausible lower and upper bounds, that is a box that ideally brackets a region of high density of the target.
You can also pass a log-likelihood as target and supply its prior
separately with prior= or log_prior=. For model evidence, include
the likelihood’s normalization constants and use a normalized prior; see
the FAQ.
The outputs are:
vp: aVariationalPosteriorobject which approximates the true target density;results: adictwith additional information. Important keys are: -"elbo": the estimated lower bound on the log model evidence (log normalization constant); -"elbo_sd": the standard deviation of the estimate of the ELBO (not the error between the ELBO and the true log model evidence, which is generally unknown).
The vp object can be manipulated in various ways, see the VariationalPosterior class documentation.
See the examples for more detailed information. The Basic options may also be useful.
For noisy likelihoods, see PyVBMC Example 6: Noisy log-likelihood evaluations and the FAQ for the target interface and noise options.
PyVBMC occasionally prints a tip or a reminder when a new run starts. Pass
options={"show_tips": False} to disable tips and reminders about old
releases. To suppress the calibration reminder and ordinary optimization
output as well, use options={"display": "off"}.
Reproducible runs#
Pass seed= to control the random generator used by a VBMC instance
and its variational posterior. The same integer seed reproduces a run
with the same target, setup and numerical environment. Reusing the same
integer seed repeats PyVBMC’s random stream. For independent runs, leave
seed unset or use different seeds; comparing their results helps assess convergence
(see PyVBMC Example 4: Multiple runs as validation).
The seed can also be a NumPy Generator. PyVBMC advances that generator,
so reusing it continues its random stream. The posterior shares the run’s
generator: drawing samples from it before resuming inference changes the
subsequent draws of the run.
If your likelihood function uses random simulations, VBMC(seed=...)
does not control the randomness inside that function. To reproduce the
whole analysis, also set the simulator’s seed before starting the run.
For example, with NumPy, create sim_rng = np.random.default_rng(123)
once outside your target function, then use that generator for its random
draws (e.g., sim_rng.normal(...) instead of np.random.normal(...)).
If you use another simulation library, use its seed or generator argument.
Let the simulator draw fresh random numbers on each evaluation; do not
reset its seed inside the target function.
The FAQ
explains reproducibility and the behavior when seed is omitted.
Bring a torch or JAX model into PyVBMC#
The default target interface calls one point at a time. It accepts a one-dimensional NumPy parameter vector and returns one finite scalar. A model implemented in Torch can be connected with a small user-written wrapper that keeps framework objects outside PyVBMC. The example below uses independent Gaussian observations with unit standard deviation and a Gaussian prior on their common mean. Install the modelling framework used by your model separately:
import numpy as np
import torch
from pyvbmc import VBMC
from scipy.stats import norm
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
observations_t = torch.tensor(
[0.4, -0.2, 0.7], dtype=torch.float64, device=device
)
def torch_log_likelihood(x):
theta_t = torch.as_tensor(x, dtype=torch.float64, device=device)
value_t = -0.5 * torch.sum(
(observations_t - theta_t[0]) ** 2 + np.log(2 * np.pi)
)
return np.asarray(
value_t.detach().cpu().numpy(), dtype=np.float64
).item()
x0 = np.array([0.0], dtype=np.float64)
lb, ub = np.array([-np.inf]), np.array([np.inf])
plb, pub = np.array([-2.0]), np.array([2.0])
vbmc = VBMC(
torch_log_likelihood,
x0,
lb,
ub,
plb,
pub,
prior=norm(loc=0.0, scale=2.0),
)
detach().cpu().numpy() completes device execution and returns the scalar
to the host. This also makes PyVBMC’s target timing cover the framework work.
The explicit torch.float64 matches PyVBMC’s numerical precision.
For JAX, enable 64-bit values before importing jax.numpy or constructing
the model. jax.device_get completes the computation and moves its result
to the host:
from jax import config
config.update("jax_enable_x64", True)
import jax
import jax.numpy as jnp
import numpy as np
observations_j = jnp.array([0.4, -0.2, 0.7], dtype=jnp.float64)
def jax_log_likelihood(x):
theta_j = jnp.asarray(x, dtype=jnp.float64)
value_j = -0.5 * jnp.sum(
(observations_j - theta_j[0]) ** 2 + jnp.log(2 * jnp.pi)
)
return np.asarray(
jax.device_get(value_j), dtype=np.float64
).item()
Vectorized targets#
Set options={"vectorized_target": True} when the likelihood can evaluate
a batch. PyVBMC collects all initial-design rows whose values were not supplied
and evaluates them in one target call. All subsequent target calls still
contain one point, with shape (1, D). The input is always a NumPy array in
original coordinates and the target must return a finite NumPy array with
shape (N,) or (N, 1). PyVBMC does not probe a target to determine
whether it supports batches. A scalar return is invalid even when N is
one.
The Torch wrapper above becomes:
def torch_vectorized_log_likelihood(x):
theta_t = torch.as_tensor(x, dtype=torch.float64, device=device)
values_t = -0.5 * torch.sum(
(observations_t[None, :] - theta_t[:, :1]) ** 2
+ np.log(2 * np.pi),
dim=1,
)
return np.asarray(
values_t.detach().cpu().numpy(), dtype=np.float64
)
vbmc = VBMC(
torch_vectorized_log_likelihood,
x0,
lb,
ub,
plb,
pub,
prior=norm(loc=0.0, scale=2.0),
options={"vectorized_target": True},
)
The equivalent JAX wrapper is:
def jax_vectorized_log_likelihood(x):
theta_j = jnp.asarray(x, dtype=jnp.float64)
values_j = -0.5 * jnp.sum(
(observations_j[None, :] - theta_j[:, :1]) ** 2
+ jnp.log(2 * jnp.pi),
axis=1,
)
return np.asarray(jax.device_get(values_j), dtype=np.float64)
A separately supplied prior keeps its scalar interface. PyVBMC evaluates it one point at a time and combines it with the batched likelihood; custom prior functions therefore do not need to accept a batch.
For a target with user-provided noise estimates, set
"specify_target_noise": True and return a pair of arrays. Each array must
have shape (N,) or (N, 1); the standard deviations must be finite and
positive. Do not combine the two outputs into an (N, 2) array. For
example:
def torch_noisy_log_likelihood(x):
values = torch_vectorized_log_likelihood(x)
sds = np.full(values.shape, 0.1, dtype=np.float64)
return values, sds
def jax_noisy_log_likelihood(x):
values = jax_vectorized_log_likelihood(x)
sds_j = jnp.full(values.shape, 0.1, dtype=jnp.float64)
sds = np.asarray(jax.device_get(sds_j), dtype=np.float64)
return values, sds
noisy_options = {
"vectorized_target": True,
"specify_target_noise": True,
}
Pass noisy_options as the options argument when constructing
VBMC with either noisy wrapper.
To reuse cached target values, pass precomputed_evaluations=(X, y) to
VBMC. Each value in y must be the output of the target supplied to
VBMC at the corresponding row of X. When a prior is supplied
separately, the target and y contain the log-likelihood and PyVBMC adds the
prior; when the target is a log joint, y contains that log joint. See
Reusing evaluations and charging initialization work for shapes,
noisy observations and budget accounting.
For a complete Torch fit, an equivalent short JAX target, Torch posterior predictions and density gradients, and ArviZ summaries, see PyVBMC Example 9: Torch and JAX models and posterior exports.
Bring a PyMC model into PyVBMC#
Install the optional integration on Python 3.12 or newer with
python -m pip install --upgrade "pyvbmc[pymc]". A
PyMCTarget provides the flat target and setup that
PyVBMC needs. This model has a named coefficient vector and a positive
observation scale:
import numpy as np
import pymc as pm
from pyvbmc import PyMCTarget, VBMC
rng = np.random.default_rng(7)
x_data = np.linspace(-1.0, 1.0, 30)
design = np.column_stack((np.ones(x_data.size), x_data))
observations = design @ np.array([0.5, 1.0])
observations += rng.normal(0.0, 0.7, size=x_data.size)
coords = {
"observation": np.arange(x_data.size),
"coefficient": ["intercept", "slope"],
}
with pm.Model(coords=coords) as model:
X = pm.Data("X", design, dims=("observation", "coefficient"))
beta = pm.Normal("beta", 0.0, 2.0, dims="coefficient")
sigma = pm.HalfNormal("sigma", 1.0)
mean = pm.Deterministic("mean", X @ beta, dims="observation")
pm.Normal("y", mean, sigma, observed=observations, dims="observation")
target = PyMCTarget(model, seed=7)
print(target)
vbmc = VBMC(
target,
seed=7,
options={"max_fun_evals": target.setup_cost + 60},
)
vp, results = vbmc.optimize()
posterior_data = target.to_arviz(vp, n_samples=1000)
posterior_data = pm.compute_deterministics(
posterior_data, model=model, extend_dataset=True
)
predictions = pm.sample_posterior_predictive(
posterior_data, model=model, var_names=["y"], random_seed=7
)
print(target) lists each model variable, its shape, its flat VBMC
coordinates, hard and plausible bounds, and its start. Here beta becomes
beta[intercept] and beta[slope]. The positive sigma keeps PyMC’s
log transform, so its VBMC coordinate is sigma_log__ and the target
includes the corresponding Jacobian.
The adapter chooses a starting point and plausible bounds automatically.
Its setup work counts toward max_fun_evals through target.setup_cost;
the example allows 60 more evaluations for inference. Use a larger budget
and validate repeated runs for your own model. Pass setup_budget= to
limit the setup work, or start= and plausible_bounds= to supply your
own choices in model coordinates.
The adapter supports continuous float64 variables with recognized support and standard PyMC transforms. It does not support discrete variables. See PyMCTarget for the supported models and setup options.
PyMCTarget keeps the model data used at construction. Build a new target
to refit changed data, and export each posterior with the target used to fit
it. PyMCTarget(seed=) controls setup draws; VBMC(seed=) controls the
inference run. PyVBMC Example 8: Fitting a PyMC model shows the full
workflow.
Use a fitted posterior downstream#
Stacking several runs#
Stacking Variational Bayesian Monte Carlo (S-VBMC)
(Silvestrin et al., 2025) combines the posteriors of several completed
VBMC runs on the same model and data into a stacked posterior, without
further model evaluations. Combining runs often improves the approximation
to the true posterior. See
PyVBMC Example 7: Stacking the posteriors of several runs (S-VBMC)
for a worked example. S-VBMC requires the optional torch extra; the
installation guide also covers dependencies for
the posterior exports below.
Torch distribution#
to_torch() exports the fitted posterior as
a torch distribution:
import torch
posterior_t = vp.to_torch()
torch.manual_seed(7)
samples_t = posterior_t.sample((1000,))
point_t = samples_t[0].detach().clone().requires_grad_(True)
gradient_t = torch.autograd.grad(posterior_t.log_prob(point_t), point_t)[0]
The default export uses original parameter coordinates, CPU tensors, and
torch.float64, regardless of torch’s global defaults. Pass
orig_flag=False for PyVBMC’s unbounded internal coordinates, or specify
dtype=torch.float32 and device=... explicitly when needed. The export
is an independent copy; sampling it uses torch’s random generator.
For bounded parameters, log_prob accepts finite values strictly inside
the hard bounds. Support validation rejects exact-bound and outside values.
See the method reference for supported shapes and boundary behavior.
The complete workflow in PyVBMC Example 9: Torch and JAX models and posterior exports uses this distribution for psychometric probability predictions and density gradients.
ArviZ DataTree#
to_arviz() draws independent samples into
the current ArviZ DataTree format:
import arviz as az
posterior_data = vp.to_arviz(
n_samples=2000, var_names=["location"]
)
summary = az.summary(posterior_data, group="posterior", kind="stats")
axes = az.plot_dist(posterior_data, group="posterior")
The result contains one posterior group and one chain. By default, each
parameter is a scalar variable named x_0, …, x_{D-1}; the structured
form below supports vector and matrix variables. Pass orig_flag=False to
export samples in internal coordinates.
For a three-dimensional posterior whose first two columns form a vector, preserve the variable shape and labels with a structured export:
# Here vp has D == 3.
structured_data = vp.to_arviz(
n_samples=2000,
variables={"beta": (2,), "sigma": ()},
dims={"beta": ["coef"]},
coords={"coef": ["intercept", "slope"]},
)
Here beta has dimensions (chain, draw, coef) and sigma has
dimensions (chain, draw). Variables consume consecutive posterior columns
in mapping order.
This method advances vp.rng in the same way as
sample(). The draws are independent samples
from a variational approximation, so MCMC convergence diagnostics computed on
them do not measure the quality of that approximation. Use posterior summaries
and plots, and assess PyVBMC convergence from its own results and repeated
runs.