Skip to content

Validate num_samples in MCMC initialization - #2236

Merged
Qazalbash merged 1 commit into
pyro-ppl:masterfrom
haansn08:patch-1
Aug 12, 2026
Merged

Validate num_samples in MCMC initialization #2236
Qazalbash merged 1 commit into
pyro-ppl:masterfrom
haansn08:patch-1

Conversation

@haansn08

@haansn08 haansn08 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

I tried to debug my MCMC kernel by doing just one iteration using

mcmc = MCMC(kernel, num_warmup=1, num_samples=0)
I got this very long error message
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[55], line 4
      2 kernel = HMCGibbs(inner_kernel=hmc_kernel, gibbs_fn=gibbs_fn, gibbs_sites = ["ability"])
      3 mcmc = MCMC(kernel, num_warmup=11, num_samples = 0)
----> 4 mcmc.run(jax.random.key(0), y, 5)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/infer/mcmc.py:711, in MCMC.run(self, rng_key, extra_fields, init_params, *args, **kwargs)
    709 map_args = (rng_key, init_state, init_params)
    710 if self.num_chains == 1:
--> 711     states_flat, last_state = partial_map_fn(map_args)
    712     states = jax.tree.map(lambda x: x[jnp.newaxis, ...], states_flat)
    713 else:

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/infer/mcmc.py:496, in MCMC._single_chain_mcmc(self, init, args, kwargs, collect_fields, remove_sites)
    490 collection_size = self._collection_params["collection_size"]
    491 collection_size = (
    492     collection_size
    493     if collection_size is None
    494     else collection_size // self.thinning
    495 )
--> 496 collect_vals = fori_collect(
    497     lower_idx,
    498     upper_idx,
    499     sample_fn,
    500     init_val,
    501     transform=_collect_and_postprocess(
    502         postprocess_fn, collect_fields, remove_sites
    503     ),
    504     progbar=self.progress_bar,
    505     progress_rate=self.progress_rate,
    506     return_last_val=True,
    507     thinning=self.thinning,
    508     collection_size=collection_size,
    509     progbar_desc=partial(_get_progbar_desc_str, lower_idx, phase),
    510     diagnostics_fn=diagnostics,
    511     num_chains=self.num_chains
    512     if (callable(self.chain_method) or self.chain_method == "parallel")
    513     else 1,
    514 )
    515 states, last_val = collect_vals
    516 # Get first argument of type `HMCState`

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:446, in fori_collect(lower, upper, body_fun, init_val, transform, progbar, progress_rate, return_last_val, collection_size, thinning, **progbar_opts)
    444 with tqdm.trange(upper, miniters=progress_rate) as t:
    445     for i in t:
--> 446         vals = _body_fn(i, *vals)
    448         t.set_description(progbar_desc(i), refresh=False)
    449         if diagnostics_fn:

    [... skipping hidden 1 frame]

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/pjit.py:253, in _cpp_pjit.<locals>.cache_miss(*args, **kwargs)
    250 if config.no_tracing.value:
    251   raise RuntimeError(f"re-tracing function {jit_info.fun_sourceinfo} for "
    252                      "`jit`, but 'no_tracing' is set")
--> 253 p, args_flat = _infer_params(fun, jit_info, args, kwargs)
    254 (outs, out_flat, out_tree, args_flat, jaxpr,
    255  executable, pgle_profiler, const_args) = _run_python_pjit(
    256      p, args_flat, fun, args, kwargs)
    258 maybe_fastpath_data = _get_fastpath_data(
    259     executable, out_tree, args_flat, out_flat, jaxpr.effects, jaxpr.consts,
    260     pgle_profiler, const_args)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/pjit.py:624, in _infer_params(fun, ji, args, kwargs)
    621 if entry.pjit_params is not None:
    622   return entry.pjit_params, entry.pjit_params.consts + dynargs
--> 624 p = _trace_for_jit(fun, ji, ctx_mesh, dbg_fn(), avals, args, kwargs)
    625 if p.params['jaxpr'].jaxpr.is_high:
    626   return p, p.consts + dynargs

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/pjit.py:526, in _trace_for_jit(fun, ji, ctx_mesh, dbg, avals, args, kwargs)
    524       jaxpr, out_avals = pe.trace_to_jaxpr(fun, in_type, dbg, qdd_token)
    525   else:
