← blog21 min read

The XAD showcase, on NablaTensor: Heston, SABR, XVA and LIBOR

Benchmark Heston, SABR calibration, XVA CVA, and LIBOR swaption kernels across XAD, NablaTensor, and MatLogica AADC.

benchmarksxadmatlogicahestonsabrxvalibormonte-carloautodiffgreeks
Bar chart titled 'Heston MC, on NablaTensor'. Seven bars of gradient time on a log scale, milliseconds, shorter is faster, for the Heston stochastic-vol Monte Carlo benchmark from auto-differentiation/ad-benchmarks: 8 inputs, 100 Euler steps, 10,000 paths. XAD in C++ on one thread with a per-path tape, rebuilt and rerun on referential machine, 36.4 ms, the 1x baseline. cpu-jit plus cache on one thread, 23.1 ms, 1.58x faster. XAD-Codegen, the closed-source backend, shown faded as a different-CPU reference only, 21.2 ms, Xcelerit's own published number, not measured here. simd plus cache on one thread, 17.6 ms, 2.06x faster. Below a divider: simd plus cache on eight threads, 3.83 ms, 9.50x faster. cpu-jit plus cache on eight threads, 3.61 ms, 10.1x faster. MatLogica AADC, Python bindings, eight threads, 1.64 ms, 22.2x faster, fastest of all seven bars. Footer notes: MatLogica AADC is fastest here with Python overhead included, 1.64 ms beats cpu-jit plus cache at eight threads by 2.2x; that number still includes aadc.evaluate's Python call and array-marshaling cost since its C++ core isn't self-serve; XAD-Codegen's bar is Xcelerit's published number on a different CPU, not directly comparable.

Every article in this series so far has run the same seven-spot ladder — a one-step European or a 252-fixing Asian call, against whatever library was under test. That ladder is this project's own invention, not XAD's. XAD's own README doesn't point at anything like it: it points at auto-differentiation/ad-benchmarks, a separate MIT-licensed repository with four different kernels, the ones Xcelerit chose to publish as evidence XAD is "the fastest tape library" — Heston stochastic-vol Monte Carlo, SABR surface calibration, a swap-portfolio XVA CVA, and Mike Giles' LIBOR market model swaption. This article reproduces those four, not the ladder, against NablaTensor's cpu-jit and simd — and, since it's already this series' third look at MatLogica AADC's free Python trial, against that too.

The rules

  1. Same formulas, same problem sizes, as published. Every model in this article is a line-for-line reimplementation of ad-benchmarks/src/{heston,sabr,xva,libor_swaption}.hpp against com.nablatensor.engine.SDouble instead of XAD's xad::adj<double> — same market data, same portfolio, same path counts, same iteration counts. Nothing here is scaled down or simplified from what XAD's own suite runs.
  2. Every quirk kept, none quietly fixed. Three of the four models have a structural oddity worth knowing about before reading the numbers — SABR's "500 iterations" is a fixed perturbation schedule, not a real converging fit; LIBOR's L0[0] is never stochastically evolved by design; XVA's additive rate-shock can blow up the CVA value at these path counts. Each is called out where it matters, and none is patched — the point is reproducing XAD's own published benchmark, not a better-behaved one.
  3. 1 and 8 threads, cpu-jit and simd, draws cached — and XAD rebuilt on referential machine, not quoted from a different one. NablaTensor's -Dnablatensor.crn=on common-random-numbers cache, same as every prior article in this series. XAD is open source and self-serve (git clone + CMake FetchContent, no license, no trial clock), so this article does what the C++ ladder article did: fetches auto-differentiation/ad-benchmarks itself, builds it with XAD enabled and the same -O3 -mavx2 -mfma flags its own CMakeLists.txt uses, and reruns its own main.cpp — FD and XAD numbers below are measured on the same box as every NablaTensor row, same session. XAD-Codegen, CppAD, Adept and autodiff are the one thing this article couldn't rebuild: XAD-Codegen has no public source or self-serve trial, and CppAD/Adept/autodiff were left out of this rebuild to skip dependencies this comparison didn't need. Those four stay ad-benchmarks' own published numbers (Intel Xeon Platinum 8488C) and are labeled "different CPU" everywhere they appear — read as directional context, not as something this article measured.
  4. MatLogica AADC measured the same way, on the same machine — but only through its Python bindings. pip install aadc is the same free trial an earlier article used, with no built-in Monte Carlo engine of its own: each benchmark's per-path kernel is recorded as a scalar computation and aadc.evaluate JIT-compiles and replays it across a numpy-array batch, one array element per path, aadc.ThreadPool(8) for its own parallelism — as it ships, same as every other row. Unlike XAD, MatLogica's C++ core has no self-serve path at all: the pip wheel ships a compiled Python extension with no headers and no library to link against, and AADC's documented C++ API needs an Enterprise or Desk/Machine-Bound license obtained by contacting MatLogica directly. Every MatLogica number below therefore includes aadc.evaluate()'s Python call and array-marshaling overhead on top of the compiled replay itself — the same distinction an earlier article in this series drew for XAD's own Python bindings, before that number turned out to be almost entirely the interpreter's cost, not the library's.
  5. Nothing trusted until it's checked — for both engines. Heston's price and Greek signs, SABR's Hagan formula against the synthetic market data it was generated from, XVA's CVA non-negativity, and LIBOR's 161-wide reverse sweep against a finite-difference bump on three representative inputs — every benchmark prints its own sanity line before the timing numbers, and the MatLogica harnesses run every one of the same checks independently, not just NablaTensor's. None of the tables below should be read without it.

