PRNGs for multiple threads/processes is a large and involved topic. Python has treated it with “benign neglect”, leaving everything up to the user. numpy
has – and appropriately so for it – taken it far more seriously.
At a bare minimum, yes, different threads should absolutely have their own PRNG states. Zero cross-talk/leakage/correlation with other threads’/processes’ PRNG states.
How to seed different threads to ensure this is a large & involved topic of its own, rarely justified beyond hand-waving intuitive probability arguments.. An exception is the Philox generator, which builds on crypto theory. It’s one of the methods numpy
supports, and is very popular in massively parallel real-world applications (it’s hard for a user to misuse it, even if they just use consecutive small ints as seeds, and gives very high quality pseudo-randomness - but “is slow” unless there’s special HW support for block cipher algorithms).
Best “simple” thing we can do with the Twister is to create a new Random
instance per thread, and seed it with, say os.urandom(16)
. See? It gets complicated right away
. Now we also need a way to tell the user which seed was picked per thread, so they can reproduce the run later.
Let users pick the seeds themselves, and they’re likely to get into subtle troubles. That’s one thing Philox solves. Seeding different threads with 0, 1, 2, 3, … works fine. Does that also work fine with the Twister? Well, who knows? See " hand-waving intuitive probability arguments" above.
When the Twister was first released, seeding with 0, 1, 2, 3, …, was an obvious and utter disaster. Not two weeks went by before they released a new version. It runs some rounds of “pseudo-random scrambling” on the initial seed state to try to spray the 1 bits all over the large internal state. That repaired the obvious disasters - but do others remain?
People wrote papers about that, with no “QED” in sight, but, as time went on, people in this world moved away from the Twister. Part of another problem is the Twister’s very large state. The full state for competitive generators can live in 2 cache lines.
Bottom line: the Twister isn’t the PRNG of choice anymore for massively parallel apps, but it’s the one we have and its massive state isn’t really a problem for “just dozens” of threads. Any sane way of using it in a parallel world, though, requires that each thread have its own copy of the state, preferably seeded with the highest-quality external approximation to “truly random” bits we can get (meaning os.urandom()
on most- all? -boxes).