Show progress during sampling#
BlackJAX provides a context manager, progress_bar(), that adds a live progress bar to any sampling call without modifying the algorithm itself. The bar works transparently under JAX transformations like jax.vmap, fixing the shape-mismatch crashes of the old progress_bar= parameter (see issue #927).
How it works#
The context manager instruments the outermost jax.lax.scan encountered at trace time and injects a step counter via a JAX callback. No algorithm parameters need to change—simply wrap your sampling call with with blackjax.progress_bar():.
The model: Bayesian linear regression#
For all examples in this guide, we’ll use the same Bayesian linear regression problem:
with known noise scale \(\sigma\) and Gaussian priors on the weight vector \(w\).
import warnings
import jax
import jax.numpy as jnp
from jax.scipy.stats import norm
import blackjax
from blackjax.util import run_inference_algorithm
# Headless doc builds have no ipywidgets; the plain-text bar is intended here.
warnings.filterwarnings("ignore", message="IProgress not found")
N, DIM, SIGMA = 300, 10, 0.5
kx, kw, kn = jax.random.split(jax.random.key(0), 3)
X = jax.random.normal(kx, (N, DIM))
true_w = 2.0 * jax.random.normal(kw, (DIM,))
y = X @ true_w + SIGMA * jax.random.normal(kn, (N,))
def logprior(w):
return norm.logpdf(w, 0.0, 5.0).sum()
def loglikelihood(w):
return norm.logpdf(y - X @ w, 0.0, SIGMA).sum()
logdensity = lambda w: logprior(w) + loglikelihood(w)
Basic usage: labeled bar around a warmup#
Wrap any sampling call with with blackjax.progress_bar() and provide a label. The bar automatically detects the outermost scan and displays live updates.
warmup = blackjax.window_adaptation(blackjax.nuts, logdensity)
k_warm = jax.random.key(1)
with blackjax.progress_bar(label="NUTS warmup"):
(state, params), _ = warmup.run(k_warm, jnp.zeros(DIM), num_steps=1000)
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
File ~/work/blackjax/blackjax/blackjax/progress_bar.py:302, in progress_bar(label, print_rate, output_file)
301 try:
--> 302 import jaxtap as tap
303 except ImportError as exc:
ModuleNotFoundError: No module named 'jaxtap'
The above exception was the direct cause of the following exception:
ImportError Traceback (most recent call last)
Cell In[3], line 4
1 warmup = blackjax.window_adaptation(blackjax.nuts, logdensity)
2 k_warm = jax.random.key(1)
3
----> 4 with blackjax.progress_bar(label="NUTS warmup"):
5 (state, params), _ = warmup.run(k_warm, jnp.zeros(DIM), num_steps=1000)
File /usr/lib/python3.12/contextlib.py:137, in _GeneratorContextManager.__enter__(self)
135 del self.args, self.kwds, self.func
136 try:
--> 137 return next(self.gen)
138 except StopIteration:
139 raise RuntimeError("generator didn't yield") from None
File ~/work/blackjax/blackjax/blackjax/progress_bar.py:304, in progress_bar(label, print_rate, output_file)
302 import jaxtap as tap
303 except ImportError as exc:
--> 304 raise ImportError(
305 "blackjax.progress_bar requires the 'progress' optional extra. "
306 "Install it with: pip install 'blackjax[progress]'"
307 ) from exc
309 state = ProgressState(label=label, print_rate=print_rate, output_file=output_file)
311 def _on_step(event):
ImportError: blackjax.progress_bar requires the 'progress' optional extra. Install it with: pip install 'blackjax[progress]'
Controlling update frequency with print_rate#
By default, the bar updates every 5% of steps (≈ num_steps // 20). For slower iterations (e.g., when each step is expensive), use print_rate=1 to update every step. This is especially useful for slow distributed clusters (see issue #960).
nuts = blackjax.nuts(logdensity, **params)
with blackjax.progress_bar(label="NUTS sampling", print_rate=1):
_, samples = run_inference_algorithm(
rng_key=jax.random.key(2),
initial_state=state,
inference_algorithm=nuts,
num_steps=1500,
transform=lambda st, _: st.position,
)
posterior_mean = samples.mean(axis=0)
print("max |posterior_mean - true_w| =", jnp.abs(posterior_mean - true_w).max())
Multi-phase workflows: adaptation and sampling in one context#
Some algorithms (like mclmc_find_L_and_step_size) perform multiple internal scans during adaptation. A single progress_bar() context wraps all phases. Watch the bar reset per phase with its own total and elapsed time—this is expected behavior for multi-phase runs.
k_init, k_tune, k_run = jax.random.split(jax.random.key(3), 3)
mclmc_state = blackjax.mcmc.mclmc.init(
position=jnp.zeros(DIM), logdensity_fn=logdensity, rng_key=k_init
)
kernel = blackjax.mcmc.mclmc.build_kernel(
integrator=blackjax.mcmc.mclmc.isokinetic_mclachlan
)
with blackjax.progress_bar(label="MCLMC adapt+sample"):
tuned_state, mclmc_params, _ = blackjax.mclmc_find_L_and_step_size(
mclmc_kernel=kernel,
logdensity_fn=logdensity,
num_steps=2000,
state=mclmc_state,
rng_key=k_tune,
diagonal_preconditioning=True,
)
alg = blackjax.mclmc(
logdensity,
L=mclmc_params.L,
step_size=mclmc_params.step_size,
inverse_mass_matrix=mclmc_params.inverse_mass_matrix,
)
_, mclmc_samples = run_inference_algorithm(
rng_key=k_run,
initial_state=tuned_state,
inference_algorithm=alg,
num_steps=1500,
transform=lambda st, _: st.position,
)
print("MCLMC max |mean - true_w| =", jnp.abs(mclmc_samples.mean(0) - true_w).max())
Multi-chain sampling with jax.vmap#
This is the centerpiece: a full multi-chain workflow using jax.vmap. The old progress bar crashed under vmap with shape mismatches (issue #927). The new implementation is vmap-safe because jaxtap inserts the step counter as jnp.int32(0) in the scan carry—a compile-time constant that depends only on itself (step + 1) and therefore stays unbatched under jax.vmap even when the rest of the carry is batched. jax.debug.callback with an unbatched argument fires once per step for the entire batch. The bar fires exactly once per real step regardless of chain count.
First, run a vmapped warmup across 4 chains in parallel:
n_chains = 4
k_wu = jax.random.split(jax.random.key(11), n_chains)
init_positions = jax.random.normal(jax.random.key(13), (n_chains, DIM))
mc_warmup = blackjax.window_adaptation(blackjax.nuts, logdensity)
def warmup_one(key, pos):
(st, chain_params), _ = mc_warmup.run(key, pos, num_steps=1000)
return st, chain_params
with blackjax.progress_bar(label=f"vmap warmup ({n_chains} chains)"):
warm_states, chain_params = jax.vmap(warmup_one)(k_wu, init_positions)
print("Per-chain adapted params shape:", jax.tree.map(jnp.shape, chain_params))
Now sample from each chain with its own adapted parameters:
k_sm = jax.random.split(jax.random.key(12), n_chains)
def sample_one(key, st, p):
alg = blackjax.nuts(logdensity, **p) # per-chain step_size + mass matrix
_, hist = run_inference_algorithm(
rng_key=key,
initial_state=st,
inference_algorithm=alg,
num_steps=1500,
transform=lambda s, _: s.position,
)
return hist
with blackjax.progress_bar(label=f"vmap sampling ({n_chains} chains)"):
mc_chains = jax.vmap(sample_one)(k_sm, warm_states, chain_params)
print("Output shape (chains, steps, dim):", mc_chains.shape)
Tempered SMC: bar ticks per temperature#
Sequential Monte Carlo iteratively tempering a likelihood is a natural fit for progress bars: the bar ticks once per temperature, not per particle move. This is because the outermost scan is over the tempering schedule.
import numpy as np
import blackjax.smc.resampling as resampling
from blackjax.smc import extend_params
num_particles, num_tempering = 300, 30
lambda_schedule = np.logspace(-5, 0, num_tempering)
hmc_parameters = extend_params(
{
"step_size": 1e-2,
"inverse_mass_matrix": jnp.eye(DIM),
"num_integration_steps": 10,
}
)
tempering = blackjax.tempered_smc(
logprior,
loglikelihood,
blackjax.hmc.build_kernel(),
blackjax.hmc.init,
hmc_parameters,
resampling.systematic,
10, # num_mcmc_steps per tempering move
)
k_particles, k_smc = jax.random.split(jax.random.key(5))
initial_particles = 5.0 * jax.random.normal(k_particles, (num_particles, DIM))
smc_state = tempering.init(initial_particles)
def smc_step(carry, lmbda):
i, st = carry
st, info = tempering.step(jax.random.fold_in(k_smc, i), st, lmbda)
return (i + 1, st), info
with blackjax.progress_bar(label=f"tempered SMC ({num_tempering} temps)"):
(_, smc_final), _ = jax.lax.scan(smc_step, (0, smc_state), lambda_schedule)
smc_mean = smc_final.particles.mean(axis=0)
print("SMC max |particle mean - true_w| =", jnp.abs(smc_mean - true_w).max())
Note: Adaptive tempering (with unknown length) runs inside a while_loop, so a determinate progress bar is not possible. Use output_file= and the external reader (see below) to get a heartbeat instead.
File-based progress: external monitoring#
For long-running jobs, write progress to a file that can be monitored from a separate terminal:
with blackjax.progress_bar(
label="file-backed", output_file="/tmp/bjx_progress.txt", print_rate=1
):
_, _ = run_inference_algorithm(
rng_key=jax.random.key(6),
initial_state=state,
inference_algorithm=nuts,
num_steps=500, # reduced for docs build time
transform=lambda st, _: st.position,
)
While sampling runs in one process, monitor it from another:
cd /path/to/blackjax && uv run python -m blackjax.progress_reader /tmp/bjx_progress.txt
This displays live updates of <current_step> <total_steps> and is useful for HPCs where Jupyter is unavailable.
Common gotchas#
JIT cache staleness#
There are two distinct JIT-cache cases with different behaviors.
Case A — compiled in an earlier context, called in a new one (improved):
jaxtap bakes a module-level singleton (_dynamic_router) into XLA artifacts rather than a closure over the creating context. A cache-hit in a new context routes events to the currently-active recorder at call time — the bar works and no warning fires.
sample_jitted = jax.jit(
lambda key: run_inference_algorithm(
rng_key=key,
initial_state=state,
inference_algorithm=nuts,
num_steps=100,
transform=lambda st, _: st.position,
)[1]
)
# Context 1: traces the function, bakes _dynamic_router
with blackjax.progress_bar(label="chain 1 (traces)"):
_ = sample_jitted(jax.random.key(7))
# Context 2: cache hit → _dynamic_router routes to ctx2's recorder → bar works
with blackjax.progress_bar(label="chain 2 (cache hit → bar works)"):
_ = sample_jitted(jax.random.key(8))
Case B — compiled before entering any context (unchanged behavior):
If a function was JIT-compiled before any progress_bar() was ever entered, no _dynamic_router callback is baked in. A later context sees zero events and the zero-step warning fires. Fix: jax.clear_caches() before entering to force a retrace.
import warnings
# Compile entirely outside any progress_bar context.
jax.clear_caches()
pre_compiled = jax.jit(
lambda key: run_inference_algorithm(
rng_key=key,
initial_state=state,
inference_algorithm=nuts,
num_steps=100,
transform=lambda st, _: st.position,
)[1]
)
_ = pre_compiled(jax.random.key(10)) # compiled now, no callback baked in
# Inside a context: 0 events → warning fires.
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with blackjax.progress_bar(label="pre-compiled (no bar)"):
_ = pre_compiled(jax.random.key(11))
if w:
print("Warning (expected):", w[0].message)
# Fix: clear caches to force retrace with the callback baked in.
jax.clear_caches()
with blackjax.progress_bar(label="after clear_caches (bar works)"):
_ = pre_compiled(jax.random.key(12)) # retraced, bar now works
Manual __enter__ without __exit__ leaks the patch#
Never use manual __enter__ / __exit__ except for error handling. Leaving a context unclosed patches jax.lax.scan for the entire kernel session:
# Show the leak and recovery
cm = blackjax.progress_bar(label="leaked")
cm.__enter__()
print("Scan is patched:", "progress_bar" in jax.lax.scan.__module__)
# Recovery: always call __exit__. (We suppress the zero-step UserWarning --
# nothing was sampled inside this demonstration context, by design.)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
cm.__exit__(None, None, None)
print("Scan is restored:", "progress_bar" not in jax.lax.scan.__module__)
Other caveats#
Checkpoint + gradient: Under
jax.checkpointcombined with differentiation, the callback fires twice per logical step (primal + recompute), so step counts appear roughly doubled. Computed values and gradients are unaffected.Multi-device sharding: Under
jax.shard_map, the callback fires once per device per step, multiplying the host dispatch overhead. The bar may reach 100% while slower shards are still running.functools.partialbypass: Afunctools.partial(jax.lax.scan, ...)captured before the context is entered silently bypasses the bar with no error.Shared
output_file: If two contexts share the same file path, their writes interleave and corrupt the file. Use a unique path per context.Composing with
jaxtap.record(): Two cases to know about:Nesting (innermost wins): If you open a
jaxtap.record()context inside aprogress_bar()context, the user’s inner context wins attribution for scans run within its block. The progress bar will be silent for those scans and resume receiving events afterward. This is documented behavior, not a bug.Cross-consumer JIT cache hits: jaxtap’s device-side
selectfunction is baked into compiled artifacts at trace time — it cannot be changed dynamically. If a function compiled under one context type is called inside a different type: (a) a function traced undertap.record(select=custom_fn)called insideprogress_bar()works correctly — since jaxtap 0.2.1,_dynamic_routerapplies the receiving context’smax_depth=0at routing time, so exactly the outermostn_stepsevents reach_on_stepregardless of how deeply the original trace instrumented;_on_stepis also safe with arbitraryevent.valuesince it only readsevent.stepandevent.total; (b) a function traced underprogress_bar()called inside user’stap.record(select=custom_fn)delivers events to the user’s recorder, butevent.value == ()(the bakedselect=lambda _: ()fromprogress_bar()wins device-side; the user’s custom select is not applied). Both directions are crash-safe; the Direction Bvalue=()behavior is documented jaxtap: “trace-time device-side select wins.”
Jupyter rendering#
By default, the progress bar displays as a rich widget in Jupyter (requires ipywidgets >= 7.0, installed separately). Without it, you get a text-based bar on stderr. Both work identically; the widget is simply prettier in notebooks.
Verified frontends: terminal, JupyterLab (widget and text modes), VS Code’s notebook interface (including Remote-SSH), and marimo (console-path text bar; for long marimo runs the output_file= + reader pattern above is a good alternative).
To use the rich widget:
pip install ipywidgets
No code changes needed—the bar detects the environment automatically.
Summary#
Wrap any sampling call with
with blackjax.progress_bar(label="...")to add a live bar.Works under
jax.vmapwithout shape mismatches (fixes issue #927).Use
print_rate=to control update frequency for expensive iterations.Multi-phase runs (e.g., MCLMC tuning + sampling) show bar resets per phase—expected behavior.
Keep one context open across repeated calls to the same compiled function to avoid JIT cache staleness.
Use
output_file=+python -m blackjax.progress_readerfor monitoring long-running jobs from another terminal.Always use the
withform; manual__enter__without__exit__leaks the patch.