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 platform, 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
- +A fill level the strategy computes, on a single-asset universe: ExecutionPrice.custom() naming one of your own signals is evaluated in the kernel, so correct fills cost no throughput
- +Higher timeframes on a single-asset universe: a strategy reading tf("1h") is resampled once on the host and read like any other column
- −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-source data, and higher timeframes on a multi-asset universe
- −Per-venue fees, and exit orders on a multi-asset universe
- −A custom fill level that names a bar column rather than a signal, or one combined with exit orders
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.
Nothing else to install
Since 0.18.0 the standard wheel carries the CUDA kernels on Linux and Windows x86_64, ARM glibc included, so there is no second package to choose between. Platform tags do the sorting, which a package name cannot: pip has no idea whether your machine has a card. macOS and musl stay CPU-only, where the path does not compile. Nothing changes if you have no GPU: the CUDA binary links no NVIDIA library at load time and costs 0.8 MB. One piece is deliberately not in the default install. NVRTC compiles the kernels at runtime, so the card cannot run without it, and it weighs about 180 MB: too much to hand to everyone who types pip install manifoldbt, which is why it sits behind pip install manifoldbt[gpu]. If a CUDA toolkit is already on the machine it supplies NVRTC and you need nothing extra. Miss it and the run falls back to the CPU and tells you which piece was missing, rather than being quietly slow.
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
Explore the rest of the engine
Every capability is documented the same way: what it does, the code that runs it, and the case where it is the wrong tool.
- The Rust Backtesting Engine
A million bars in 22 ms on one core, a grid swept 25x faster than the same backtests one at a time, and where that advantage stops.
- The Expression DSL
Signals declared as composable expressions and evaluated vectorized in Rust: 104 indicators, 38 candlestick patterns, 74 methods, cross-asset references, parameters that sweep.
- The Research Workflow
Parameter maps, stability scores, walk-forward folds, Monte Carlo resampling and look-ahead detection, each answering a question one backtest cannot.
- Execution Realism
Slippage, fees, funding, borrow, partial fills and decision-to-fill delay, each an independent model rather than one flat cost assumption.
- Multi-Asset Backtesting
Rank a universe on every bar and trade the cross-section: nine perpetuals in one call, one shared account, portfolio-level exposure and fees.
- Options Backtesting
Contracts that expire, cash-settle at intrinsic value, and post margin under a named model: a real multi-leg spread, held to expiry, on live Deribit data.
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.