Parallel Luau [Version 2 Release]

How does this work regarding functions and tables, will it be the same as bindables where a table is made immutable and functions cannot be loaded cross-VM

6 Likes

I’m wondering about that as well. One possibility could be to include functions within the SharedTable concept. However, it’s worth noting that the term “SharedTable” might be misleading to some, as it could imply a table that replicates across a network between clients and servers.

5 Likes

Actual W update, however on the topic of having more api’s available to parallel scripts, would it be possible to mark FilterDescendantsInstances from RaycastParams to safe? Right now only way to get around this is to either cripple performance (due to switching from parallel, serial then back to parallel again) or just mark the RaycastParams at the top of the script before any meaningful execution.

Hi @XenoDenissboss1, we found that users generally wanted to add to a filter, so we created a new method to allow this: RaycastParams:AddToFilter

Although we realize that method may not cover all use cases. Does this method help for the use cases you had in mind?

If it doesn’t help, could you provide some more details on the type of use case(s) you would like addressed?

7 Likes

For the Thread-safe APIs, I would love to have:

  • Model:GetBoundingBox
  • Model:GetExtentsSize

If you need to do a calculation using those things from many models, you are unable to do so, like the rendering distance I currently do which I use the origin of the Model by accessing the CFrame returned by GetBoundingBox and then check the distance between the player and that position to parent objects to a specific folder or back to workspace. (Another weird thing we have to do to avoid lag)


Small Issue I Found: SharedTable doesn’t show auto-completion like for a normal table.


Overall great, I barely know anything about parallel execution and the implications it has, but I would really love to have a function that when called runs in parallel kinda like:

task.runParallel(Function) → Yields the current coroutine until it finishes and returns anything I returned from the function.

I truly don’t like having to create an actor and put scripts under it.

13 Likes

Personally ive needed some methods in order to get ray casts to bypass some specific objects (bypassing transparent objects and whatever else). Good example would be one of my own little raycast renderer:


It does support transparent objects along with recursive transparency so i primarily needed the ability to edit the FilterDescendantsInstances straight in parallel as constantly switching around serial and parallel in such performance critical phase will reduce the performance of the whole thing by a ton. I primarily got around this issue by just slightly moving the ray’s start position by like 0.001 studs in their direction to not cause a stack overflow error (since the rays would just keep hitting the same spots indefinitely).
Ive tried using the AddToFilter function and im really not sure if there’s something wrong with my code or if theres something wrong with this function as this isnt the first time ive had problems with various roblox features that just break or act weirdly in parallel. Right now the issue im facing is the fact that it now simply will not display any transparent objects. (It will display a few pixels of the transparent objects for a frame then after it will simply not display anything)

Technically there have been barely any changes and logically there should not be that big of a difference between just casting a ray slightly inside a part to go past its collisions and simply adding said part to a filter so im really not sure since again, ive had issues with other things just in parallel code. (Bindable events would magically transform numbered table entries into strings and setting signal behavior to deferred just causes the whole thing to no longer function)

(Also while the ray cast renderer may not entirely be a thing actually usable for real games, the same code to get rays to bypass transparent objects is currently being used in some other of my projects that are actually designed to be used in a real world scenario.)

9 Likes

@SuperDave6502 Are there any plans to make Terrain:ReadVoxels() and (maybe) Terrain:WriteVoxels() compatible with parallel? Currently they mutex lock, which means they can be used in parallel, but only one thread at a time can use them, ruining the point of using them in parallel :

6 Likes

What is the difference between using the actor messaging feature and using bindable events under the actor other than safety?
Is there a performance benefit, is serialization faster.
Can you send complicated tables with tables as indices, or arrays which contains nil values?

5 Likes

From what testing ive done and ideas ive got in how to use it, i see the situation with bindables and shared tables to be the following:

  • Bindables = 1.25x-2x faster, Less convenient, most likely will use more memory (depends on your setup and workload)
  • SharedTables = 1.25x-2x slower, WAY more convenient, should use less memory (again, depends on setup, but fact is that you can just use a single table shared across like a module while for bindables i see a decent chunk of setups requiring the cloning and sending back and fourth said duplicated data for use)

This is just my observation. Technically maybe you can get away with better performance from SharedTables if you had to fire a lot of bindables back and fourth but thats most likely about it. Could be wrong.

10 Likes

Making GetPivot() safe to call in parallel was definitely a lifesaver. Thanks for the change!

9 Likes

This is really awesome!

The addition of a messaging API will be super helpful for ergonomics, but I’d like to see a way to send messages out of actors as well. For example, an actor “emits” a message that any other actor or non-actor script could listen to.

I’d also love to see some kind of instance locking method, so an actor can “claim” instances and read/write any properties/methods in parallel on the condition that no other actors can do the same until the lock is released.

