Parallel Luau Developer Preview

What’s the intended way to install these builds?

1 Like

It’s low priority, I know, so I have no expectations but can the various (safe) CollectionService methods be whitelisted in the next build? Obviously AddTag and RemoveTag aren’t safe to use but the other ones seem like they’d be safe, at least from an outside perspective.

2 Likes

There’s no intended way to install these, but my recomendation would be to store them inside a folder called “BetaBuilds” located at %LOCALAPPDATA%/Roblox/BetaBuilds.

Of course, it’s up to you where you decide to store beta builds.

2 Likes

Did somebody say parallel voxel terrain generation? :stuck_out_tongue:

Just unzip it and run it wherever - doesn’t matter :slightly_smiling_face:

29 Likes

GetTags, GetTagged and HasTag will be unlocked in the next build, whenever that happens.

6 Likes

Yeah. I’m assuming so.

6 Likes

Sometimes during renders it’ll just randomly stop all threads and one of my cores get maxed out and the other ones go idle. Here’s a benchmark with me running 2 threads. Shortly after this it crashed

I also managed to get these weird errors lol

1 Like

Is there a downside to using potentially thousands of actors? In your raytracer demo you used 1 actor per row, why not use 1 actor per pixel?

6 Likes

Likely because if the task is too small, the overhead from setting up and entering into parallel execution for each pixel would be too large for it to give you a meaningful speedup compared to doing it in parallel for each row. You should be able to benchmark this and see what happens.

7 Likes

Is this a way to create parallel “units of work” without spamming Actor instances? The system is cool as-is, but I somehow dislike the idea of creating a few dozen Actors and script instances when trying to dispatch a parallelized task. Stuff like the scanner camera or the voxel generation example you mentioned would be a lot cleaner if they could be done in a single script that just dispatches several parallel threads instead.

5 Likes

The script has to be inside an actor to use that functionality

1 Like

Yes - that’s correct. Transitioning between actors/coroutines takes some time; we’re likely going to improve this in the future, but in any system there’s some dispatch overhead. When I was writing this code I just did the first thing that was going to work reasonably well :slight_smile:

You still need to use Actors to gain parallel execution. Right now there’s no way to run something in parallel without using Actors, and this is for two reasons:

  1. To allow instance modification in the future, we need to be able to scope code that runs in parallel to a specific hierarchy. Without this code that runs “outside” of that hierarchy would only be able to ever read the state of the world.

  2. Somewhat more crucially, we can’t run a function outside of a VM it’s created in, and we can’t run multiple VMs in parallel (this is due to many high performance systems inside the VM such as the garbage collector, not liking concurrent execution). For this reason the only interface we’d be able to provide as a first class would be pretty awkward - e.g. we’ve discussed APIs like task.run but they’d need to take a closure without upvalues, which means you wouldn’t be able to refer to locals from the outer scope, and that significantly limits the usability and makes the feature more surprising.

This may change in the future. For now we hope that the community will experiment with this and come up with different nicer-to-use libraries for ad-hoc parallelism without us having to bake anything into the language, and use Actors + Scripts for entity-based parallelism where the API is pretty intuitive and natural.

8 Likes

If you provide a standalone .rbxl file we’ll be able to look into it, otherwise there’s not much to go on here. Also make sure to try this with the updated builds as maybe we already fixed the problem.

2 Likes

Hi!

One thing about this that immediately “stinks” is that it’s instance-based. My project’s codebase is already quite large, and is only growing; it has scripts organized carefully by “what they do” rather than by what thread I expect them to be on, or what execution order they’re in (it’s all designed such that it doesn’t matter; race conditions are handled by… well… avoiding them, if that makes sense)

Basically, to use the parallelization features (by setting scripts as descendants to actors), I’d need to restructure this codebase entirely, which would probably make it harder to maintain.

Also, it’s just an added layer of clunk. Ideally, at one stage, I’d be able to read the user’s core count and set threads accordingly on a per-script basis, rather than having to have everything parented to certain instances.

Thus, i’m suggesting having “actor” as a script property, that can be changed by scripts upon game startup or script/module initialization (or runtime?).

