I built a CUDA options pricer mostly to see how hard I could work an RTX 4050. The plan was simple. Write something heavily parallel, run it on the GPU, watch the card do a lot of work. What I got was two kernels that behave very differently, and a clearer sense of what working the GPU hard actually means.

The first kernel does closed-form Black-Scholes pricing. One thread per contract, no communication between threads, nothing depending on anything else. It prices a million contracts in about half a millisecond, roughly 270 times faster than the single-threaded CPU version I wrote as a baseline. That number looks good until you think about what the kernel is doing. Each thread reads a few numbers, runs them through a formula, and writes the result. There is almost no arithmetic involved relative to the amount of data being moved around. So most of that half millisecond goes to reading inputs and writing outputs, and the compute units sit mostly idle. The 270x is real, but it comes from the memory system, not from the math. The card was barely trying.

The second kernel is the one I actually cared about. It prices American options, which are harder than European ones because the holder can exercise early, on any day before expiry. To price that you have to work out, at every point in the option’s life, whether exercising now is worth more than holding on. The standard method (Longstaff and Schwartz, 2001) runs a Monte Carlo simulation of price paths and then walks backward from expiry, fitting a small regression at each step to estimate the value of holding. I run one thread per simulated path, so the simulation itself is very parallel, hundreds of thousands of paths moving at once.

The result was a 7x speedup over the CPU. After 270x on the easy kernel, 7x felt like a letdown, and for a while I assumed I had written something inefficient.

The problem is not the GPU. It is the shape of the algorithm. Walking backward through the option’s life is a sequence of steps, and each step depends on the one after it, so they cannot run at the same time. On top of that, every step needs to solve a tiny 3x3 system of equations to fit the regression. A 3x3 solve is too small to be worth doing on the GPU, so I hand it back to the CPU each time. That means the program runs a burst of work on the GPU, stops, waits for the CPU to do a little arithmetic, hands control back, and repeats, fifty times per contract. While the GPU kernel is actually running it sits in the high 80s for utilization, which is fine. The time is lost in the gaps, in all the stopping and starting and waiting.

So I ended up with two kernels that fail to push the card for opposite reasons. The simple one finishes too fast to stress anything except memory. The complicated one keeps interrupting itself to check in with the CPU. In neither case is the limit the card’s ability to compute, which is the thing I set out to test. The 4050 has plenty of headroom in both. It is just that one workload never asks for it and the other one keeps pausing before it can.

This is the part I did not expect when I started. I had a vague picture of a GPU as a thing you feed work to, and if you feed it enough, the work goes fast. In practice the card is rarely the bottleneck. You hit the speed of memory, or you hit parts of the problem that have to happen in order, long before you hit the limit of the arithmetic. Picking a problem that genuinely saturates the compute turns out to be its own skill, and a closed-form pricer or a sequential Monte Carlo is not it.

The fix for the slow kernel is reasonably clear. The reason I bounce out to the CPU is that the 3x3 solve is awkward to do on the GPU, but awkward is not the same as impossible. If I write the solve directly into the kernel and keep it on the device, the whole backward pass stays on the card and the stop-start pattern goes away. I expect that to recover most of the lost time. That is the next thing I want to try.

None of this made the pricer wrong. It validates against the original Longstaff-Schwartz paper and against a finite-difference reference, and the numbers line up. It just means the project taught me something other than what I went in for. I wanted to find the ceiling of the card. What I found instead was that for two fairly ordinary workloads, the card was never the thing in my way.