--> 526     jaxpr, out_avals = pe.trace_to_jaxpr(fun, in_type, dbg, qdd_token)
    528 if config.debug_key_reuse.value:
    529   # Import here to avoid circular imports
    530   from jax.experimental.key_reuse._core import check_key_reuse_jaxpr  # pytype: disable=import-error

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/interpreters/partial_eval.py:2283, in trace_to_jaxpr(***failed resolving arguments***)
   2281 with core.set_current_trace(trace):
   2282   args, kwargs = in_tracers.unflatten()
-> 2283   ans_pytree = fun(*args, **kwargs)
   2284   if fun_returns_flat_tree:
   2285     # TODO(dougalm): make result paths optional
   2286     ans = ans_pytree

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:402, in fori_collect.<locals>._body_fn(i, val, collection, start_idx, thinning)
    399 def update_collection(collection, val):
    400     return jax.tree.map(update_fn, collection, transform(val))
--> 402 collection = update_collection(collection, val)
    403 return val, collection, start_idx, thinning

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:400, in fori_collect.<locals>._body_fn.<locals>.update_collection(collection, val)
    399 def update_collection(collection, val):
--> 400     return jax.tree.map(update_fn, collection, transform(val))

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/tree.py:156, in map(f, tree, is_leaf, *rest)
    116 def map(f: Callable[..., Any],
    117         tree: Any,
    118         *rest: Any,
    119         is_leaf: Callable[[Any], bool] | None = None) -> Any:
    120   """Maps a multi-input function over pytree args to produce a new pytree.
    121
    122   Args:
   (...)    154     - :func:`jax.tree.reduce`
    155   """
--> 156   return tree_util.tree_map(f, tree, *rest, is_leaf=is_leaf)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/tree_util.py:397, in tree_map(f, tree, is_leaf, *rest)
    395 leaves, treedef = tree_flatten(tree, is_leaf)
    396 all_leaves = [leaves] + [treedef.flatten_up_to(r) for r in rest]
--> 397 return treedef.unflatten(f(*xs) for xs in zip(*all_leaves))

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/tree_util.py:397, in <genexpr>(.0)
    395 leaves, treedef = tree_flatten(tree, is_leaf)
    396 all_leaves = [leaves] + [treedef.flatten_up_to(r) for r in rest]
--> 397 return treedef.unflatten(f(*xs) for xs in zip(*all_leaves))

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:391, in fori_collect.<locals>._body_fn.<locals>.update_fn(collect_array, new_val)
    390 def update_fn(collect_array, new_val):
--> 391     return cond(
    392         idx >= 0,
    393         collect_array,
    394         lambda x: x.at[idx].set(new_val),
    395         collect_array,
    396         identity,
    397     )

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:153, in cond(pred, true_operand, true_fun, false_operand, false_fun)
    151         return false_fun(false_operand)
    152 else:
--> 153     return lax.cond(pred, true_operand, true_fun, false_operand, false_fun)

    [... skipping hidden 1 frame]

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/lax/control_flow/conditionals.py:231, in cond(pred, true_fun, false_fun, operand, *operands)
    229 if callable(false_fun) and len(operands) == 2 and callable(operands[1]):
    230   x_true, f_true, x_false, f_false = true_fun, false_fun, *operands
--> 231   return cond(pred, lambda x, _: f_true(x), lambda _, x: f_false(x), x_true, x_false)
    232 else:
    233   raise TypeError("lax.cond: true_fun and false_fun arguments should be callable.")

    [... skipping hidden 1 frame]

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/lax/control_flow/conditionals.py:280, in cond(***failed resolving arguments***)
    277   api_util.check_no_aliased_ref_args(lambda: dbg_true, list(avals), list(args))
    278 dbg_false = api_util.debug_info("cond", false_fun, operands, {})
