NablaTensor

September 21, 2026 · View on GitHub

nablatensor.com

CI Maven Central

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.

Adjoint AD, compiled to Vulkan — a barrier note priced with every Greek

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 test is green on a laptop with no GPU, no native library and no incubator flag — the default cpu-jit backend 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.

methodreplayswall clockspeedup
adjoint — value + 5 Greeks, one reverse sweep11.11 s9.7×
central bump — 1 + 2×5 price-only revaluations1110.76 s1.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:

engineprecisionscenarios/s1e10 runruns on
cpufp641.0×10⁶2.7 hscalar JVM reference (the oracle)
cpu-jitfp641.8×10⁶1.6 hgenerated straight-line bytecode kernel, segmented for C2
simdfp644.0×10⁶42 minJDK Vector API, 8×fp64/lane (--add-modules jdk.incubator.vector)
rocmfp321.3×10⁷13 minfused forward+adjoint HIP kernel, HIPRTC-compiled
vulkanfp321.6×10⁷10 minfused GLSL→SPIR-V compute shader, on-device Philox
cudafp32(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

modulewhat
nablatensor-coreADouble, AadRecorder, AadTape, the AadEngine SPI, AadResult, Philox plumbing, the shared CUDA-C tape codegen
nablatensor-engine-cpuCPU 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-simdJDK Vector API replay — opt-in (--add-modules jdk.incubator.vector)
nablatensor-engine-vulkaneverything Vulkan (FFM runtime + tensor backend + replay engine): tape → GLSL→SPIR-V fused compute shader, dispatched through the Vulkan loader
nablatensor-engine-rocmeverything ROCm/HIP: tape → fused HIP kernel, HIPRTC-compiled, for AMD devices
nablatensor-engine-cudaeverything CUDA: tape → fused CUDA kernel, NVRTC-compiled
nablatensor-engine-opencleverything OpenCL: tape → fused OpenCL C kernel (CUDA codegen, rewritten), clBuildProgram
nablatensor-tensorinternal: the tensor-op SPI (ComputeBackend, DeviceBuffer, Shape) the GPU modules implement
nablatensor-opssmoothed STEP/GT/band indicators, N(x) / erf / pow, a macro-form custom-op registry — all in primitive nodes, exact adjoint, every backend
nablatensor-quantEquityMarket + 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-riskRiskFactor, Sensitivities, Portfolio / netting-set composition, NestedAggregation (the FRTB/SIMM √(ΣK² + ΣγSS) engine), CorrelationScenarioEnum, TimeProfile
nablatensor-examplesworked 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)

  1. the payoff/model lambda — Product<M> is a functional interface over any double-only market record; write the valuation in ADouble.
  2. 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.
  3. custom ops — CustomOp.registerUnary(name, macro); the code generators pick it up on every backend.
  4. hooks — Hooks.antithetic(...) / Hooks.ControlVariate.of()...build() wrap a payoff with a transformed draw stream.
  5. model blocks — subclass a *Model step block; override drift() / diffusion().
  6. scenario DSL — ScenarioRunner.run(kernel, base, ScenarioSet.grid(...), …); shocks are data.
  7. aggregation — record per-trade; Portfolio.aggregate() / byNettingSet(); compose buckets and correlations outside the tape.
  8. 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

Compare

License

Apache-2.0. See LICENSE and NOTICE.