I’m sure this presents an immense technical challenge and I don’t expect you to “just change it, duh” or anything - I just wanna make sure you’re aware of my perspective (as a dev).

The ability to adjust thread count to the user is important as Roblox runs on a ton of different machines; tailoring the execution architecture to the user presents an interesting way to boost performance, especially for devices without GPUs that are more restricted in usable frame time.

11 Likes

Not sure what you mean about simulation not running but yeah there does seem to be a memory leak - thanks for letting us know.

edit the memory leak has been found and fixed, will post a new build tomorrow.

3 Likes

There’s a massive, and I mean MASSIVE problem with this right now. I’ve been testing this out because I’ve been needing something like this for ages, but when I actually ran it, it destroyed my computer. After some brief testing I’ve narrowed it down to an issue with the ConnectParallel function. For whatever reason, it seems to not be cleaning up any of the threads produced by ConnectParallel, even when I stop playtesting, it seems they just persist, using up massive amounts of memory. At one point I saw this dev preview version using 11 GIGABYTES of my computer RAM. Here’s a screenshot I took when actually intentionally letting it get high so I could prove a point, but it can and will cause your computer to have a bad time.

Here’s a pastebin containing the code I ran:
https://pastebin.com/R7Cb5CTB

EDIT: I think it says Connect in that code, but it was ConnectParallel when I actually ran it

2 Likes

Yes, see the message posted right before yours.

2 Likes

Any source code for this? I can probably throw it together based on the explanation you provided but you seem to have it working fine if the voxel generation is anything to go off of, so it would be great to not have to redo the work.

1 Like

Sure! I was thinking of releasing it as an open source library anyway, though I might still tweak it before then:

Parallel.rbxm (3.4 KB)

And a raycast demo using the library, for reference:

RaytraceWithLibraryDemo.rbxl (27.6 KB)

13 Likes

Actors seem like they’re meant for games that have hundreds of copies of the same script in individual models spread throughout game. Inheriting from Model for this use-case only really makes sense with respect to the server, because clients can’t run LocalScripts in the workspace. Model also comes with baggage like PrimaryPart and LevelOfDetail, which seems a bit arbitrary and unrelated to multithreading itself. I can work with this API for narrow tasks, but it just doesn’t seem ideal.

My game is 100% ModuleScripts, with 1 Script and 1 LocalScript that initialize the game. Every single part or animated model in my game’s map is generated locally on the client (or locally on the server in a Camera) so that I have absolute control over replication.

It’s important that plugins, commands, and other code are able to benefit from multithreading without spamming Actors and Scripts. Would it possible to pass serialized parameters to the task function? Proper usage could be enforced through syntax warnings, and it could be idiomatically similar to pcall(foo, ...):

task.run(function(cframe: CFrame, parts: {BasePart}, offsets: {CFrame})
	-- This "chunk" is equivalent to starting a script inside an Actor, and can't access upvalues.
	
	local partCFrames: {CFrame} = table.create(#parts)
	game:GetService("RunService").Heartbeat:ConnectParallel(function()
		local cframe2: CFrame = cframe * CFrame.Angles(0, os.clock() % (math.pi*2), 0)
		for i, offset: CFrame in ipairs(offsets) do
			partCFrames[i] = cframe2 * offset
		end
		task.synchronize()
		workspace:BulkMoveTo(parts, partCFrames, Enum.BulkMoveMode.FireCFrameChanged)
	end)
end, CFrame.new(0, 16, 0), parts, offsets)

Perhaps upvalues could serialize automatically so long as they are immutable upvalues of simple types. This includes boolean, nil, string, number, most roblox types, and maybe even other functions that can serialize their immutable upvalues in the same way (this could back-propagate and cache each referenced static function for the new thread, which is likely more performant than completely re-requiring all dependencies.) An error could be thrown if an upvalue is of an incompatible type. Serializing table upvalues are where things start to break down; A record type or some way to permanently lock a table might make this usable without confusion, but they’d still need to be serialized.

Would it be possible for a task to explicitly mark itself to run in parallel with a specific hierarchy, hierarchies, or specific instances?
Would we expect any problems from nesting Actors?

25 Likes