1. Heston stochastic-vol Monte Carlo

Euler-discretized Heston model, European call, 100 time steps, 10,000 paths, 8 differentiable inputs (spot, strike, maturity, rate, initial variance, mean-reversion speed, long-run variance, vol-of-vol; correlation fixed at −0.7). XAD records one tape per path — tape.clearAll(), register 8 inputs, newRecording(), price, computeAdjoints(), accumulate — the same idiom the C++ ladder article used. NablaTensor records the same per-path kernel once as a Nabla.model, batches the 10,000 paths through scenarios(10_000), and lets cpu-jit/simd compile and thread the replay.

Bar chart titled 'Heston MC, on NablaTensor'. Seven bars of gradient time on a log scale, milliseconds, shorter is faster. XAD one thread, same machine, 36.4 ms (1x baseline). cpu-jit plus cache one thread 23.1 ms (1.58x). XAD-Codegen, faded, different CPU, 21.2 ms. simd plus cache one thread 17.6 ms (2.06x). simd plus cache eight threads 3.83 ms (9.50x). cpu-jit plus cache eight threads 3.61 ms (10.1x). MatLogica AADC, Python bindings, eight threads, 1.64 ms (22.2x), fastest of all seven.
enginethreadsmachineprimalgradient (value + 8 Greeks)
FD1referential machine7.43 ms67.45 ms
XAD1referential machine7.43 ms36.39 ms
XAD-Codegen (ad-benchmarks)1different CPU9.25 ms21.20 ms
CppAD (ad-benchmarks)1different CPU9.27 ms268.26 ms
Adept (ad-benchmarks)1different CPU9.27 ms90.69 ms
cpu-jit + cache1referential machine9.45 ms23.07 ms
cpu-jit + cache8referential machine9.49 ms3.61 ms
simd + cache1referential machine7.06 ms17.63 ms
simd + cache8referential machine7.20 ms3.83 ms
MatLogica AADC (Python)8referential machine1.01 ms1.64 ms

Price at spot 100 / strike 105: NablaTensor 7.835519, all four configurations agreeing to every printed digit (same seed, same Philox stream, different code generator); MatLogica 7.555955, a different draw stream (numpy default_rng, not Philox, not mt19937) landing in the same economically sensible range. The Greeks agree closely across both implementations — delta 0.5995 (NablaTensor) / 0.5934 (MatLogica), vega-like d/dv0 42.95 / 41.99, d/dtheta 59.66 / 56.90, d/dxi −1.84 / −1.88 — three independent implementations of the same published formula landing on the same numbers, which is the point of checking at all: no bit-exact draws were ever expected across three different random number generators.