--> 280 true_jaxpr_, out_avals = pe.trace_to_jaxpr(true_fun, avals, dbg_true)
    281 true_jaxpr_, true_consts = pe.separate_consts(true_jaxpr_)
    282 false_jaxpr_, false_out_avals = pe.trace_to_jaxpr(false_fun, avals, dbg_false)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/interpreters/partial_eval.py:2283, in trace_to_jaxpr(***failed resolving arguments***)
   2281 with core.set_current_trace(trace):
   2282   args, kwargs = in_tracers.unflatten()
-> 2283   ans_pytree = fun(*args, **kwargs)
   2284   if fun_returns_flat_tree:
   2285     # TODO(dougalm): make result paths optional
   2286     ans = ans_pytree

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/lax/control_flow/conditionals.py:231, in cond.<locals>.<lambda>(x, _)
    229 if callable(false_fun) and len(operands) == 2 and callable(operands[1]):
    230   x_true, f_true, x_false, f_false = true_fun, false_fun, *operands
--> 231   return cond(pred, lambda x, _: f_true(x), lambda _, x: f_false(x), x_true, x_false)
    232 else:
    233   raise TypeError("lax.cond: true_fun and false_fun arguments should be callable.")

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/numpyro/util.py:394, in fori_collect.<locals>._body_fn.<locals>.update_fn.<locals>.<lambda>(x)
    390 def update_fn(collect_array, new_val):
    391     return cond(
    392         idx >= 0,
    393         collect_array,
--> 394         lambda x: x.at[idx].set(new_val),
    395         collect_array,
    396         identity,
    397     )

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/numpy/array_methods.py:1178, in _IndexUpdateRef.set(self, values, indices_are_sorted, unique_indices, mode, out_sharding, wrap_negative_indices)
   1176   assert isinstance(out_sharding, (NamedSharding, PartitionSpec))
   1177   out_sharding = canonicalize_sharding(out_sharding, '.set')
-> 1178 return scatter._scatter_update(
   1179     self.array, self.index, values, lax_slicing.scatter,
   1180     indices_are_sorted=indices_are_sorted, unique_indices=unique_indices,
   1181     mode=mode, out_sharding=out_sharding,
   1182     normalize_indices=wrap_negative_indices)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/ops/scatter.py:88, in _scatter_update(x, idx, y, scatter_op, indices_are_sorted, unique_indices, mode, normalize_indices, out_sharding)
     84 if out_sharding is not None:
     85   return auto_axes(internal_scatter, out_sharding=out_sharding,
     86                    axes=out_sharding.mesh.explicit_axes
     87                    )(x, y, dynamic_idx)
---> 88 return internal_scatter(x, y, dynamic_idx)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/ops/scatter.py:111, in _scatter_impl(x, y, dynamic_idx, scatter_op, treedef, indices_are_sorted, unique_indices, mode, normalize_indices)
    103   warnings.warn(
    104     "scatter inputs have incompatible types: cannot safely cast value "
    105     f"from dtype={lax.dtype(y)} to dtype={lax.dtype(x)} with "
    106     f"jax_numpy_dtype_promotion={config.numpy_dtype_promotion.value}. "
    107     "In future JAX releases this will result in an error.",
    108     FutureWarning)
    110 general_indexer = tree_util.tree_unflatten(treedef, dynamic_idx)
--> 111 indexer = general_indexer.to_gather(
    112     core.typeof(x).sharding, normalize_indices=normalize_indices)
    114 # Avoid calling scatter if the slice shape is empty, both as a fast path and
    115 # to handle cases like zeros(0)[array([], int32)].
    116 if core.is_empty_shape(indexer.slice_shape):

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/numpy/indexing.py:579, in NDIndexer.to_gather(self, x_sharding, normalize_indices)
    577 def to_gather(self, x_sharding: NamedSharding | Any,
    578               normalize_indices: bool = True) -> _GatherIndexer:
--> 579   return _index_to_gather(self, x_sharding=x_sharding, normalize_indices=normalize_indices)

File /nix/store/bkqp5piw63cwdwqcxpzwwm89zaisfhji-python3-3.13.13-env/lib/python3.13/site-packages/jax/_src/numpy/indexing.py:1387, in _index_to_gather(indexer, x_sharding, normalize_indices)
   1383 if index.typ in [IndexType.INTEGER, IndexType.ARRAY] and np.ndim(index.index) == 0:  # pyrefly: ignore[bad-argument-type]
   1384   # Basic scalar int indices
   1385   if core.definitely_equal(indexer.shape[x_axis], 0):
   1386     # XLA gives error when indexing into an axis of size 0
