-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
Random: allow negative seeds #51416
Random: allow negative seeds #51416
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Alternative to #46190, see that PR for background. There isn't a strong use-case for accepting negative seeds, but probably many people tried something like `seed = rand(Int); seed!(rng, seed)` and saw it failing. As it's easy to support, let's do it. This might "break" some random streams, those for which the upper bit of `make_seed(seed)[end]` was set, so it's rare.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,12 +83,12 @@ MersenneTwister(seed::Vector{UInt32}, state::DSFMT_state) = | |
Create a `MersenneTwister` RNG object. Different RNG objects can have | ||
their own seeds, which may be useful for generating different streams | ||
of random numbers. | ||
The `seed` may be a non-negative integer or a vector of | ||
`UInt32` integers. If no seed is provided, a randomly generated one | ||
is created (using entropy from the system). | ||
See the [`seed!`](@ref) function for reseeding an already existing | ||
`MersenneTwister` object. | ||
The `seed` may be an integer or a vector of `UInt32` integers. | ||
If no seed is provided, a randomly generated one is created (using entropy from the system). | ||
See the [`seed!`](@ref) function for reseeding an already existing `MersenneTwister` object. | ||
|
||
!!! compat "Julia 1.11" | ||
Passing a negative integer seed requires at least Julia 1.11. | ||
|
||
# Examples | ||
```jldoctest | ||
|
@@ -290,20 +290,51 @@ function make_seed() | |
end | ||
end | ||
|
||
""" | ||
make_seed(n::Integer) -> Vector{UInt32} | ||
|
||
Transform `n` into a bit pattern encoded as a `Vector{UInt32}`, suitable for | ||
RNG seeding routines. | ||
|
||
`make_seed` is "injective" : if `n != m`, then `make_seed(n) != `make_seed(m)`. | ||
Moreover, if `n == m`, then `make_seed(n) == make_seed(m)`. | ||
|
||
This is an internal function, subject to change. | ||
""" | ||
function make_seed(n::Integer) | ||
n < 0 && throw(DomainError(n, "`n` must be non-negative.")) | ||
neg = signbit(n) | ||
n = abs(n) # n can still be negative, e.g. n == typemin(Int) | ||
if n < 0 | ||
# we assume that `unsigned` can be called on integers `n` for which `abs(n)` is | ||
# negative; `unsigned` is necessary for `n & 0xffffffff` below, which would | ||
# otherwise propagate the sign bit of `n` for types smaller than UInt32 | ||
n = unsigned(n) | ||
end | ||
seed = UInt32[] | ||
while true | ||
# we directly encode the bit pattern of `abs(n)` into the resulting vector `seed`; | ||
# to greatly limit breaking the streams of random numbers, we encode the sign bit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stream of random numbers is documented not to be stable across versions, even for the same seed. This seems like a lot of additional code to avoid breaking something we don't guarantee in the first place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can definitely break the random streams, but it can be some work then to fix the seeds in LinearAlgebra! And probably for some people in the wild. But I don't think that not breaking most seeds didn't represent much more code; the alternatives I saw were:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed in the other PR, I still don't understand why the sign bit needs to be treated specially at all. It's a bit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already explained at length my reasonning in the other PR, I'm afraid I don't have much more ideas on how to explain this. A Julia integer can be seen as a bag of bits, but it's first and foremost an object representing a mathematical number. I simply want (1) Property (1) is what we expect from mathematical functions, and property (2) is just useful for
Yes, with different seeds, we expect different streams. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My objection is with the interpretation that This is necessarily an operation that can't produce unique RNG objects for every possible seed, by the pigeonhole principle: the state of the PRNG is of finite size. You can of course say that you only really care about finitely sized inputs (so So, with that in mind, I don't understand why those two properties in particular are desirable, other than to give the illusion of uncorrelated RNG streams. I get that the properties seem nice, but you don't really get much out of them algorithmically. Documenting that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree it might be easier to maintain, but not by much as this PR shows. But also, again, I see no particular reason to treat integers as bag of bits: it's only an implementation detail of Besides, by just applying There are two steps in the process:
If the two properties (P1) mathematical function and (P2) injectivity, i.e. For 2), (P1) is already generally implemented: for a given initialization vector, the RNGs implemented by
This is a good point, that users should be aware of; though I don't have to prove anything, I'm not trying to solve this here, only trying to do the best possible thing for seeding. To be certain to avoid overlapping streams, the only way I know of (for the RNGs we have) would be to use "jumping", which is already implemented for
It's maybe an illusion in theory, but with the arguments above, i believe that for all practical matters, these properties go a long way into ensuring uncorrelated streams. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No; Julia integers are not mathematical integers (i.e., the set of natural numbers, if that's what you mean). They are modular integers..
Naive conversion via
which is exactly why I chose it in the original PR, in order to not have to take special considerations. So by special casing
"suitable" is doing a lot of heavy lifting here. Why is the direct interpretation of "bag of bits" not suitable - other than losing numerical interpretation, which
Could you expand on what you mean with "(P1) mathematical function"? That is not a testable property of a piece of code, as far as I'm aware. What criterion makes a function "mathematical"?
SHA cannot make a non-injective input injective overall. The failure of injectivity already happened before SHA ever saw any input; it cannot "disentangle" that without additional information, which is not provided as far as I can tell.
Yes, I agree with this. But I don't agree with "different inputs" meaning "different numerical interpretations of a string of bits" for the purpose of seeding an RNG.
I don't follow here. This is not about jumping or multiprocessing at all - this is about what Further, any user relying on task-based multithreading in julia will (or does) already benefit from jumping, both in MT as well as Xoshiro; the tasks are already seeded with a jumped value from the parent RNG.
No, it's also an illusion in practice, and one we should document & make visible, so that people can avoid shooting themselves in the foot by accident, by encouraging use of jumping and heavily discouraging seeding multiple RNGs. The correct procedure for getting multiple statistically uncorrelated RNG streams is to seed one parent RNG and create multiple sub-RNGs by jumping. Seeding multiple RNGs and hoping for uncorrelation is not sound. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't understand what makes you say that
I dont' understand this point.
I believe not: one way to see that is that, unless of course the implementation in this PR is faulty, it implements the following:
But even then, even if the sign bit was counted twice, for whatever that means, I fail to see what problem this would lead to.
I think it's suitable to a great extent, but I like the interpretation as "math integers" better here, as for example
As an anecdote,
Sorry I was too terse I guess. The property So
I don't get this. The pipeline being
Yes and I don't see any advantage to that, except slightly simpler code if we didn't bother to accept
But the big difference is that with the other PR using
I'm all for making jumping more visible, and finding it useful, I even implemented the jumping code for julia's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is counted twice:
Once in propagation of the original data (hence the leading
Those are both "mathematical functions". Both map an input to an output. What you're describing with P1 and P2 sounds to me like bijectivity, a property some mathematical functions (i.e., functions) have. Since we've already established in the other PR that this function cannot be injective (and thus not bijective) precisely because of
Why is that important? We intentionally barely guarantee anything about randomness. Why guarantee this? Why should a user expect this to be the case, when in tons of other contexts (e.g. hashing) we don't provide stableness across architectures at all?
No, it's just as trivial. Just create two random integers, convert them to
So you mean to tell me that it's more likely that a user runs into an overlap between Not to mention that due to SHA, "by accident" is also not going to work for the approach in the other PR. Are there bitpatterns for signed integers that overlap with the results from unsigned integers? Yes, but this PR does nothing to prevent that, save for pushing an additional There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oh I think i now see what you mean. But no, when interpreting julia integers as mathematical integers, I'm not counting the sign bit twice: a But anyway, I don't actually make much sense of "You're "extracting" two bits of information from a single input bit", for a single bit i can extract at most a single bit of information; the way i encode it doesn't change that; i could encode the sign bit by using 10 bits of storage, these 10 bits would still store one bit of information. And this redundant information wouldn't affect at all the soundess of the
By "both", are you refering to the examples I gave, i.e.
For a given input
No, P1 is
I will repeat for the last time: while
It's not crucial, but nice to have and trivial to implement. We have
AFAIK,
It can't be trivial if no one on earth is able to provide an instance of two distinct seeds producing the same stream. It's trivial only to an oracle.
I'm talking in terms of
I don't understand the "save for", encoding the sign bit in the upper bit |
||
# as the upper bit of `seed[end]` (i.e. for most positive seeds, `make_seed` returns | ||
# the same vector as when we didn't encode the sign bit) | ||
while !iszero(n) | ||
push!(seed, n & 0xffffffff) | ||
n >>= 32 | ||
if n == 0 | ||
return seed | ||
end | ||
n >>>= 32 | ||
end | ||
if isempty(seed) || !iszero(seed[end] & 0x80000000) | ||
push!(seed, zero(UInt32)) | ||
end | ||
if neg | ||
seed[end] |= 0x80000000 | ||
end | ||
seed | ||
end | ||
|
||
# inverse of make_seed(::Integer) | ||
from_seed(a::Vector{UInt32})::BigInt = sum(a[i] * big(2)^(32*(i-1)) for i in 1:length(a)) | ||
function from_seed(a::Vector{UInt32})::BigInt | ||
neg = !iszero(a[end] & 0x80000000) | ||
seed = sum((i == length(a) ? a[i] & 0x7fffffff : a[i]) * big(2)^(32*(i-1)) | ||
for i in 1:length(a)) | ||
neg ? -seed : seed | ||
end | ||
|
||
|
||
#### seed!() | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -21,6 +21,13 @@ multiple interleaved xoshiro instances). | |||
The virtual PRNGs are discarded once the bulk request has been serviced (and should cause | ||||
no heap allocations). | ||||
|
||||
The `seed` may be an integer or a vector of `UInt32` integers. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually a but undecided about committing yet to supporting
Suggested change
TODO: add allowed seeds as a type annotation in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like to use a vector of 4 random UInt64 for seeding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah perfect! So my idea was that low entropy seeds like integers should ultimately go through SHA. But we could consider that when you pass an array of 4 UInt64 (or 8 UInt32) words, these 256 bits already have sufficient entropy and can be used directly to initialize the state. So my idea was to allow any array of integer bits types, but only of the correct size (256 bits in total), and to skip SHA in this case. Tuples of integer bit integers could also be accepted. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am fine with doing an extra SHA, because it may be hard to explain the concept of sufficient entropy in the docs of Random.seed!(collect(reinterpret(UInt64, sha256("seed1")))) But I think it would be much nicer if the following worked. Random.seed!(codeunits("seed1")) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a local branch actually implementing |
||||
If no seed is provided, a randomly generated one is created (using entropy from the system). | ||||
See the [`seed!`](@ref) function for reseeding an already existing `Xoshiro` object. | ||||
|
||||
!!! compat "Julia 1.11" | ||||
Passing a negative integer seed requires at least Julia 1.11. | ||||
|
||||
# Examples | ||||
```jldoctest | ||||
julia> using Random | ||||
|
@@ -191,6 +198,12 @@ endianness and possibly word size. | |||
|
||||
Using or seeding the RNG of any other task than the one returned by `current_task()` | ||||
is undefined behavior: it will work most of the time, and may sometimes fail silently. | ||||
|
||||
When seeding `TaskLocalRNG()` with [`seed!`](@ref), the passed seed, if any, | ||||
may be an integer or a vector of `UInt32` integers. | ||||
|
||||
!!! compat "Julia 1.11" | ||||
Seeding `TaskLocalRNG()` with a negative integer seed requires at least Julia 1.11. | ||||
""" | ||||
struct TaskLocalRNG <: AbstractRNG end | ||||
TaskLocalRNG(::Nothing) = TaskLocalRNG() | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this introduce a type instability?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but this should be handled fine by union-splitting.