Math.random() returns same number every time after a delay from setting randomseed

I checked each script for calls to math.randomseed() so it cant be one of mine either.

I do get random numbers initially (after the randomseed call), but that only seems to be a temporary state :confused:

I tried moving the seed setting to AFTER the initial delay, thinking the seed might be getting reset during that somehow, but then it gets reset after ANOTHER delay, so Im thinking its either a loop calling randomseed somewhere, or just multiple staggered calls.

Yeah Im giving it enough time, as I only call randomseed when server starts, once, and theres multiple seconds of difference there (I even printed the time to make sure)

Try this yourself:
math.randomseed(124524) wait() print(math.random(), math.random(), math.random())

The first is always the same as ‘expected’. But the ones after are different every call (FOR THE SAME SEED).
This could just be something about the commandline though, Ill try in script.

If I print it continuously in a script I get this:

0.3502914517655 0.89596240119633 0.82284005249184
0.80874050111393 0.58500930814539 0.47987304300058
0.3502914517655 0.89596240119633 0.82284005249184
0.80874050111393 0.58500930814539 0.47987304300058
0.80874050111393 0.58500930814539 0.47987304300058
0.4109317300943 0.016724143192846 0.55265968810083
0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.7466048158208 0.17410809656056 0.85894344920194
0.7466048158208 0.17410809656056 0.85894344920194
0.4109317300943 0.016724143192846 0.55265968810083
0.77895443586535 0.41578417310099 0.60463270973846
0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.4109317300943 0.016724143192846 0.55265968810083
0.77895443586535 0.41578417310099 0.60463270973846
0.4109317300943 0.016724143192846 0.55265968810083

It’s true that the delay apparently affects the number sequence that you get, but if you compare the rows of this output, then the first, second and third number always occur in the same way in sequence. These are the unique lines in my output:

0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.7466048158208 0.17410809656056 0.85894344920194
0.77895443586535 0.41578417310099 0.60463270973846
0.80874050111393 0.58500930814539 0.47987304300058

Yeah I noticed that weird repetition as well.

But anyways, I couldnt reproduce in studio play, but I could reproduce in START SERVER:

wait(1)
math.randomseed(os.time())
for i=1,157 do
	local a=math.random()
	if i<10 then
		print(a) --should be randomish because just set seed
	end
end
wait(6)
print("delayed:")
print(math.random()) --same like every time

(throw in script, start server, repeat a few times)

Look at the number it gives after the delay (the last one), I got something like 0.0125 most of the time. Which is the exact same number I kept getting in my actual place.

(the first set of numbers should be somewhat random, but after the delay it always gives the same number)

math.randomseed() is global, but I do not know of any ROBLOX scripts that would set it to a constant.

It’s the same for all LocalScripts only I thought, I think CoreScripts have their own math environment.

Okay I also get that number for the same code in Test Server, that’s weird.

EDIT: although sometimes it is a different number, but a majority of the time it’s 0.0012512588885159

There is now reproduction instructions in main post, in case you didnt notice already

Great find.

Any estimate on when this bug might be fixed? (just making sure its at least on some bug list somewhere…)

You’re witnessing the behavior of one of the most pathetic C functions, rand().

There is no such thing as math environment - Lua implements math.randomseed/math.random by directly routing them to C stdlib.h rand()+srand() (which is a really poor choice but what can you do).

There are three great implementation decisions that a platform vendor can make when implementing rand()/srand():

  1. Store RNG state as a global, update it in srand() and rand().
    The problem is, if you have several threads all calling rand(), the random state gets intermixed and you may see many threads concurrently generating the same random value, which would be a problem if you’re trying to generate independent streams of data.

  2. Store RNG state as a global, update it in srand() and rand() but hold a mutex during the update.
    This fixes the state intermixing issue - now all state updates are sequenced so that threads calling rand() get different items from essentially a single random stream.
    The problem is, many threads calling rand() end up serializing their execution with a mutex, which reduces the performance of parallel programs that are dumb enough to use rand().

  3. Store RNG state in the thread environment (that is C thread, not Lua thread obviously), update it in srand() and rand().
    This fixes the performance issue and a state intermixing issue - now rand() is very fast since it does not require synchronization, and different threads do not step on each other’s toes so they get different random streams.
    The problem is, now you need to call srand() in EVERY thread before using the random number generator since it only updates the state of the current thread!

1 is used by OSX.
2 is used by Linux.
3 is used by Windows.

Now, Lua code in ROBLOX runs using a task scheduler that has N threads (where N is your core count) and schedules work on these threads. It does not have a concept of affinity - it can schedule a job on any core assuming there is no concurrent work that conflicts with it.

This means that calling math.randomseed() only initializes ONE random seed for one of the threads.

In their infinite wisdom, Microsoft has set the default RNG seed to 1. The state update function looks like this:

return ((result = result * 214013 + 2531011) >> 16) & 32767