-> 1387     raise IndexError(f"index is out of bounds for axis {x_axis} with size 0")
   1388   i_converted = lax.convert_element_type(index.index, index_dtype)  # pyrefly: ignore[bad-argument-type]
   1389   gather_indices.append((i_converted, len(gather_indices_shape)))

IndexError: index is out of bounds for axis 0 with size 0

and spend a lot of time debugging my kernel for indexing mistakes.
This PR adds a check for num_samples that hopefully saves the next person from doing that.

@Qazalbash Qazalbash self-assigned this Aug 5, 2026
@Qazalbash
Qazalbash self-requested a review August 5, 2026 15:42

@Qazalbash Qazalbash 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.

Thanks for tackling this — an early, explicit error beats an IndexError from deep inside fori_collect. I ran the cases below against this branch to check the guard's coverage, and I think it needs a bit of widening before it lands. Findings are ordered by severity.

1. The guard checks the value but not the type

num_samples < 1 accepts any non-integer that compares against 1, so the argument types users actually get wrong still slip through:

MCMC(NUTS(model), num_warmup=2, num_samples=10.0)  # constructs fine
# .run() -> TypeError: Shapes must be 1D sequences of concrete values of integer type, got (10.0,)

MCMC(NUTS(model), num_warmup=2, num_samples=2.5)   # constructs fine
# silently truncates to 2 samples, no warning

num_samples=True is also accepted as 1 (bool subclasses int). The thinning check two lines below already has the right shape for this:

if not isinstance(thinning, int) or thinning < 1:

The 2.5 case is the one I'd most want to catch — it produces a quietly wrong number of draws rather than an error.

2. TypeError instead of ValueError for None

MCMC(NUTS(model), num_warmup=2, num_samples=None)
# TypeError: '<' not supported between instances of 'NoneType' and 'int'

None is the common case when num_samples comes from a config file or CLI arg with a missing key, and callers wrapping construction in except ValueError won't catch it. The isinstance form in point 1 fixes this for free.

3. This rejects a warmup-only workflow that currently works

MCMC(..., num_samples=0) followed by mcmc.warmup(key, collect_warmup=True) works today and is now rejected at construction. Verified on this branch with the guard removed:

mcmc = MCMC(NUTS(model), num_warmup=6, num_samples=0)
mcmc.warmup(random.key(0), collect_warmup=True)
mcmc.get_samples()  # {'x': (6,)} -- correct

The reason it works is that the collect_warmup=True branch never reads self.num_samples:

if collect_warmup:
self._set_collection_params(0, self.num_warmup, self.num_warmup, "warmup")
else:
self._set_collection_params(
self.num_warmup, self.num_warmup, self.num_samples, "warmup"
)

It passes self.num_warmup as the collection size. So adaptation-only runs (tune step size / mass matrix, stash post_warmup_state, sample later) are a legitimate use of num_samples=0. The collect_warmup=False path genuinely is broken with 0, so the constraint is real — it just isn't a constructor-level invariant. Validating in run() / the collect_warmup=False branch would catch the actual breakage without rejecting the working case.

4. num_samples >= 1 isn't the real precondition — thinning interacts

MCMC(NUTS(model), num_warmup=4, num_samples=1, thinning=2)
# passes both guards, then .run() -> IndexError: index is out of bounds for axis 0 with size 0

collection_size = collection_size // self.thinning gives 1 // 2 == 0:

collection_size = (
collection_size
if collection_size is None
else collection_size // self.thinning
)

Since this PR adds validation immediately next to the thinning check, num_samples >= thinning seems like the constraint worth enforcing.

5. num_warmup on the line above has a worse, silent failure

mcmc = MCMC(NUTS(model), num_warmup=-5, num_samples=4)
mcmc.run(random.key(0))
mcmc.get_samples()['x']   # [-0. -0. -0. -0.]

No error at all — uninitialized buffer contents returned as posterior draws. _set_collection_params computes lower=-5, upper=-1, fori_collect's assert lower <= upper passes, and the loop body never runs. num_chains=0 is also unvalidated (IndexError: tuple index out of range).

