Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider removing rng_seeder user-interface from Model #5785

Closed
ricardoV94 opened this issue May 19, 2022 · 4 comments · Fixed by #5787
Closed

Consider removing rng_seeder user-interface from Model #5785

ricardoV94 opened this issue May 19, 2022 · 4 comments · Fixed by #5787
Labels
Milestone

Comments

@ricardoV94
Copy link
Member

ricardoV94 commented May 19, 2022

Right now draw, prior and posterior_predictive can be seeded only from rng_seeder passed when creating a new pm.Model. This makes it convenient in that a user does not need to pass seeds to these functions individually to get seeded results, but it is less convenient in that there is an "invisible" dependency from the order and number of methods that are called.

In concrete, if you call pm.draw before pm.sample_prior_predictive you will get different results than if you do it in the opposite order.

import pymc as pm
with pm.Model(rng_seeder=3) as m:
    x = pm.Normal("x")
    print("draw: ", pm.draw(x))
    print("prior: ", pm.sample_prior_predictive(samples=1, return_inferencedata=False)["x"][0])
    
with pm.Model(rng_seeder=3) as m:
    x = pm.Normal("x")
    print("prior: ", pm.sample_prior_predictive(samples=1, return_inferencedata=False)["x"][0])
    print("draw: ", pm.draw(x))   
draw:  1.1557538103058285
prior:  -1.2931919932524942
prior:  1.1557538103058285
draw:  -1.2931919932524942

This happens because shared RandomState/Generator variables for the model RVs are only set once when they are created (based on the rng_seeder) and never re-seeded. There is currently no easy mechanism to re-seed all variables although it seems like there was one at some point during the V4 refactor, probably when we were using a RandomStream under the hood.

If there was one mechanism (which will probably be reintroduced when #5733 gets fixed), users could in theory call Model.seed(seed) between calls to control/reset the seeding. It would look something like this:

import pymc as pm
with pm.Model(rng_seeder=3) as m:
    x = pm.Normal("x")
    print("draw1: ", pm.draw(x))

    # Does not exist currently
    m.seed(3)
    print("prior1: ", pm.sample_prior_predictive(samples=1, return_inferencedata=False)["x"][0])
    
with pm.Model(rng_seeder=3) as m:
    x = pm.Normal("x")
    print("prior2: ", pm.sample_prior_predictive(samples=1, return_inferencedata=False)["x"][0])

    # Does not exist currently
    m.seed(3)
    print("draw2: ", pm.draw(x))    
draw:  1.1557538103058285
prior:  1.1557538103058285
prior:  1.1557538103058285
draw:  1.1557538103058285

But if users want to do this, it seems more naturally to allow them to pass a seed directly to these methods and then do the re-seeding internally for the user. It would look something like this:

import pymc as pm
# rng_seeder would be removed to avoid confusion over which "way" is best
with pm.Model() as m:
    x = pm.Normal("x")
    # Neither of these methods accepts seeds currently
    print("draw1: ", pm.draw(x, seed=3))
    print("prior1: ", pm.sample_prior_predictive(samples=1, seed=3, return_inferencedata=False)["x"][0])
    
with pm.Model() as m:
    x = pm.Normal("x")
    # Neither of these methods accepts seeds currently
    print("prior2: ", pm.sample_prior_predictive(samples=1, seed=3, return_inferencedata=False)["x"][0])
    print("draw2: ", pm.draw(x, seed=3))

With the same results as in the previous example.

Right now only sample accepts seeds explicitly, but if that's missing it defaults to rng_seeder. IMHO it is confusing to allow two ways to seed things, and this has emerged a couple of times in pymc-examples (cc @OriolAbril).

There is one tiny technical difference in that users can directly specify a seed per chain only via sample, but I think this is not enough reason to justify keeping two interfaces for that function. Also it's probably safer if users pass a single seed and we spawn multiple ones "smartly", to avoid the type of issues described in #5733


Creating this as an issue instead of discussion, because I think it should be addressed quickly to not create confusion as we release V4 / too much work re-running pymc-examples.

@pymc-devs/dev-core

@twiecki
Copy link
Member

twiecki commented May 19, 2022 via email

@michaelosthege
Copy link
Member

Yes, I would also prefer passing seed to the functions instead of a RNG-stateful Model.

@lucianopaz
Copy link
Contributor

How do you think this should look like internally? Should we add an entry in the model that points to the shared random generators that feed into the random variables and swap them with new ones in a copy or a function graph? Or should we set their values somehow?
I’m thinking about this keeping in mind that in the near future we want to make it easy to pickle a compiled function and unpickle it, swaping the random seeds in the copies.

@ricardoV94
Copy link
Member Author

ricardoV94 commented May 19, 2022

@lucianopaz I opened a draft PR in #5787.

Internally it just swaps RNGs in compile_pymc, as we were already collecting the updates there anyway.

There are two accessible helper functions to find all RNG variables in a graph and update them, which could be used when copying a compiled function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants