Parallel groups (/g_parallel and --workers)
A graph that no longer fits in one core can run its independent parts on several: Clausters can process the children of a marked group in parallel stages derived from the same bus analysis that powers the auto-sorted groups (auto-order.md). The result is the analogue of supernova's ParGroup, except the independence is inferred and verified by the engine instead of promised by the user — a wrong declaration cannot corrupt audio, it just serializes.
Usage
clausters --workers 3 # real-time server, 3 DSP threads
clausters --nrt score.osc out.wav --workers 3 # faster offline renders too
/g_parallel groupID mode # 1 = process children in stages, 0 = strict order
Without --workers (the default) everything stays sequential; /g_parallel is then a no-op flag, accepted and remembered. The flag is schedulable in timed bundles and valid in NRT scores, like /g_sortMode.
The layout that benefits is independent chains: subgroups (voices, mixer channels) under one parallel group, each chain writing its own buses. Each child of the parallel group — synth or whole subgroup — is one unit; units that touch disjoint buses run concurrently.
How stages are formed
Per processing block the engine scans the group's children in order, batching them greedily into a stage while each next unit:
- writes nothing the stage already reads or writes, and
- reads nothing the stage writes.
A conflicting unit closes the stage and opens the next one, so anything that must be ordered (two writers summing into the same bus, a reader after its writer) is automatically serialized — same-bus mixing keeps the child order, exactly like sequential execution. A dynamic unit (a bus index computed by a signal: the engine cannot know what it touches) always runs alone. Nested parallel groups dispatched to a worker run sequentially inside it (parallelism does not nest in v1).
The masks come from the same per-def analysis as the auto-sorted groups (In/Out/ ReplaceOut and the Faust reserved in/out buses, constants and controls; /n_set on a control used as a bus index re-ships the masks), but the copy the scheduler trusts lives in the engine, shipped with each synth — stage safety never depends on network-side state.
Determinism
Parallel rendering is bit-identical to sequential rendering. Stage members touch pairwise disjoint buses and read nothing the stage writes, so their results are independent of worker interleaving; stages and the serialized conflicts preserve child order. tests/parallel.rs pins this down for live engines and NRT renders, including across an /n_set that retargets a bus. Goldens, the RT/NRT sample-identity guarantee and the --workers flag therefore compose: workers only change wall-clock time.
Workers and the real-time path
--workers N spawns N DSP threads at startup. The audio thread (the conductor) publishes each stage with a few atomic stores, the workers and the conductor race through it with an atomic work-stealing cursor, and the conductor spin-waits (bounded, no locks) for completion. Idle workers spin briefly, then yield, then park; waking a parked worker costs one unpark syscall when audio resumes — while audio runs hot they never park. The conductor path allocates nothing (tests/rt_safety.rs covers it, parallel dispatch included).
Measure before reaching for it: cargo run --release --example bench ends with a /g_parallel section (8 chains × 125 sines on disjoint buses) comparing worker counts — on the development machine it peaks around 3× with 3 workers and degrades past the physical core count. A graph that already fits in one core gains nothing; a stage with fewer units than workers caps the speedup at the unit count.