NUTS ~100–200× slower on numpyro 0.21.0 than 0.20.0 for a simple hierarchical Bernoulli model (GPU)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2.8k
- Forks
- 315
- Avg merge
- 3d 9h
- Merged PRs (30d)
- 27
Description
Bug Description
After upgrading from numpyro 0.20.0 → 0.21.0, NUTS becomes dramatically slower
for a simple Bayesian hierarchical model with a binary (Bernoulli) likelihood, when
the number of groups K is small relative to the number of observations N.
The slowdown is not caused by the sampler taking more leapfrog steps: the mean
number of steps per sample is essentially unchanged across versions. Only the
wall-clock runtime explodes, which points at a regression in per-step / compilation
cost rather than sampler behavior.
The upgrade also moves jax from 0.9.1 → 0.10.2, so the root cause could live in
either package.
Results (single L4 GPU, N = 100000, 1000 warmup + 2000 samples, 1 chain)
| jax | numpyro | K | runtime_s | mean_num_steps |
|---|---|---|---|---|
| 0.9.1 | 0.20.0 | 8 | 26.76 | 23.66 |
| 0.9.1 | 0.20.0 | 48 | 23.70 | 28.76 |
| 0.9.1 | 0.20.0 | 900 | 18.19 | 15 |
| 0.9.1 | 0.20.0 | 15000 | 38.16 | 31 |
| 0.10.2 | 0.21.0 | 8 | 5101.77 | 25.86 |
| 0.10.2 | 0.21.0 | 48 | 1554.13 | 29.15 |
| 0.10.2 | 0.21.0 | 900 | 40.66 | 15 |
| 0.10.2 | 0.21.0 | 15000 | 40.15 | 31 |
Observations:
- At K=8 runtime goes from 27s to 5100s (190×); at K=48, 24s → 1550s (65×).
mean_num_stepsis basically identical between versions (e.g. 23.66 vs 25.86 at
K=8), so the sampler is doing the same amount of work — each step is just far
slower.- The regression shrinks as
Kgrows: at K=900 and K=15000 the two versions are
comparable. The blow-up is worst whenKis small (few groups, many observations).
Steps to Reproduce
import time
import csv
import numpy as np
import jax
import jax.numpy as jnp
import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS
from numpyro.infer.reparam import LocScaleReparam
N = 100000
def bernoulli_hier(group_idx, K, y=None):
tausq = numpyro.sample("tausq", dist.InverseGamma(5.0, 0.5))
with numpyro.plate("levels", K):
with numpyro.handlers.reparam(config={"alpha": LocScaleReparam(centered=0)}):
alpha = numpyro.sample("alpha", dist.Normal(0.0, jnp.sqrt(tausq)))
numpyro.sample("obs", dist.Bernoulli(logits=alpha[group_idx]), obs=y)
def make_bernoulli_data(N, K, seed=42):
rng = np.random.default_rng(seed)
group_idx = rng.integers(0, K, size=N).astype(np.int32)
alpha_true = rng.normal(0.0, 0.5, size=K)
p = 1.0 / (1.0 + np.exp(-alpha_true[group_idx]))
y = (rng.random(size=N) < p).astype(np.int32)
return jnp.asarray(group_idx), jnp.asarray(y)
def run_fit(model_fn, **model_kwargs):
mcmc = MCMC(NUTS(model_fn), num_warmup=1000, num_samples=2000, num_chains=1)
t0 = time.time()
mcmc.run(jax.random.PRNGKey(42), extra_fields=("num_steps",), **model_kwargs)
runtime = time.time() - t0
ns = np.asarray(mcmc.get_extra_fields()["num_steps"])
return runtime, ns
rows = []
for K in [8, 48, 900, 15000]:
group_idx, y = make_bernoulli_data(N, K)
runtime, ns = run_fit(bernoulli_hier, group_idx=group_idx, K=K, y=y)
row = {
"jax_version": jax.__version__,
"numpyro_version": numpyro.__version__,
"K": K,
"runtime_s": round(runtime, 2),
"mean_num_steps": round(float(ns.mean()), 2),
}
rows.append(row)
print(row)
fieldnames = ["jax_version", "numpyro_version", "K", "runtime_s", "mean_num_steps"]
with open("numpyro_issue_results_local.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
Environment
- GPU: NVIDIA L4
- numpyro: 0.20.0 vs 0.21.0
- jax / jaxlib: 0.9.1 (with 0.20.0) vs 0.10.2 (with 0.21.0)
- Python: 3.12.13
- CUDA / cuDNN / driver: cuda 13
- OS: Ubuntu 22.04.5 LTS
Expected Behavior
- Is this a known regression introduced by the 0.21.0 changes, the jax 0.10.x
upgrade, or the interaction of the two? - Is there a workaround (e.g. a config flag, a different reparameterization, or
pinning a specific jax version) to recover 0.20.0 performance on 0.21.0?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the supplied reproduction script with NUTS and LocScaleReparam across the listed K values. Compare the JAX and NumPyro version combinations to isolate whether the regression belongs to JAX, NumPyro, or their interaction, focusing on compilation and per-step runtime. Done means identifying the cause and documenting a verified workaround or regression boundary.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- machine-learning, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100