Super excited to see what comes next for parallel Luau.

4 Likes

@SuperDave6502 This is a big limitation because it prevents sharing many very common data structures. Consider a table mapping Player objects to how many points they have, or mapping a team (where teams are {leader: Player, members: {Player}, name: string} to what objectives they control.

The fundamental promise of Lua, and by extension Luau, is that tables map any value that isn’t nil or 'NaN` to any non-nil value.

6 Likes

This looks extremely promising! One question: how does a server decide how many cores are accessible to it? I’ve done some testing and found that my single player test world only had access to one core server side, which leads me to believe core count is scaled based on player count or historical load?

Any form of documentation on this would be massively appreciated, I’m guessing the algorithm for this relies on a lot of hidden data so even “server core counts are based on your game’s server demands” as an acknowledgement would be helpful.

6 Likes

While it may be inconvenient, I don’t think it’s actually that big of a restriction and is an understandable technical limitation. For your player mapping example, it is not terribly difficult and from what I’ve seen actually more common to use the player’s UserId in place of the Player instance.

7 Likes

So that’s the catch… I suspect the current iteration of SharedTable is more so targeted for convenience, not large scale performance. I have a large data structure with over 100k entries… I’m extremely curious to find out how this will fare for performance versus a regular non-actor table.

4 Likes

It is a big limitation. For example when using player identifier instead of Player, you have to consider that two different Player objects may share the same identifier if the same player rejoins the game. Also consider that some APIs take Player objects (such as firing to clients with a RemoteEvent), and is less typesafe (ignoring the lack of type safety with SharedTable), and that looking up player by user identifier may not return a Player, while a Player is guaranteed to have a user identifier.

Keying by user identifier is fundamentally less sound in many cases.

A Player key is only one common example. There are many more, such as the other one I just mentioned (yes for non-unique group names you’d need an identifier for networking anyway, but that doesn’t mean you want to key by that).

4 Likes

Nevermind, the whole AddToFilter issue was because of some oversights in my code. It works well to be honest. Tho any chance for perhaps a ClearFilter method? Could maybe save some time by just clearing a single one out every time rather than having to make new RaycastParams every time.

Also would it be possible to have GetSharedTable() return nil if the given string wasnt an entry created for it already? Would probably be more convenient than it returning an empty table.

3 Likes

Typically you should be deleting player data from tables when they leave the game in-order to clear up memory anyways, but if there are any cases where you do not, I cannot imagine why the same player rejoining and occupying the same key would be a bad thing.

Further, you can deserialize the UserId back into the Player instance for the API calls you mentioned quite easily with the Players:GetPlayerByUserId() method.

My point here is that while yes, it may be somewhat inconvenient at times, this limitation likely won’t actually prevent most use cases and is probably quite important from a performance standpoint.

My understanding is that parallel actors run in different Luau VMs, so enabling non-primative keys like tables and userdatas (Instances) would require synchronization of the internal identifiers for these datatypes which would create huge overhead if it is even feasible to begin with. Not worth it for convience in my eyes, but who knows, maybe it will be supported down the line.

If you have any further questions or want to continue discussing, feel free to send me a message. I don’t want to clog up this topic too much.

3 Likes

It is significantly slower than just accessing a normal table, but if you compare it to sending a table through a BindableEvent like you previously had to do, the results are a little better. My testing is showing iterating over an entire SharedTable takes approximately the same amount of time as sending it’s normal table equivalent through a BindableEvent.

cc @Fluffmiceter

edit: I should mention that this assumes you are just reading, writing by index, or writing with the SharedTable.increment() function. Using SharedTable.update() takes (understandably) significantly longer, about 10x the other methods.

2 Likes

Much anticipated and needed update: :+1:

I’ve updated my ActorPool module to support the new parallel luau 2 features.


Example

local ActorPool = require(game:GetService("ReplicatedStorage").ActorPool)

local myPool = ActorPool.new {
	baseModule = script.BaseModule,
	poolFolder = script.Actors,
	min = 1,
	max = 2,
	retries = 10,
	retriesInterval = 0
}

local myConnection = myPool:take()

local numsToAdd = SharedTable.new { 2, 4, 6, 8 }
myConnection:run(numsToAdd)
print(numsToAdd["total"]) -- PRINTS: 20

script.BaseModule

return function(numsToAdd)
	local total = 0
	for _,num in numsToAdd do total += num end
	numsToAdd["total"] = total
end
7 Likes

My use case involves only reads so hopefully performance in this case will be addressed with future updates. Maybe it would be best to copy over the data to each actor during loading so each VM can access the table at normal speeds? My concern is with the amount of memory that might consume, right now my level data takes about ~100MB in script memory.

2 Likes