Silently wrong numbers are worse than the crash this PR fixes, and it's the adjacent line. Rather than three more one-off if statements, a small shared helper validating num_warmup, num_samples, num_chains, and thinning uniformly would fix the whole family at the right depth.

Smaller points

  • No test. The new branch has no coverage, so a future refactor could drop it silently. (The neighbouring thinning check is untested too, so there's no existing pattern to follow — a small pytest.raises(ValueError) parametrized over 0 and -1 in test/infer/test_mcmc.py would cover both.)
  • Docstring. Line 272 still reads :param int num_samples: Number of samples to generate from the Markov chain. The constraint isn't discoverable except by triggering it; compare thinning, documented as Positive integer that controls....
  • Validation only at __init__. num_samples is a plain public attribute read live at run time (_set_collection_params, _compile, warmup, run), so mcmc.num_samples = 0 after construction reinstates every failure mode. Worth noting if the intent is a real invariant rather than a typo catcher.
  • Message wording. "num_samples must be at least 1" vs. line 349's "thinning must be a positive integer" for the same class of constraint. Including the received value would also help when the argument is computed rather than literal.

Happy to be wrong on #3 if warmup-only with num_samples=0 isn't considered supported — but if it's being dropped deliberately, that's worth calling out as a behaviour change.

Review generated with Claude Code

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Benchmark report

this PR patch-1 at 182a5ac3 vs baseline master at a47333ee

  run time:     unchanged across 32 benchmarks
  compile time: unchanged across 32 benchmarks

No significant changes

Every benchmark stayed within ±5% run time and ±25% compile time.

Red is slower, green is faster; a row is coloured by the worse of its two columns. A delta in parentheses cleared the threshold on a measurement below the resolution floor, so it is shown without being called a change. † marks a benchmark that could not be compared — see below.

Full results

distributions

                                 ──────── run time ────────     ────── compile time ──────
  benchmark                      baseline  this PR        Δ     baseline   this PR       Δ
──────────────────────────────────────────────────────────────────────────────────────────
  biject_to_constraints            3.0 ms   3.0 ms    -2.2%     181.5 ms  195.7 ms   +7.8%
  categorical_log_prob             1.6 ms   1.6 ms    +2.6%      51.3 ms   53.0 ms   +3.3%
  dirichlet_log_prob               420 µs   430 µs    +2.5%     366.8 ms  332.6 ms   -9.3%
  dirichlet_sample                42.2 ms  42.3 ms    +0.3%     556.5 ms  573.5 ms   +3.0%
  gamma_log_prob                   1.4 ms   1.3 ms    -2.9%       2.04 s    1.99 s   -2.1%
  gamma_sample                    19.6 ms  19.8 ms    +1.5%     529.4 ms  545.2 ms   +3.0%
  lkj_cholesky_sample              4.3 ms   4.2 ms    -2.2%     828.0 ms  855.8 ms   +3.4%
  mixture_same_family_log_prob     1.5 ms   1.5 ms    +0.4%      74.5 ms   82.3 ms  +10.5%
  multivariate_normal_log_prob     425 µs   423 µs    -0.7%     122.8 ms  129.4 ms   +5.4%
  normal_log_prob                  454 µs   462 µs    +1.8%      50.1 ms   49.3 ms   -1.7%
  normal_sample                   19.6 ms  19.3 ms    -1.1%     159.7 ms  156.0 ms   -2.3%
  stick_breaking_transform         4.5 ms   4.5 ms    -0.6%     156.7 ms  160.2 ms   +2.3%
  student_t_log_prob               2.5 ms   2.4 ms    -1.2%      69.5 ms   70.2 ms   +1.0%
  truncated_normal_log_prob        484 µs   511 µs  (+5.5%)      44.6 ms   44.6 ms   +0.2%

handlers

                                  ──────── run time ───────     ────── compile time ─────
  benchmark                       baseline   this PR      Δ     baseline   this PR      Δ
─────────────────────────────────────────────────────────────────────────────────────────
  initialize_model_hierarchical    26.4 ms   27.2 ms  +3.2%       3.39 s    3.34 s  -1.6%
  log_density_hierarchical          2.2 ms    2.2 ms  +2.1%       1.08 s    1.07 s  -0.1%
  nested_handler_stack              929 µs    936 µs  +0.8%       723 µs    688 µs  -4.9%
  potential_energy_and_grad          19 µs     19 µs  -1.1%      79.9 ms   82.7 ms  +3.4%
  predictive_forward_sampling     517.6 ms  529.1 ms  +2.2%     130.0 ms  132.0 ms  +1.5%
  trace_seeded_model                525 µs    508 µs  -3.2%     444.9 ms  450.1 ms  +1.2%

mcmc

                             ──────── run time ───────     ───── compile time ─────
  benchmark                  baseline   this PR      Δ     baseline  this PR      Δ
───────────────────────────────────────────────────────────────────────────────────
  hmc_logistic_regression    556.5 ms  544.6 ms  -2.1%       2.55 s   2.71 s  +6.1%
  nuts_dense_mass_funnel     900.4 ms  902.9 ms  +0.3%       2.30 s   2.43 s  +5.5%
  nuts_eight_schools         890.9 ms  875.8 ms  -1.7%       2.18 s   2.25 s  +3.2%
  nuts_hierarchical_glm        3.24 s    3.22 s  -0.6%       4.05 s   3.98 s  -1.9%
  nuts_logistic_regression   814.7 ms  839.1 ms  +3.0%       2.65 s   2.70 s  +1.9%
  nuts_vectorized_chains       1.85 s    1.86 s  +0.6%       2.37 s   2.31 s  -2.3%

svi

                                             ──────── run time ───────     ───── compile time ─────
  benchmark                                  baseline   this PR      Δ     baseline  this PR      Δ
───────────────────────────────────────────────────────────────────────────────────────────────────
  svi_autodelta_map_logistic                 224.6 ms  227.5 ms  +1.3%       2.52 s   2.61 s  +3.3%
  svi_autodiagonalnormal_hierarchical        780.4 ms  798.9 ms  +2.4%       4.37 s   4.33 s  -0.8%
  svi_automultivariatenormal_eight_schools   547.7 ms  552.3 ms  +0.8%       3.55 s   3.50 s  -1.3%
  svi_autonormal_logistic                    561.1 ms  577.6 ms  +3.0%       2.75 s   2.72 s  -1.1%
  svi_multi_particle_elbo                      1.09 s    1.08 s  -0.9%       2.94 s   2.75 s  -6.7%
  svi_trace_mean_field_elbo                  930.9 ms  937.6 ms  +0.7%       4.60 s   4.63 s  +0.7%
Methodology and environment

Each benchmark is set up untimed, then called once with the JAX caches cleared and several more times warm. Run is the fastest warm call; compile is the first call minus that, i.e. the tracing, lowering and XLA compilation the warm calls did not have to pay for.

Both refs were measured on the same runner over 2 interleaved round(s), taking the best observation per benchmark. A result is called neutral when it moves less than ±5% (run) or ±25% (compile), or when the measurement itself is under 1 ms (run) / 50 ms (compile) — a shared CI runner cannot resolve changes below that. Compile time gets the looser band because it is measured once per round rather than best-of-N, and swings by roughly 20% even between two runs of identical code. A delta shown in parentheses did clear its threshold, but on a measurement below the resolution floor, so it is reported without being called a change.

baseline this PR
ref master patch-1
commit a47333ee 182a5ac3
numpyro 0.21.0 0.21.0
jax 0.11.0 0.11.0
backend cpu cpu
python 3.14.7 3.14.7

Runner: Linux-6.17.0-1020-azure-x86_64-with-glibc2.39, 4 CPUs.

Produced by this benchmark run.

@Qazalbash Qazalbash added the awaiting review Awaiting review from maintainers label Aug 11, 2026

@Qazalbash Qazalbash 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.

Thanks @haansn08!

@Qazalbash
Qazalbash merged commit 1f6877a into pyro-ppl:master Aug 12, 2026
10 checks passed
@Qazalbash Qazalbash removed the awaiting review Awaiting review from maintainers label Aug 12, 2026
@haansn08
haansn08 deleted the patch-1 branch August 12, 2026 13:30
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