MatLogica is fastest here, Python overhead and all. 1.64 ms beats NablaTensor's best (cpu-jit + cache at 8 threads, 3.61 ms) by 2.2x, and XAD's tape by 22x. That number is the whole Python round trip — building the input dict, calling aadc.evaluate, unpacking the result — not a bare measurement of AADC's compiled kernel, since there's no self-serve way to strip that overhead out and measure the C++ core alone. That it still wins by this much says the compiled replay underneath is doing something seriously fast; it doesn't say how much faster a pure-C++ AADC call would be.

cpu-jit beats XAD's own tape on a single thread, before any threading helps — and this is the same machine, not a cross-CPU guess. 23.07 ms against XAD's 36.39 ms, rebuilt and rerun right here, is 1.58x; simd at one thread is 2.06x. This isn't a batching story yet — one thread, no scenario parallelism engaged beyond what the engine already does per call — it's compiled replay against a per-path tape recorded fresh 10,000 times. XAD's own number on this box (36.39 ms) is actually a little faster than Xcelerit's published one (39.81 ms on their Xeon) — a single-thread workload like this one rewards a high boost clock more than a server chip's extra cores, worth remembering before assuming bigger hardware always wins. XAD-Codegen's 21.20 ms is Xcelerit's own number and stays un-rebuildable (no public source, no self-serve trial) — the chart shows its bar faded for exactly that reason, a reference point rather than a measurement. At 8 threads NablaTensor doesn't need that comparison to be close anyway: 3.61 ms is 5.9x faster than Codegen's published number, whichever CPU either one ran on.

2. SABR surface calibration

Hagan 2002 implied-vol formula, 5 expiries fit simultaneously (alpha, rho, nu per expiry, 15 inputs total), 100 strike/expiry points, wrapped in ad-benchmarks' own "500-iteration optimizer loop" — a fixed cosine perturbation schedule applied to the parameters each iteration, not a real converging fit. What's timed is 500 sequential gradient evaluations. There is no Monte Carlo anywhere in this benchmark — the entire tape is deterministic, which matters for reading the chart below.

Bar chart titled 'SABR calibration, on NablaTensor'. Seven bars of gradient time for the 500-iteration loop, log scale, shorter is faster. simd plus cache eight threads 171.9 ms (23.5x slower than baseline). simd plus cache one thread 142.3 ms (19.5x slower). MatLogica AADC, Python bindings, eight threads, 87.9 ms (12.0x slower). cpu-jit plus cache eight threads 15.1 ms (2.07x slower). XAD one thread, same machine, 7.31 ms (1x baseline). cpu-jit plus cache one thread 6.61 ms (1.11x faster). XAD-Codegen, faded, different CPU, 4.90 ms. Footer: the one benchmark where threads and batching hurt everyone who has them, since there is nothing to parallelise across a single deterministic 15-input tape; MatLogica pays the same tax as simd, worse, 500 separate Python calls into a compiled kernel; cpu-jit at one thread still edges out XAD's tape, same machine.
enginethreadsmachineprimal (500 iters)gradient (500 iters)
FD1referential machine1.95 ms30.93 ms
XAD1referential machine1.86 ms7.31 ms
XAD-Codegen (ad-benchmarks)1different CPU2.12 ms4.90 ms
CppAD (ad-benchmarks)1different CPU2.12 ms32.03 ms
Adept (ad-benchmarks)1different CPU2.10 ms18.67 ms
autodiff (ad-benchmarks)1different CPU2.10 ms38.09 ms
cpu-jit1referential machine12.68 ms6.61 ms
cpu-jit8referential machine21.52 ms15.11 ms
simd1referential machine77.67 ms142.28 ms
simd8referential machine93.81 ms171.88 ms
MatLogica AADC (Python)8referential machine79.65 ms87.88 ms

