NablaTensor
September 21, 2026 · View on GitHub
Adjoint automatic differentiation for quantitative finance on the JVM. Write the valuation in Java once — get price and every Greek from one reverse sweep, on CPU, SIMD or GPU.

Record a Monte-Carlo valuation once in plain Java against ADouble scalars.
NablaTensor flattens it to a tape and replays that tape — millions of scenarios —
on a generated bytecode kernel, on SIMD, or on a GPU, from the same
recording. One forward sweep gives the price; one reverse sweep gives all
first-order Greeks, at roughly the cost of the price alone.
- Apache-2.0, single repo, no CLA.
- On Maven Central — current release 0.2.0; see Install below.
- Clone-and-run.
mvn -o testis green on a laptop with no GPU, no native library and no incubator flag — the defaultcpu-jitbackend is plain Java. - Customizable. Changing a payoff or a model step is a three-line diff, not a fork.
Not a deep-learning framework, a market-data platform, or a certified regulatory-capital product. It computes the numbers a regulation asks for; sign-off is yours.
Install
Published to Maven Central — browse the group at central.sonatype.com. Current release: 0.2.0.
<dependency>
<groupId>com.nablatensor</groupId>
<artifactId>nablatensor-core</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.nablatensor</groupId>
<artifactId>nablatensor-quant</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.nablatensor</groupId>
<artifactId>nablatensor-engine-cpu</artifactId>
<version>0.2.0</version>
</dependency>
Add nablatensor-engine-vulkan / -rocm / -cuda / -opencl / -simd for a
GPU or SIMD backend — every module is the same groupId:artifactId:version
shape and gates itself at runtime whether or not its toolchain is present.
The benchmark
Arithmetic-average Asian call, 252 daily fixings, fp64, 2,000,000 scenarios,
seed 42. One recording; value + 5 Greeks from one adjoint sweep versus the same
five Greeks by central bump-and-revalue (1 + 2×5 price-only replays).
Machine: JDK 25.0.1, Linux amd64, 16 vCPU. Reproduce with the command below — your numbers will differ.
| method | replays | wall clock | speedup |
|---|---|---|---|
| adjoint — value + 5 Greeks, one reverse sweep | 1 | 1.11 s | 9.7× |
| central bump — 1 + 2×5 price-only revaluations | 11 | 10.76 s | 1.0× |
Same 1536-node tape, every backend this box can run — 1e10-scenario Asian
risk run (1,000,000 × 10,000), projected from a 1e6 probe:
| engine | precision | scenarios/s | 1e10 run | runs on |
|---|---|---|---|---|
cpu | fp64 | 1.0×10⁶ | 2.7 h | scalar JVM reference (the oracle) |
cpu-jit | fp64 | 1.8×10⁶ | 1.6 h | generated straight-line bytecode kernel, segmented for C2 |
simd | fp64 | 4.0×10⁶ | 42 min | JDK Vector API, 8×fp64/lane (--add-modules jdk.incubator.vector) |
rocm | fp32 | 1.3×10⁷ | 13 min | fused forward+adjoint HIP kernel, HIPRTC-compiled |
vulkan | fp32 | 1.6×10⁷ | 10 min | fused GLSL→SPIR-V compute shader, on-device Philox |
cuda | fp32 | (no NVIDIA device on this box — see below) | — | fused forward+adjoint CUDA kernel, NVRTC-compiled |
The dev box has no NVIDIA GPU. On a separate Tesla T4 run of a heavier
benchmark — a 252-step down-and-in barrier, 4057-node tape, value + 5 Greeks
(notebooks/engine-benchmark.ipynb) —
cuda does 1.3×10⁷ scenarios/s (20 M in 1.5 s, warm), ~1.4× ahead of the
same T4's opencl path, with the fp32 result reconciling to the fp64 oracle to
~1×10⁻⁵. Not directly comparable to the rows above (different tape, different
machine), but it places cuda in the GPU tier as expected.
All backends reproduce the scalar oracle path-for-path at equal seed
(cpu-jit bit-exact, the rest to reduction/rounding order; rocm/vulkan
price and delta agree to 5 d.p.). See docs/validation.md.
GPU backends gate themselves at runtime — absent a device or its loader they
report unavailable and selection falls back, so mvn -o test stays green on a
bare laptop.
Reproduce:
mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.bench.Benchmarks \
-Dscenarios=2000000 -Dsteps=252
20 lines
import com.nablatensor.quant.*;
import com.nablatensor.engine.Nabla;
EquityMarket market = EquityMarket.atmOneYear(); // S0=K=100, sigma=20%, r=3%, T=1y
try (MonteCarlo<EquityMarket> mc = MonteCarlo.of(Products.asianCall()) // Seam 1: swap this line for any payoff
.market(market)
.steps(252) // or .timeGrid(TimeGrid.of(t1, t2, ...))
.greeks() // value + every first-order Greek
.on("cpu-jit") // or .fastest(), or "simd" / "vulkan"
.build()) {
Nabla.TypedValuation<EquityMarket> p = mc.run(1_000_000, /*seed*/ 42L);
System.out.printf("price %.4f delta %.4f vega %.4f rho %.4f (± %.4f)%n",
p.price(), p.greek(EquityMarket::spot), p.greek(EquityMarket::vol),
p.greek(EquityMarket::rate), p.standardError());
// Seam 2: move the market on the compiled kernel — no re-record, no recompile.
var bumped = mc.run(market.withSpot(101.0), 1_000_000, 42L);
}
p.greeks() returns the gradient as an EquityMarket of the same shape
(p.greeks().spot() is delta); p.greek(EquityMarket::spot) reads one directly.
Run the worked version: VanillaEuropeanGreeks, AsianGreeksBackends,
FrtbCurvatureShowcase, FrtbFullShowcase, SwapThePayoff,
HestonSabrCalibration, MnistMlp in nablatensor-examples.
Docs under docs/examples/: vanilla, Asian, swap-the-payoff,
exotic-models, barrier-digital, rates-fx, multi-output, sabr-calibration,
heston-calibration, mnist-mlp, FRTB curvature; plus
docs/cookbook/custom-ops.md.
Or watch one happen: demo/ has six narrated jshell sessions —
greeks-on-gpu.sh (a barrier note, every Greek from one sweep, 20 M paths on
Vulkan), adjoint-vs-bump.sh (one reverse sweep vs eleven revaluations),
calibrate-a-smile.sh (a SABR fit with an adjoint gradient), and
one-tape-every-backend.sh (one recording replayed on every backend, each
checked against the scalar oracle).
What's in the box
| module | what |
|---|---|
nablatensor-core | ADouble, AadRecorder, AadTape, the AadEngine SPI, AadResult, Philox plumbing, the shared CUDA-C tape codegen |
nablatensor-engine-cpu | CPU replay — a scalar interpreter (the always-available deterministic oracle) and a tape→straight-line JVM bytecode engine via the Class-File API (the LTS-clean default) |
nablatensor-engine-simd | JDK Vector API replay — opt-in (--add-modules jdk.incubator.vector) |
nablatensor-engine-vulkan | everything Vulkan (FFM runtime + tensor backend + replay engine): tape → GLSL→SPIR-V fused compute shader, dispatched through the Vulkan loader |
nablatensor-engine-rocm | everything ROCm/HIP: tape → fused HIP kernel, HIPRTC-compiled, for AMD devices |
nablatensor-engine-cuda | everything CUDA: tape → fused CUDA kernel, NVRTC-compiled |
nablatensor-engine-opencl | everything OpenCL: tape → fused OpenCL C kernel (CUDA codegen, rewritten), clBuildProgram |
nablatensor-tensor | internal: the tensor-op SPI (ComputeBackend, DeviceBuffer, Shape) the GPU modules implement |
nablatensor-ops | smoothed STEP/GT/band indicators, N(x) / erf / pow, a macro-form custom-op registry — all in primitive nodes, exact adjoint, every backend |
nablatensor-quant | EquityMarket + GbmPath + Products (European / Asian / lookback) + MonteCarlo + BlackScholes; ExoticProducts (barrier / digital / cliquet / autocallable), HestonModel · SabrModel · LocalVolModel · HullWhite1F · LmmModel, BasketOption, Hooks (antithetic / control-variate), CurveBootstrap + analytic Jacobian, Calibrator (adjoint-gradient L-BFGS), MultiMetric; the Shock / Scenario / Ladder / ScenarioRunner ladder runner (setInput + replay, no recompile); the BinomialTree lattice convergence check; the one-factor Gaussian-copula credit / CdoTranche pricers |
nablatensor-risk | RiskFactor, Sensitivities, Portfolio / netting-set composition, NestedAggregation (the FRTB/SIMM √(ΣK² + ΣγSS) engine), CorrelationScenarioEnum, TimeProfile |
nablatensor-examples | worked demos (each also a test and a docs page); the com.nablatensor.bench comparison harness above; the com.nablatensor.validate evidence pack (replay on every backend at equal seed, diff vs the oracle, bump cross-check → text report) |
Every GPU backend compiles with or without its toolchain present and gates at
runtime through AadEngine.isAvailable(); none is a build- or test-time
dependency of the cpu-jit path.
The seams (change without forking)
- the payoff/model lambda —
Product<M>is a functional interface over anydouble-only market record; write the valuation inADouble. setInput— market data and model params are re-settable on a compiled kernel (mc.run(shockedMarket, …))..timeGrid(TimeGrid.of(t1, t2, …))swaps the schedule (non-uniform sampling) without touching the payoff.- custom ops —
CustomOp.registerUnary(name, macro); the code generators pick it up on every backend. - hooks —
Hooks.antithetic(...)/Hooks.ControlVariate.of()...build()wrap a payoff with a transformed draw stream. - model blocks — subclass a
*Modelstep block; overridedrift()/diffusion(). - scenario DSL —
ScenarioRunner.run(kernel, base, ScenarioSet.grid(...), …); shocks are data. - aggregation — record per-trade;
Portfolio.aggregate()/byNettingSet(); compose buckets and correlations outside the tape. - backend choice —
.on("cpu-jit" | "simd" | "vulkan" | …)or.fastest(); same tape, same numbers.
Building
Requires JDK 25+ and Maven 3.9+.
mvn -o -q test # green with no GPU, no native lib, no incubator flag
mvn -o -q install
Risk-capital showcases
docs/examples/frtb-curvature-showcase.md— executable shocked-repricing showcasedocs/examples/frtb-curvature-for-beginners.md— illustrated beginner's guide to the calculationdocs/examples/portfolio-aggregation.md·docs/examples/scenario-dsl.md
Compare
docs/compare/vs-bump-and-revalue.md— per-product adjoint-vs-bump tabledocs/compare/vs-finmath.mddocs/compare/vs-quantlib.md