GPU-accelerated parameter sweeps
A parameter sweep is thousands of independent backtests over the same bars, which is exactly the shape a GPU is built for. One keyword moves the grid onto yours, and the results come back identical to the CPU path, bit for bit. A million backtests, exact, in a little over three seconds.
Same strategy, same config, same result objects. The device is the only thing that changes.
One keyword, and the grid moves to the GPU
There is no separate GPU API to learn, no array to hand-manage, no second strategy definition to keep in sync. The sweep you already run takes a device.
import manifoldbt as mbt
# A grid of 30,000 combinations over the same bars: 30,000 independent
# backtests, which is exactly the shape a GPU is built for.
sweep = mbt.run_sweep_lite(
strategy,
param_grid={"fast": range(5, 105), "slow": range(20, 320)},
config=config,
store=store,
device="auto", # "cpu", "cuda", or "auto" (the default)
)
sharpe = mbt.sweep_columns(sweep, "sharpe") # one numpy column, no per-combo objects
best = sweep[int(sharpe.argmax())]auto is the default
It decides per sweep. Small grids stay on the CPU, large ones move to the GPU when a build, a device and a license are all available. The threshold is a thousand combinations, and MBT_GPU_AUTO_MIN_COMBOS overrides it.
No silent downgrade
With no GPU, no Pro license or a CPU-only build, auto is simply the CPU sweep and the results are the same. device="cuda" is the explicit form: it raises rather than quietly running somewhere else, and even a typo in the device string is an error.
The results are ordinary results
Same objects, same 21 metrics, same order as the Cartesian product of your grid. sweep_columns pulls one metric across a million combinations into a numpy array without building a million Python objects.
Where the GPU wins, and where it loses
A GPU is not a multiplier you bolt onto everything. It has a fixed cost to start work and enormous throughput once started, so the honest question is never how fast it is, it is where the two curves cross. Here is that measurement rather than a marketing ratio.
| Grid | CPU | GPU | Winner |
|---|---|---|---|
| 100 combinations | 9 ms | 221 ms | CPU 23x |
| 10,000 combinations | 0.86 s | 0.30 s | GPU 2.9x |
| 250,000 combinations | 21.1 s | 3.98 s | GPU 5.3x |
The flat left half of the GPU line is a floor, not a plateau: about 50 ms of uploads, kernel launches and result building that a two-combination sweep pays in full. The first sweep in a process adds roughly 180 ms more for the CUDA context and the module cache.
Below the crossover the CPU does not merely compete, it wins outright. That is the number most GPU claims leave out, and it is the whole reason device="auto" exists.
Auto switches at a thousand combinations, deliberately earlier than the crossover on this workload. Near the crossing both paths cost about the same, so being wrong there costs milliseconds, while being wrong at either extreme costs 10 to 100x. The threshold was re-checked at 8,800 bars and at 527,000 bars: the crossing point in combinations barely moves with series length.
Bit-identical, and proven metric by metric
Speed you cannot trust is not speed. If a GPU sweep ranked combinations even slightly differently, you would have to re-run every candidate on the CPU to believe the ranking, which costs more than the GPU saved. So the default path is not approximate.
f64, and fused multiply-add off
numericsDouble precision, fused multiply-add disabled, exact division and square root. The GPU evaluates the same arithmetic in the same order at the same precision as the CPU engine, and nothing is reassociated for speed: a few percent of throughput traded for an answer that needs no re-checking.
Asserted on the bits, not on a tolerance
testedThe end-to-end suite compares every reported metric with raw bit equality across SMA and EMA crossovers, threshold entries, RSI, TRIX, custom sizing, named signals and multi-asset universes. Not close: equal.
The same random numbers
monte carloThe GPU bootstrap draws from the same seeded generator as the CPU path, verified against fixed reference streams, so one seed produces the same paths on either device and the per-path returns, drawdowns, percentiles and probability of ruin all match exactly.
Scan mode: rank in fp32, decide in f64
Single precision is an opt-in second gear for the single-asset kernel, and it is honest about what it is: a scan. A signal sitting within about 1e-7 of a decision threshold can flip, so individual combinations do diverge from the exact run.
What survives that is the ranking, which is what a scan is for. On a one-million-combination grid, the overlap with the f64 ranking is 100% at the top 10, the top 100 and the top 1,000, the argmax is identical, and the median relative error is 2.1e-6.
So the workflow is explicit rather than implicit: scan wide in fp32, then settle the shortlist in f64, where the numbers are bit-identical to the CPU. fp32 requires device="cuda", never auto, so you can only get it by asking for it.
# Scan: rank the whole grid in single precision.
scan = mbt.run_sweep_lite(
strategy, wide_grid, config, store,
device="cuda", precision="fp32",
)
# Settle: re-run the shortlist in f64, which is exact and bit-identical
# to the CPU. Ranking in fp32, deciding in f64.
final = mbt.run_sweep_lite(strategy, shortlist, config, store, device="cuda")What runs on the GPU, and what tells you when it does not
Not every strategy shape has a kernel. The dangerous version of that is a sweep that quietly runs on the CPU at a twentieth of the speed while you assume the GPU is busy, so a fallback here always says so, and always says why.
- +Arithmetic, comparisons, boolean logic and if/else over any pointwise expression
- +EMA on any input, SMA, RSI, ROC, z-score, rolling std and rolling sum over a column or a pointwise expression
- +Rolling min and max, and parameter-free scans
- +Exit orders: stop-loss, take-profit and trailing stops on a single-asset universe
- +Multi-asset universes, exogenous columns, and perpetual funding on both
- −Lag and lead, refused on purpose rather than approximated: a missing value and an out-of-range one mean different things to the simulation
- −A windowed indicator nested inside another windowed indicator
- −Scans that carry a swept parameter
- −Cross-sectional operations, multi-timeframe and multi-source data
- −Per-venue fees, and exit orders on a multi-asset universe
UserWarning: gpu-sweep-unsupported: exit orders on a multi-asset
universe, ran on the CPU
>>> sweep[0].profile["gpu_fallback_reason"]
'exit orders on a multi-asset universe'A fallback raises a warning once per sweep and leaves the reason on the result, in profile["gpu_fallback_reason"]. The reasons are stable strings, so you can assert on them in your own tests.
They are also specific enough to act on. Learning that one stop-loss on a multi-asset universe is what moved your grid back onto the CPU is the difference between rewriting the strategy and shrugging at a slow run.
The refusals are deliberate too. A shape that would run almost right on the GPU stays on the CPU instead, because an answer that is nearly the CPU’s is worse than one that is honestly slower.
Monte Carlo, where the resolution actually hurts
The bootstrap has the same shape as a sweep: many independent resamplings of one return series. On a daily equity curve the CPU is already quick and the GPU is a convenience. On a minute-resolution curve the CPU becomes memory-starved, and that is where the gap opens.
Scaled to a study rather than a demo, the difference is what makes the question askable at all: ten million paths over a 1-minute equity curve take about an hour on the GPU against roughly fourteen on the CPU. One is an afternoon of research, the other is a job you schedule and stop iterating on.
Here too the GPU is additive rather than a separate mode. Asking for the trade bootstrap, or for the full paths to be stored, or running without a device at all, simply routes the study back to the CPU, and the numbers that come back are the same ones.
What it takes to turn it on
Three things, and the engine tells you clearly which one is missing.
An NVIDIA GPU
CUDA. Every number on this page was measured on an RTX 3090 with 24 GB. VRAM decides how much of the grid runs in one pass; beyond that the sweep is chunked automatically, which costs throughput rather than correctness.
A CUDA-enabled build
The default wheel from PyPI is CPU-only, so pip install manifoldbt gives you the engine without the kernels. The GPU build is the same engine compiled with CUDA support: get in touch and we will point you at the right wheel for your CUDA version.
Ask for the CUDA build →A Pro license
device="cuda" raises a clear PermissionError without one, and device="auto" simply stays on the CPU. The Community CPU sweep is not throttled in any way: the GPU is an extra path, never a tax on the normal one.
See Pro pricing →FAQ
Sweep the whole map, not a corner of it
A parameter map you can redraw in seconds is a map you actually read. That is the habit the GPU buys, and the one that keeps a strategy from being tuned into a single lucky point.