Pro featureCUDA

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.

>>>sweep = mbt.run_sweep_lite(strategy, grid, config, store, device="auto")

Same strategy, same config, same result objects. The device is the only thing that changes.

1,000,000
backtests in 3.3 s end to end, exact, on one RTX 3090
5.3x
a fully loaded 20-core CPU on a 250,000-combination grid
12.9x
Monte Carlo on a 1-minute equity curve, f64 both sides
bit-identical
to the CPU sweep in f64, asserted metric by metric

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.

run_sweep_lite(..., device="cpu") vs device="cuda", 100k bars
00.2s0.4s0.6s0.8s02k4k6k8k10kparameter combinationsCPU fasterGPU fasterthey cross near 3,000 combos0.86s0.30sCPU sweep, 20 coresGPU sweep, RTX 3090
Same strategy, same 100,000 bars. The CPU climbs in a straight line, one combination at a time; the GPU barely moves, because up to a few thousand combinations it is still paying its fixed launch and transfer floor rather than computing. The frame stops at 10,000 to keep the crossing readable: the two lines keep those slopes, and at 250,000 combinations they read 21.1 s against 3.98 s.
GridCPUGPUWinner
100 combinations9 ms221 msCPU 23x
10,000 combinations0.86 s0.30 sGPU 2.9x
250,000 combinations21.1 s3.98 sGPU 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

numerics

Double 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

tested

The 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 carlo

The 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.

On the GPU today
  • +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
Falls back to the CPU, by name
  • 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.

monte_carlo(method="block", n_paths=1_000_000)
Daily equity ~2,700 points00.5s1s1.5sCPU1.36sGPU0.32s4.3x1-minute equity ~3.9M points030min60min90minCPU~82minGPU6.3min12.9x
Both paths are f64 and bit-identical. Each workload has its own scale, since one runs in seconds and the other in hours. The GPU earns its keep on the fine-resolution curve, where the CPU sustains about 205 paths per second no matter how many cores it has, because bootstrapping a 31 MB return array is memory-bound rather than compute-bound. The 1-minute CPU time is extrapolated from a measured small-N rate, since the full run takes hours.

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.