Which happens to return 41 on the first call. math.random() then returns 41 / 32767 = 0.00125.

Now, I am kind of surprised that you’re hitting this problem on the server - the server should only create 1 thread, so math.randomseed() should reset the only RNG state that is used.

13 Likes

The way around this mess is for us to reimplement math.randomseed()/math.random() to stop using braindead C functions.

The only crucial implementation decision being:

  • Should random state be local to the Lua VM being used?
  • Should random state be local to the Lua script being ran?
  • Should random state be local to the Lua thread being ran?

All of these are possible and not too hard to implement, with obvious behavior implications. I feel like #2 would make the most sense, although I see value in #1 as well. I believe @UristMcSparks may have some opinions on this.

1 Like

For me the optimal implementation for improved randomness would be a PRNG instance (PRNG.new([seed])) with methods to generate random numbers (floats, integers in range, more can be added later which is good), with convenient default instances available so people dont have to understand what a “PRNG” is (global, per script, per context, whatever).

Then throw a warning whenever math.random() is called without providing line number hehe. No but seriously, if you add your ‘custom’ PRNG instance, how math.random() works doesnt really matter as long as existing functionality isnt broken (and given that it already is pretty broken, thats not much of a constraint is it)

If you decide to just ‘fix’ the lua generator, the best would probably be for the state to be per lua thread, otherwise setting manual seed is pretty useless (if it can just randomly be interfered with by external side effects), at which point theres nothing to fix in the first place (the seed works fine until you yield). (well I guess the default seed being always the same is annoying - cant you fix this by simply setting the seed to some non-constant when a thread starts, though?)

Wait, does that mean the seed for math.random is always 1 on windows unless you use math.randomseed in the same thread?

For me, how it works, is that if I set random seed in a script, then after a wait it gets reset back to 1 (if I understood correctly). Not sure if this is a one-time thing around server start or if its consistent (the repro in first post could be modified to check this)

So if I want seed to work I have to set it, do random things without yielding, and then assume that the seed has changed after I yield (=time passes). So anything that relies on the seed must happen within the same frame from setting it (if I wanted 100% correctness).

Apparently that shouldnt happen online so idk why it works like that.

Thanks for taking the time to write these posts, really interesting.

For myself I would really argue against implementing option 2. Say I have a map generator which uses a bunch of modules, which all call math.random a number of times to generate the map. If my map is seeded and I want it to be the same every time I call it with a particular seed, then it would be convenient if option 1 or 3 were the case, so that I don’t have to set the seed myself in every module. (I assume this is what option 2 meant anyway)

I would personally prefer option 1, as this is how I imagined the random state was working up til now. Option 3 is not so appealing to me, it seems like its behaviour would just be confusing and not very useful.

The problem with option 3 is that it limits determinism only to the cases where you:
A) Dont yield at all (so a one-time operation)
or
B) Dont have any other use of randomness (which is a global restriction, destroying scalability, and worse, with no checks to inform you if you accidentally break the implicit rule through even a single call to math.random() somewhere else)

Fine for replacing math.random(), not fine for a PRNG designed in the past decade or two.

I look at this as independent. We could provide a custom PRNG module - however, it won’t help people who are already using math.random() (which unfortunately is a builtin in Lua). Plus if you really want good random numbers you can just write a PRNG yourself :slight_smile:

So ideally I’d rather fix math.randomseed first, and then if there is still demand for instance-based RNG add that. This is why I’m thinking of scripts as units of isolation - it’s pretty commonplace in ROBLOX, with globals separation etc.

The problem with option 1 is that any other script calling math.randomseed will reset your RNG state. Here’s the issue I ran into some time ago:

I had code like this:

for i=1,100 do
    local npc = Zombie:Clone()
    local x = (math.random() * 2 - 1) * 100
    local z = (math.random() * 2 - 1) * 100
    npc:SetPrimaryPartCFrame(CFrame.new(x, 10, z))
    npc.Parent = workspace
end

All zombies were spawned in the same place, which took me some time to figure out. The problem was that the NPC model had a script that called math.randomseed(os.time()) in it upon startup. What happened is that every time I parented the new model to the workspace, the script ran and reset the randomseed; since os.time() value did not change between invocations since the loop is pretty fast the seed was always reset to the same number at the end of every iteration, and I got the same x/z values on every iteration.

5 Likes

Setting the script as the environment could work to enforce determinism which is my main concern with random numbers. I’m still interested in the idea of a new type or instance so we can support different distribution types or algorithms (although I suppose we could just use setter functions for those).

Could we have it set the seed per lua stack?

I feel that if we were to introduce new RNG APIs we’d definitely go with object-based API - implicit global state is pretty bad no matter how we slice it. That is, I’d rather have new API, than extend Lua’s math.random()/randomseed() - the only reason I’m even suggesting fixing math.random/randomseed is because a lot of people know it and use it.

1 Like