VOLTAreference

Reference / Work in Studio

Run DSP on the GPU

Work in Studio

Open in the workbench →
On this page · 6 sections

On an Apple-silicon Mac, two kinds of work can go to the GPU: the tail of a long convolve reverb, and a modalCloud — a bank of up to 32 768 tuned resonators too wide for one CPU core to render live. The audio stays bit-for-bit what the CPU renders; the GPU only changes how long a block takes. It combines with rendering on several cores (Render on every core).

Turn it on#

  • In Studio: Session → Audio/MIDI I/O… → AUDIO OUT → gpu (shown only where Metal is available). It applies to effect chains built from then on — reopen the song to move the chains it already has. Studio remembers it.
  • For renders and tools: VOLTA_GPU=1 in the environment.
  • In code: volta_engine::gpu::set_enabled(true) (it returns false when no device is usable), before the module whose chains should use it loads.

Rings of thousands of resonators: modalCloud#

hit = patch { source = noise |> lpf 1800Hz, ampEnv = adsr 2ms 35ms 0 40ms }
bell = play hit !"c2 ~ ~ c2" |> modalCloud 36.7Hz 32768 7s { detune = 9, stretch = 0.07, amount = 0.8 }

modalCloud root [modes] [decay] with named detune (cents), stretch, bright, width, seed and amount puts up to 32 768 two-pole resonators under a track. They cluster on the partials of root (root·h^(1+stretch) up to 0.45 × the sample rate); with more modes than partials, each partial is a seeded cluster detuned by up to ±detune. The track's mid, scaled by amount, rings them, and their stereo sum is added to the dry track; zero is a true null. Every knob but amount (which rides a macro) is constant.

On the CPU a 32 768-mode cloud costs about 1.6 ms of a 2.67 ms block — one core is not enough with anything else playing. From 4 096 modes, with offload on, the GPU renders it. The compiler says so: past its load budget, a song whose excess is GPU-eligible banks gets a GpuEligible hint (what the CPU is left with when offload is on) instead of the DspLoad warning.

examples/songs/foundry-of-bells.volta and glass-choir.volta are built on two clouds each.

What moves for a convolver#

A convolution reverb is, per channel, a short direct filter for the first 1 024 taps plus one spectral multiply-add per later 1 024-sample partition of the impulse, summed once per hop (every 1 024 samples). With the GPU on, that sum — every partition but the newest — is computed on the GPU a whole hop ahead, from spectra that already exist. Only impulses of at least 16 partitions (about 0.35 s) move. The reverb effect is an algorithmic tank, not a convolution, and stays on the CPU.

Same audio, every time#

Both GPU jobs compute in exactly the CPU's order, with fused multiply-add and fast math off, so their results are the CPU's to the bit. A render with the GPU on nulls against one with it off (engine/tests/gpu_null.rs, alone and on the render pool). The audio thread never calls Metal — service threads do.

  • Convolver tails (the asynchronous lane) are asked for a hop early; if the GPU has not answered by the hop that needs it, the CPU sums that hop itself.
  • Banks (the synchronous lane) must answer within the block. The bank's thread posts the block, then renders it on the CPU at the same time, checking between groups of 256 modes whether the GPU has answered, and takes whichever finishes first. A block therefore costs about the shorter of the two, and never more than the CPU alone.

Before any bank uses the GPU, a self-test renders a 32 768-mode bank on it for 24 blocks and compares every output and state bit with the CPU; if it fails, every bank stays on the CPU. gpu::lane_s_report() says how it went and how fast the GPU answered, and whether this machine's GPU lets a running kernel see the CPU's writes — the precondition of a lower-latency resident-kernel design, which an M5 Max on macOS 27 does not meet.

A busy or absent GPU therefore costs CPU time, never a glitch or a different sample.

How much it saves#

Measured on an M5 Max, not listened to; µs per 128-frame block.

Banks (engine/examples/deadline-bench.rs, paced like a device on a time-constraint thread; the machine was loaded by other work, so tails are wide), p50 / p99:

LoadCPUGPU
one 8 192-mode cloud416 / 474308 / 473
one 32 768-mode cloud1 654 / 1 841567 / 1 659
foundry-of-bells2 479 / 2 516, 24 blocks late853 / 1 653 on 6 workers, none late
glass-choir2 423 / 2 617, 51 blocks late799 / 1 644 on 6 workers, none late

Convolver tails (engine/examples/gpu-bench.rs, device-paced), mean / p99:

LoadCPU tailsGPU tails
6 tracks × stereo 4 s IR, serial911 / 1 669566 / 1 205
same, 4 render workers453 / 728246 / 447
12 tracks × stereo 8 s IR, 6 workers388 / 978287 / 497

Every run nulled.

Limits#

  • macOS on Apple silicon only; elsewhere everything stays on the CPU, with the same output. The browser renders on the CPU.
  • Convolver tails and track-level modalCloud banks only; banks inside a patch's voices, additive and granular work on the GPU are not built.
  • A bank's GPU round trip is a command buffer per block, about 0.2 ms typically and a few ms in the tail; narrower banks than 4 096 modes stay on the CPU, where they are cheaper.
  • A switch applies to chains built after it.