(SABR has no Monte Carlo, so there's no -Dnablatensor.crn cache to turn on — every row here is the same "cached" concept trivially, since nothing is random.)

Hagan's formula at the true parameters reproduces the synthetic market vols it was generated from to 0.000e+00 absolute error, and the adjoint gradient of d(objective)/d(alpha_0) matches a finite-difference bump to six decimal places (6.037100 both ways) in both implementations — MatLogica's independent Python recording lands on exactly the same 6.037100, since this benchmark has no randomness to disagree about, only the same formula computed twice by two different AD systems. The implementation is correct in both before any of the timings above are trusted.

This is the one benchmark where NablaTensor's design doesn't help, and MatLogica's actively hurts. cpu-jit at one thread is still 1.11x faster than XAD's tape, rebuilt and rerun on this same machine — a real, if narrow, edge, since NablaTensor is compiling and replaying a kernel rather than recording an object graph fresh each time. But eight threads make it slower (15.11 ms, worse than one thread), because there are 500 sequential, data-dependent iterations to run and nothing to split across cores — the perturbation schedule doesn't depend on the previous iteration's result, but the harness runs it as XAD's own does, one call after another. simd is worse again, by an order of magnitude in both directions: its batched-replay design has a fixed per-call setup cost that never gets to amortize when every call is a batch of exactly one scenario. MatLogica pays the same tax and then some: 87.88 ms for the full loop is 12.0x slower than XAD's tape, because every one of the 500 iterations is a separate aadc.evaluate() Python call into a kernel that itself runs in microseconds — the batch that would let MatLogica's compiled replay show what it can do (as it did on the three Monte-Carlo benchmarks) is a batch of exactly one call, 500 times over, and Python's own per-call cost dominates every single one. XAD-Codegen's published 4.90 ms would be the fastest thing on this chart, but it's Xcelerit's own number on a different CPU, not something rebuilt here — the only same-machine, self-serve comparison available, cpu-jit at one thread against XAD's tape right here, is a narrow NablaTensor win.

Did you know? The Hagan formula being calibrated in this section — SABR's asymptotic expansion for Black-implied volatility — comes from a single 2002 Wilmott paper, Managing Smile Risk, by Hagan, Kumar, Lesniewski and Woodward. Two decades and a great deal of fully-numerical competition later, it's still the default vol-smile interpolation on most trading desks — not because nothing more accurate exists, but because "closed-form, one line, differentiable by inspection" keeps winning against alternatives that need a solver of their own just to produce the number this formula gets in a dozen arithmetic operations.

3. XVA CVA

A 15-swap interest-rate portfolio, credit valuation adjustment over 20 semi-annual time buckets, 10,000 Monte-Carlo paths, 40 differentiable market inputs: 20 zero-curve points, 10 hazard-curve points, 10 vol-term-structure points. The heaviest of the four kernels — ad-benchmarks estimates 5,000–10,000 operations per path.

Bar chart titled 'XVA CVA, on NablaTensor'. Seven bars of gradient time, log scale, shorter is faster. XAD one thread, same machine, 2.17 s (1x baseline). simd plus cache one thread 1.42 s (1.53x faster). cpu-jit plus cache one thread 1.24 s (1.75x faster). simd plus cache eight threads 728 ms (2.98x faster). XAD-Codegen, faded, different CPU, 572 ms. cpu-jit plus cache eight threads 278 ms (7.80x faster). MatLogica AADC, Python bindings, eight threads, 55.2 ms (39.3x faster), fastest of all seven. Footer: MatLogica is fastest on the heaviest kernel of the four, 55.2 ms is 5.0x cpu-jit plus cache at eight threads; the published toy model's additive rate-shocks produce an astronomically large CVA value at this path count for every engine measured, MatLogica included, explained in the article, not a bug.
enginethreadsmachineprimalgradient (value + 40 sensitivities)
FD1referential machine505.3 ms20.75 s
XAD1referential machine516.3 ms2.169 s
XAD-Codegen (ad-benchmarks)1different CPU591.0 ms571.9 ms
CppAD (ad-benchmarks)1different CPU590.3 ms7.972 s
Adept (ad-benchmarks)1different CPU590.1 ms7.076 s
cpu-jit + cache1referential machine675.3 ms1.239 s
cpu-jit + cache8referential machine683.4 ms278.1 ms
simd + cache1referential machine288.7 ms1.416 s
simd + cache8referential machine282.7 ms728.3 ms
MatLogica AADC (Python)8referential machine19.0 ms55.2 ms

MatLogica is fastest on the heaviest kernel of the four, Python overhead included. 55.2 ms is 5.0x cpu-jit + cache at 8 threads (278.1 ms) and 39.3x XAD's tape, rebuilt and rerun right here. This is the benchmark where a real Monte-Carlo batch (10,000 paths, ~5,000–10,000 ops each) gives a compiled, vectorized replay the most room to work with, and both MatLogica and NablaTensor's cpu-jit show it: cpu-jit at 8 threads already beats XAD-Codegen's published 571.9 ms — on a CPU this article can't verify, since XAD-Codegen has no public source or self-serve trial — by 2.06x, and against XAD's own tape, same machine, cpu-jit at one thread is already 1.75x faster and simd at one thread 1.53x, before 8 threads pull cpu-jit to 7.80x.

Now the caveat this benchmark needs — and it holds for every engine measured, MatLogica included, which is itself useful confirmation. ad-benchmarks' additive rate-shock model re-applies the same per-path Gaussian draw at all 20 time buckets — the shock array is generated once per path and never redrawn between buckets, so it compounds linearly rather than as a random walk with independent increments. Over the 20,000 independent draws behind 10,000 paths x 20 rate points, a handful will land far enough in the tail that, multiplied by 20 buckets, a curve point random-walks to something like −13. Because exp(-rate * t) sits inside the swap discount factor, that single path's exposure comes out on the order of 10^40+, and dominates the mean — NablaTensor's CVA value is 5.8399 × 10^43; MatLogica's, an entirely independent Python implementation with its own numpy random stream, lands at 2.6319 × 10^44 — a different draw sequence finding a different worst-case path, same astronomical order of magnitude, both correctly computed and both completely uninterpretable as a real risk number. Confirmed a third way, outside either implementation entirely, with a standalone Python re-implementation of just the diffusion loop: the worst curve point over 20,000 trials at these parameters reaches roughly −13, and exp(13 × 10) alone is already 10^56-scale. ad-benchmarks never prints or checks the CVA value itself — only the timing — so this is a property of the published toy model that its own authors would have no reason to have noticed. The gradient sanity check (CVA >= 0) still holds in both implementations, because every term in the sum is non-negative by construction; it just holds at an astronomical scale. Read this scenario as a computational-load benchmark, the same as ad-benchmarks itself does, not as a real CVA methodology — the SA-CVA article and the CVA capital showcase in the main engine repo are where a real, bounded exposure model lives.

4. LIBOR market model swaption

Mike Giles' LIBOR market model benchmark (testlinadj.cpp), by way of ad-benchmarks/src/libor_swaption.hpp: 15 swaptions, 80 forward LIBOR rates, 10,000 paths, 161 differentiable inputs — 1 accrual period plus 80 initial rates plus 80 vols. This is the benchmark Giles & Glasserman's Smoking Adjoints exists to justify: a naive one-input-at-a-time bump needs 162 evaluations per gradient, so the whole point is getting every adjoint out of a single reverse sweep.

Bar chart titled 'LIBOR swaption, on NablaTensor'. Seven bars of gradient time, log scale, shorter is faster. XAD one thread, same machine, 753 ms (1x baseline). simd plus cache one thread 646 ms (1.17x faster). cpu-jit plus cache one thread 531 ms (1.42x faster). simd plus cache eight threads 312 ms (2.41x faster). XAD-Codegen, faded, different CPU, 305 ms. cpu-jit plus cache eight threads 89.9 ms (8.38x faster). MatLogica AADC, Python bindings, eight threads, 42.8 ms (17.6x faster), fastest of all seven. Footer: MatLogica wins the widest-input benchmark too, 42.8 ms beats cpu-jit plus cache at eight threads by 2.1x; the 161-wide reverse sweep is checked against a finite-difference bump on delta, L0[0] and lambda[0] for both engines before any timing is trusted.
enginethreadsmachineprimalgradient (value + 161 sensitivities)
FD1referential machine106.5 ms17.24 s
XAD1referential machine107.5 ms752.9 ms
XAD-Codegen (ad-benchmarks)1different CPU139.8 ms305.3 ms
CppAD (ad-benchmarks)1different CPU138.0 ms4.572 s
Adept (ad-benchmarks)1different CPU138.2 ms1.155 s
cpu-jit + cache1referential machine210.2 ms531.1 ms
cpu-jit + cache8referential machine208.4 ms89.9 ms
simd + cache1referential machine128.2 ms645.8 ms
simd + cache8referential machine121.5 ms312.4 ms
MatLogica AADC (Python)8referential machine11.7 ms42.8 ms

MatLogica wins the widest-input benchmark too, and by the smallest margin of the three Monte-Carlo scenarios. 42.8 ms, Python overhead included, beats cpu-jit + cache at 8 threads (89.9 ms) by 2.1x — still a win, but the narrowest of the three, against NablaTensor's strongest showing of the article. cpu-jit + cache at 8 threads is itself 8.38x XAD's own tape, rebuilt and rerun right here (752.9 ms) — the widest input count is where NablaTensor's compiled, batched replay pulls furthest ahead of XAD's per-path recording. XAD-Codegen's published 305 ms would still be ahead of cpu-jit at one thread (531.1 ms), but it's Xcelerit's number on a CPU this article never touched; both cpu-jit at 8 threads and MatLogica beat it regardless of that caveat.

One detail worth being precise about, because it looks like a bug and isn't: L0[0] — the very first forward rate — is never touched by the stochastic path-generation loop. Giles' evolution only updates L[n+1..] at time step n, so L[0] stays pinned at its initial value for the entire path. It still has a real, non-zero gradient, because the final discount-back loop divides through by every one of L[0..N-1], L[0] included. NablaTensor's sanity check catches exactly this: d(price)/d(L0_0) comes out to −0.383149 by adjoint and −0.383149 by a finite-difference bump under common random numbers, agreeing to six decimal places, alongside delta (203.009482 both ways) and lambda[0] (0.046194 both ways). MatLogica's independent Python implementation, different draws entirely, checks the same three inputs and agrees with itself just as tightly: delta 202.195978 adjoint against 202.196005 finite-difference, L0_0 −0.381923 both ways, lambda_0 0.040311 both ways. Two unrelated AD systems, two unrelated random number generators, the same algorithm reproduced exactly rather than "fixed" — and both pass the check that matters most for a 161-input reverse sweep.

The pattern across all four

Batching is the whole story, and MatLogica makes the case even harder than NablaTensor does. Heston, XVA and LIBOR are all Monte Carlo — 10,000 independent scenarios that a compiled, vectorized replay can chew through in one call. On all three, cpu-jit already beats XAD's own single-threaded tape — rebuilt and rerun on this same machine, not quoted from a different one — before any threading (1.58x, 1.75x, 1.42x), and at 8 threads that same same-machine comparison reaches 7.8x to 10.1x. MatLogica goes further still: its Python bindings, call overhead included, beat every other engine measured on all three, by 2.2x, 5.0x and 2.1x over cpu-jit's own best 8-thread numbers. SABR has no Monte Carlo — one deterministic 15-input tape, 500 sequential evaluations — and there cpu-jit at one thread is a narrow 1.11x ahead of XAD, same machine, adding threads makes it slower, simd loses outright to a tape library with no batching of its own at all, and MatLogica loses worse than either: 12.0x behind XAD, because 500 separate Python calls into a kernel that runs in microseconds pay Python's own per-call tax 500 times over. Same shape of engine, same batching design, opposite verdict on opposite sides of "is there a batch" — for NablaTensor and for MatLogica alike.

simd is consistently behind cpu-jit on all four benchmarks measured here, which is a different result from the equity-option ladder articles earlier in this series, where simd + cache was the fastest engine measured. The four kernels in this article are heavier per path (100 Euler steps with two correlated draws, a 20-bucket nested swap valuation, an 80-rate LIBOR evolution) and narrower in path count (10,000 against the ladder's 20,000,000), a different point on the tradeoff simd's wider-but-fixed-cost vectorization makes against cpu-jit's generated scalar code — worth its own article rather than a guess here. MatLogica's position swings the widest of any engine measured: fastest of all seven rows on three benchmarks, second-slowest on the fourth, which is what a compiled, batch-oriented replay wrapped in a thin Python layer looks like when the batch disappears.

Did the RNG difference matter?

XAD seeds std::mt19937; NablaTensor seeds its own Philox counter-based stream; MatLogica has no RNG of its own, so this harness seeds numpy's default_rng. Three generators, three implementations, and nobody should expect the same 10,000 paths' worth of draws from any pair of them — this article never claims bit-exact agreement. What's checked instead is that each implementation's own internal consistency holds, in both NablaTensor and MatLogica: SABR's Hagan formula reproduces its own synthetic market data exactly in each, to the same 6.037100 adjoint gradient; Heston's price and Greeks have the signs a call should have and land within a few percent of each other (7.835519 vs 7.555955, different draws); XVA's CVA is non-negative by construction in both, at the same astronomical order of magnitude; and LIBOR's 161 adjoints agree with finite differences to six decimal places under common random numbers, independently, in each implementation. That is the correctness bar this series has used throughout, and it is the right one here too — the tables above compare compute cost, not whether three different random number generators happened to draw the same numbers.

Try it

Everything — the NablaTensor harness, the MatLogica scripts, and the exact commands that rebuilt XAD — is in bench/xad-showcase-on-nablatensor on the main engine repo, not this site's own repo: one Java class per benchmark plus a shared warmup-then-median timing harness under nablatensor/, the four MatLogica scripts alongside it, and the log behind every table above.

git clone https://github.com/nablatensor-dev/nablatensor
cd nablatensor && git checkout bench/xad-showcase-on-nablatensor
cd bench/xad-showcase-on-nablatensor/nablatensor
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
javac --add-modules jdk.incubator.vector -cp "$(cat cp.txt)" -d out *.java

java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
    -Dengine=cpu-jit -Dthreads=8 -Dcached=true HestonBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
    -Dengine=simd -Dthreads=1 SabrBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
    -Dengine=cpu-jit -Dthreads=8 -Dpaths=10000 XvaBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
    -Dengine=simd -Dthreads=8 -Dpaths=10000 LiborBench

The XAD/FD numbers in every table above aren't quoted from ad-benchmarks' own published results — they're a straight rebuild of the actual upstream repository, on this same machine, same session:

git clone https://github.com/auto-differentiation/ad-benchmarks
cd ad-benchmarks
cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
    -DENABLE_XAD=ON -DENABLE_CPPAD=OFF -DENABLE_ADEPT=OFF -DENABLE_AUTODIFF=OFF
cmake --build build -j"$(nproc)"
./build/ad_benchmarks   # 10,000 paths, 10 iterations, 3 warmup — its own defaults

XAD-Codegen, CppAD, Adept and autodiff stay ad-benchmarks' own published numbers, because XAD-Codegen needs a commercial license this article doesn't have and the other three were left disabled to skip dependencies this rebuild didn't need — every one of them is labeled "different CPU" everywhere it's quoted.

MatLogica's four scripts sit next to the Java ones, one level up:

cd bench/xad-showcase-on-nablatensor
uv venv --python 3.12 venv-aadc && source venv-aadc/bin/activate
uv pip install aadc numpy
python heston_matlogica.py
python sabr_matlogica.py
python xva_matlogica.py
python libor_matlogica.py

pip install aadc activates a free trial silently, no account and no terms text referential machine could find, 48 days left when this ran. As covered above, this measures the Python bindings only: AADC's C++ API is real and documented, but needs a license obtained by contacting MatLogica directly, not a pip install.

ad-benchmarks itself is MIT-licensed (Xcelerit Computing Ltd.); nothing here copies its C++, only its published formulas, problem sizes and methodology — median of the timed samples after a discarded warmup, same as its own src/timing.hpp. The LIBOR model is originally Mike Giles', reproduced by way of ad-benchmarks' own adaptation of it.


Questions or corrections? open an issue