Right now, it is very hard to ascertain progress when running MCMC with num_chains > 1. Let us figure out ways to show progress bar updates. Even a progress bar without diagnostic updates will be useful.
Currently, chain methods that support progress bar:
With this, we can make the following enhancement.
Hmm, is this possible? AFAIK, the progressbar will only show the progress of compiling the code, while the real execution is hidden. Hopefully, there will be some tools in JAX allowing us measure this progress.
is this possible?
Maybe not yet; it will likely depend on solutions for https://github.com/google/jax/issues/364. For sequential chains, it might be possible (though I am not fully sure).
JAX now supports an experimental host_callback. ~It seems that this is doable now, but we need to study how that module works.~ Edit host_callback just collects intermediate results at the end of the process
If it's helpful: this answered question in the JAX Discussion shows how to print the progress of an MCMC chain within a compiled function (using the host_callback module mentioned above).
This solution might need improvement when running several chains in parallel though.
That's a great news @jeremiecoullon! Have you come up with a solution for MCMC chain yet? This would be one of the most requested feature from NumPyro users. :)
So I did a quick test which seems to work.
In numpyro/util.py I added the following functions:
from jax.experimental import host_callback
def _print_consumer(arg, transrorm):
i, n_iter = arg
print(f"Iteration {i}/{n_iter}")
@jit
def progress_bar(arg, result):
"""
Print progress of loop only if iteration number is a multiple of the print_rate
Usage: carry = progress_bar((iter_num, n_iter, print_rate), carry)
"""
i, n_iter, print_rate = arg
result = lax.cond(
i%print_rate==0,
lambda _: host_callback.id_tap(_print_consumer, (i, n_iter), result=result),
lambda _: result,
operand=None)
return result
Then I simply added the following to the fori_collect() function in the _body_fn() definition, just after idx = (i - start_idx) // thinning:
print_rate = int(upper/10)
i = progress_bar((i, upper, print_rate), i)
i is the current iteration number, upper is the number of iterations, and the print rate is set to be 1/10 of the number of iterations.
I tested it on the 8 schools example on the readme and set progress_bar=False: mcmc = MCMC(nuts_kernel, num_warmup=500, num_samples=1000000, progress_bar=False, num_chains=1). I ran for 1million samples so you have time to see the progress bar unfold (otherwise it's too fast).
The way it works is that progress_bar is an identity function: it takes in i as argument and returns i. A side effect of this function is that it calls host_callback.id_tap() which invokes the python print function only if the print_rate divides the number of iterations. The reason you have to pass in i (or any other variable that is dependent of the arguments of the main compiled function) is to make sure the python print function is called at the correct time.
I did a quick speed benchmark (running a single chain of the 8 schools example for 1 million iterations), and the progress bar seems to make no difference to speed. This is because the python function is only called very 100K iterations. For every other iteration progress_bar() simply acts like the identity function.
The main problem with this solution is that it messes up the existing tqdm progress bar, as _body_fn() is also used in that case.
Suggested solution: define a decorator within the fori_collect() definition that adds the new progress bar to a function, and wrap _body_fn() in this decorator when passing it tofori_loop(). First import functools, then add the following within the fori_collect() definition:
def add_progress_bar(func):
@functools.wraps(func)
def wrapper_progress_bar(i, vals):
print_rate = int(upper/10)
i = progress_bar((i, upper, print_rate), i)
return func(i, vals)
return wrapper_progress_bar
....
if not progbar:
last_val, collection, _, _ = fori_loop(0, upper, add_progress_bar(_body_fn), (init_val, collection, start_idx, thinning))
"chain 3: Iteration 1000/10000"). Not sure how to do this as the chain number isn't included in each mcmc chainfori_loop) is called when progress_bar=False in the mcmc object. If we add a progress bar to the compiled version as well this would be confusing. Suggestion: we could rename that argument to compile=True/False instead._body_fn has a caching decorator. How does the progress bar (in decorator form) interact with this?As mentioned in the first message of this issue, we could also add info about compilation. The hack I do when writing samplers in JAX is to simply add a python print function at the beginning of the function (print("Compiling..")) and then I add another print function at the end (print("Done.")). This will only be called when the function is compiling and is a simple way to see how long compilation takes vs actually running.
Example:
@partial(jit, static_argnums=(1,))
def my_sampler(key, num_samples):
print("Compiling..")
samples = function_that_gets_samples(key, num_samples)
print("Done.")
return samples
I'm just making a pull request for the progress bar within a decorator for this.
There may of course be loads more problems that I'm missing as I'm not familiar with the NumPyro codebase :)
Oops, I missed your comment. As far as I see, your PR works as expected. So I think migrating it to tqdm would not be so complicated.
not familiar with the NumPyro codebase
I see. I'll play more to understand those issues and will get back to you soon.
This issue has been resolved thanks to @jeremiecoullon !
Most helpful comment
So I did a quick test which seems to work.
Suggested solution:
In
numpyro/util.pyI added the following functions:Then I simply added the following to the
fori_collect()function in the_body_fn()definition, just afteridx = (i - start_idx) // thinning:iis the current iteration number,upperis the number of iterations, and the print rate is set to be 1/10 of the number of iterations.I tested it on the 8 schools example on the readme and set
progress_bar=False:mcmc = MCMC(nuts_kernel, num_warmup=500, num_samples=1000000, progress_bar=False, num_chains=1). I ran for 1million samples so you have time to see the progress bar unfold (otherwise it's too fast).The way it works is that progress_bar is an identity function: it takes in
ias argument and returnsi. A side effect of this function is that it callshost_callback.id_tap()which invokes the python print function only if theprint_ratedivides the number of iterations. The reason you have to pass ini(or any other variable that is dependent of the arguments of the main compiled function) is to make sure the python print function is called at the correct time.Speed:
I did a quick speed benchmark (running a single chain of the 8 schools example for 1 million iterations), and the progress bar seems to make no difference to speed. This is because the python function is only called very 100K iterations. For every other iteration
progress_bar()simply acts like the identity function.Interaction with tqdm
The main problem with this solution is that it messes up the existing tqdm progress bar, as
_body_fn()is also used in that case.Suggested solution: define a decorator within the
fori_collect()definition that adds the new progress bar to a function, and wrap_body_fn()in this decorator when passing it tofori_loop(). First importfunctools, then add the following within thefori_collect()definition:Other issues with this:
"chain 3: Iteration 1000/10000"). Not sure how to do this as the chain number isn't included in each mcmc chainfori_loop) is called whenprogress_bar=Falsein themcmcobject. If we add a progress bar to the compiled version as well this would be confusing. Suggestion: we could rename that argument tocompile=True/Falseinstead._body_fnhas a caching decorator. How does the progress bar (in decorator form) interact with this?Showing when the sampler is compiling:
As mentioned in the first message of this issue, we could also add info about compilation. The hack I do when writing samplers in JAX is to simply add a python print function at the beginning of the function (
print("Compiling..")) and then I add another print function at the end (print("Done.")). This will only be called when the function is compiling and is a simple way to see how long compilation takes vs actually running.Example:
I'm just making a pull request for the progress bar within a decorator for this.
There may of course be loads more problems that I'm missing as I'm not familiar with the NumPyro codebase :)