Benchmark on interest rate swap portfolios under a Hull-White model with CSA collateral rules. AADC computes prices and all 532 CVA/DVA sensitivities for 0.6–0.8× the CPU time of one plain pricing run; bump & revalue needs 533 runs.
| Parameter | Value |
|---|---|
| Model | Hull-White 1-factor with mean reversion |
| Instruments | Interest rate swaps (fixed + floating legs) |
| Collateral | CSA rules with threshold and minimum transfer amounts |
| Time horizon | 30 years (10,950 days) |
| Model step | 30 days (365 steps) |
| Pricing frequency | Every 3rd model step (122 pricing times) |
| Risk factors | 532 (r0, sigma, 250 MR curve, 140+140 survival curves) |
| Monte Carlo paths | 10,000 (same random numbers for every run) |
| Hardware | Intel Xeon Platinum 8280L, AVX2 (4 paths per instruction), 5 threads |
Note: This is an isolated benchmark example using a single-factor Hull-White model with interest rate swaps. Production XVA libraries typically involve additional complexity — multi-factor models, exotic instruments, cross-currency exposures, MVA/KVA, and regulatory constraints — all of which are well-understood by MatLogica. Production deployments also benefit from additional optimisations (kernel partitioning, incremental recompilation, warm caching across trading desks) not exercised here. As a result, real-world speedups will vary by implementation, but remain comfortably in the 10× and above range even for the most complex portfolios.
Full sensitivity set — 532 risk factors, all computed simultaneously
N+1 full MC re-simulations for N risk factors. Each sensitivity requires a complete Monte Carlo reprice.
1 forward + 2 reverse sweeps (CVA, DVA) per MC path. All 532 gradients computed simultaneously — cost independent of N.
Log scale — includes all CVA + DVA Greeks
| Phase | 5 Trades | 50 Trades | 200 Trades |
|---|---|---|---|
| Kernel compilation (one-time) | 1.20s | 2.38s | 5.48s |
| Evaluation (prices + 532 Greeks) | 0.98s | 5.88s | 20.8s |
| Cost vs one pricing run (same cores) | 0.62× | 0.73× | 0.80× |
Cost vs one pricing run = AADC evaluation (forward + 2 reverse sweeps, 5 threads) divided by one double pricing run spread over the same 5 threads. Values below 1× mean prices plus all 532 sensitivities cost less than a single repricing. 10,000 paths, 5 threads.
| Portfolio | Primal (1 run, 1 thread) | 533 Runs (est.) |
|---|---|---|
| 5 trades | 7.94s | ~14.1 min |
| 50 trades | 40.3s | ~71.6 min |
| 200 trades | 129.8s | ~3.8 hr |
Estimated total = (532 + 1) × one measured pricing run, divided across the same 5 threads as AADC with perfect parallel scaling. Six real bumps per portfolio were timed: each cost 0.97–1.05× one pricing run. Cost grows linearly with the number of risk factors.
Bump & revalue cost grows linearly — AADC stays constant at 0.8× one pricing run (200 trades, same 5 threads)
Warm run: When only market data changes (rate curves, survival probabilities), AADC reuses the compiled kernel — 0s compilation overhead. For 50 trades at 10K paths: cold start 8.2s (including 2.5s compilation) → warm run 5.8s (1.4× faster).
AADC produces mathematically exact derivatives — to machine precision
| Metric | AADC | Bump & Revalue (1e-8) |
|---|---|---|
| CVA match to primal | 8.1e-14 or better | N/A |
| DVA match to primal | 1.2e-13 or better | N/A |
| dCVA/d(sigma) agreement | Reference | 1.4e-6 to 3.3e-5 |
| dCVA/d(r0) agreement | Reference | 2.3e-8 to 1.1e-6 |
Compute CVA/DVA + all 532 sensitivities for the entire portfolio.
Rate curves or survival probabilities change — re-compute CVA + sensitivities.
A new trade of a type already in the portfolio is added.
aadc::idouble replaces double directly — no templates required
double rate = 0.05; double price = notional * exp(-rate * T); double cva = max(price, 0.0) * (1.0 - survival_prob);
aadc::idouble rate = 0.05; aadc::idouble price = notional * exp(-rate * T); aadc::idouble cva = max(price, 0.0) * (1.0 - survival_prob);
double → aadc::idouble in the valuation path
startRecording() / stopRecording() around computation
markAsInput() for each risk factor, markAsOutput() for outputs
forward() + reverse() → all sensitivities
No refactoring needed. aadc::idouble overloads all arithmetic operators and math functions.
The template approach (XVAProblem<T>) used in this benchmark is an engineering convenience for maintaining both code paths side-by-side — it is not an AADC requirement.
Full source from the benchmark — same business logic runs with double or aadc::idouble
// XVAProblem.h, same code for both double and aadc::idouble template<typename T> class XVAProblem { public: void simulatePath(const std::vector<T>& random_vec) { m_model->initT0(m_model_times[0]); HullWhiteIrMarket<T> market = m_model->getIrMarket(); m_CSA_object->initCollateral(); for (int i = 0; i < m_model_times.size(); i++) { if (i > 0) m_model->nextQT(random_vec[i], m_model_times[i]); if (m_is_pricing[i]) { market = m_model->getIrMarket(); m_total_price = 0; for (int ti = 0; ti < numTrades; ++ti) { m_total_price += m_fixed_leg[ti]->getPrice(market, t); m_total_price += m_float_leg[ti]->getPrice(market, t); } m_CSA_object->next(m_total_price); T CSA_total_price = m_total_price - m_CSA_object->getCollateral(); m_PEE[price_time_index] += max(CSA_total_price, 0.); m_NEE[price_time_index] += min(0., CSA_total_price); } } } void computeXVAMeasures() { // Trapezoidal rule for CVA/DVA integration for (int i = 0; i < i_end - 1; i++) { m_CVA += (m_PEE[i] + m_PEE[i+1]) * (ctrparty_prev - ctrparty_next) * 0.5; m_DVA += (m_NEE[i] + m_NEE[i+1]) * (company_prev - company_next) * 0.5; } } };
The template parameter T is the only difference. Supporting classes (HullWhiteMarketModel, CSARules, FixedLeg, FloatLeg) are all templated on T.
// XVAJobRequest.h, bumpAndRevalue() void bumpAndRevalue(const json& request_data) { double bump_size = 1e-8; // For EACH mean reversion curve point (250 points): for (int i = 0; i < xva.getModel()->getMeanRev()->getVals().size(); i++) { json bump_data(request_data); bump_data["Currencies"]["EUR"]["HWMeanReversionCurve"]["bump_index"] = i; bump_data["Currencies"]["EUR"]["HWMeanReversionCurve"]["bump_size"] = bump_size; XVAProblem<double> bumped_xva; bumped_xva.initData(bump_data); // Re-initialize with bumped parameter // Run FULL Monte Carlo simulation again calcRiskByBump(risk_results, base_results, bumped_xva, "MeanRev", i, bump_size, mc_iterations, randoms); } // For EACH survival curve point (140 + 140 points): for (int i = 0; i < xva.getCtrpSurvCurv()->getZeroRatesVector().size(); i++) { json bump_data(request_data); bump_data["CounterPartySurvivalCurve"]["bump_index"] = i; bump_data["CounterPartySurvivalCurve"]["bump_size"] = bump_size; // ... full MC re-simulation ... } // ... same for company survival, sigma, r0 ... } // Each bump requires the full MC loop: void calcRiskByBump(json& risk_results, const json& base_results, XVAProblem<double>& XVA, ...) { XVA.prepareSimulations(); for (int mc_i = 0; mc_i < mc_iterations; mc_i++) { XVA.simulatePath(randoms[mc_i]); // Full path simulation } XVA.computeXVAMeasures(); // Finite difference: (bumped_CVA - base_CVA) / bump_size risk_results["CVA"][risk_id][risk_index] = (XVA.getCVA() / mc_iterations - base_results["CVA"].get<double>()) / bump_size; }
Total work: (532 + 1) full MC simulations = 533× the cost of a single pricing run. Each sensitivity is independent but requires a complete reprice.
// XVAJobRequest.h, compileAADFunction() void compileAADFunction(const json& request_data) { // Random variables as input (no derivatives needed for these) std::vector<idouble> aad_random_vec(XVA.numberOfRandomVars()); m_aad_funcs->startRecording(); // Mark random inputs markVectorAsInput(m_random_arg, aad_random_vec, false); // Mark market data as variable inputs (can change without recompilation) markVariableInputs(getArgumentsMap(), true); // Create the XVA problem with aadc::idouble, same code as double version XVAProblem<idouble> aad_XVA; aad_XVA.initData(m_data); idouble::CheckPoint(); // Mark EVERY risk factor as its own input (one-time setup). // Each input remembers where its value comes from, so the compiled // kernel is fed the current market data on every run. auto markPoint = [&](const idouble& v, const std::string& json_path) { aadc::AADCArgument arg = v.markAsInput(); m_point_inputs.push_back({arg, json_path}); return arg; }; m_xva_diff_args.ir_crvs[0].r0 = markPoint(aad_XVA.getModel()->getR0(), "/Currencies/EUR/r0"); m_xva_diff_args.ir_crvs[0].sigma = markPoint(aad_XVA.getModel()->getSigma(), "/Currencies/EUR/sigma"); for (int i = 0; i < meanRevSize; i++) // 250 points m_xva_diff_args.ir_crvs[0].mr_crv.push_back(markPoint( aad_XVA.getModel()->getMeanRev()->getVals()[i], "/Currencies/EUR/HWMeanReversionCurve/flat_rate")); for (int i = 0; i < survivalCurveSize; i++) // 140 points m_xva_diff_args.company_surv_crv.default_rates.push_back(markPoint( aad_XVA.getCompSurvCurv()->getZeroRatesVector()[i], "/CompanySurvivalCurve/flat_rate")); // ... counterparty survival curve, 140 points, same pattern ... // Record ONE forward pass (same simulatePath + computeXVAMeasures) aad_XVA.prepareSimulations(); aad_XVA.simulatePath(aad_random_vec); aad_XVA.computeXVAMeasures(); // Mark outputs m_res_args.CVA = aad_XVA.getCVA().markAsOutput(); m_res_args.DVA = aad_XVA.getDVA().markAsOutput(); m_aad_funcs->stopRecording(); // Kernel is now compiled to optimised AVX2/AVX-512 machine code }
markAsInput() registers each risk factor as a kernel input and tells AADC it needs a gradient. All 532 risk factors are marked once during recording; their values are set from the current market data before each run, and the compiled kernel computes all their gradients on every reverse sweep.
// XVAJobRequest.h, aADExecution() void aADExecution(const json& request_data, int threads_num) { auto threadWorker = [&](int th_i) { // Each thread gets its own AADC workspace auto ws = m_aad_funcs->createWorkSpace(); // Set market data inputs (can update without recompilation) for (auto& [path, arg] : request_variable_inputs) ws->val(arg.second) = mmSetConst<mmType>(request_data[path]); // Set every risk-factor input from the current request for (auto& [arg, path] : m_point_inputs) ws->val(arg) = mmSetConst<mmType>(request_data[path]); m_aad_funcs->forward(*ws, 0, 0); // Initialise once for (int mc_i = 0; mc_i < AVX_iterations; mc_i++) { // Set random numbers (4 paths at once with AVX-256) setAVXVector(*ws, m_random_arg, mm_randoms[mc_i + AVX_iterations * th_i]); // Forward sweep: computes prices for 4 MC paths simultaneously m_aad_funcs->forward(*ws, 1, -1); // Reverse sweep for CVA sensitivities (ALL 532 gradients at once) ws->resetDiff(); ws->diff(m_res_args.CVA) = mmSetConst<mmType>(1); m_aad_funcs->reverse(*ws, 1, -1); // Reverse sweep for DVA sensitivities (ALL 532 gradients at once) ws->resetDiff(); ws->diff(m_res_args.DVA) = mmSetConst<mmType>(1); m_aad_funcs->reverse(*ws, 1, -1); } }; // Launch threads, each processes its share of MC paths for (int i = 0; i < threads_num; i++) threads.push_back(new std::thread(threadWorker, i)); for (auto& t : threads) t->join(); } // Kernel caching: zero-cost market data updates void processRequest(const json& request_data, ...) { auto cached = find_in_cache(m_data); if (cached != cache.end()) { m_aad_funcs = cached->aad_funcs; // REUSE: ~0ms } else { compileAADFunction(request_data); // COMPILE: 1.2-5.5s (5 threads) cache.push_back(new_kernel); } aADExecution(request_data, threads_num); }
AVX-256 processes 4 MC paths per SIMD instruction. Each thread works independently on its share of paths. Market data updates reuse the cached kernel with zero recompilation overhead.
| Aspect | Bump & Revalue | AADC |
|---|---|---|
| Type change | None (native double) | double → aadc::idouble |
| Business logic | Unchanged | Unchanged |
| Sensitivity loop | 533 full MC re-runs | 0 (reverse sweep) |
| One-time setup | None | Tape + compile (1.2–5.5s) |
| Per-path work | 1 forward pass | 1 fwd + 2 reverse |
| Parallelism | Parallel across bumps, (N+1)× work | Multi-threaded + AVX |
| Market data update | Re-run everything | No recompilation |
AADC reverse sweeps — cost independent of N risk factors
Over bump & revalue for 532 parameters, same 5 threads
Kernel vs plain pricing; finite differences agree to a median 1.0e-6
Prices + all 532 sensitivities, 10,000 paths, 5 threads
| Dimension | Bump & Revalue | AADC |
|---|---|---|
| Code changes | None (native double) | double → aadc::idouble + tape recording |
| Sensitivity cost | O(N) full MC re-simulations | O(1) reverse sweeps |
| Speedup (532 params) | Baseline | 665–864× (same 5 threads) |
| Accuracy | Median 1.0e-6 vs adjoint (bump-dependent) | Machine precision (≤1.2e-13 vs primal) |
| Market data update | Full re-computation | Kernel reuse (0s compilation) |
| SIMD vectorisation | Not applicable | AVX-256/512 (4–8 paths/cycle) |
| Multi-threading | Parallel but N× work | Parallel with 1× work |
AADC transforms XVA risk computation from an O(N) problem into an O(1) problem with respect to the number of risk factors, while maintaining mathematical exactness and requiring minimal code changes to the existing valuation library.
| Risk Factor | Count | Description |
|---|---|---|
| r0 (short rate) | 1 | Hull-White initial rate |
| sigma (volatility) | 1 | Hull-White volatility |
| Mean reversion curve | 250 | Piecewise-linear, 50yr horizon, 0.2yr step |
| Counterparty survival curve | 140 | 14,000-day horizon, 100-day step |
| Company survival curve | 140 | 14,000-day horizon, 100-day step |
| Total | 532 |
The benchmark: Hull-White 1-factor model with IR swaps, CSA collateral rules, 532 risk factors (r0, sigma, mean reversion curve, counterparty and company survival curves), 30-year horizon.
Important context: This benchmark was produced with Intel in 2019 to demonstrate AADC's capabilities on a clean, well-understood model. It deliberately uses vanilla IR swaps to isolate the AAD performance gains from model complexity. It doesn't cover more complex exotics like Bermudans, TARFs, or multi-factor hybrids. The timings on this page were measured in October 2026 with the current AADC release.
If there's a specific asset class that would make this more convincing for your book, we'd be happy to extend the benchmark — let us know what would resonate.
Bump & revalue times are estimated as 533 × one measured pricing run, spread over the same 5 threads as AADC with perfect parallel scaling; six real bumps per portfolio were timed to confirm the per-bump cost.
On the same 5 threads, AADC is 665-864× faster than bump & revalue for all 532 CVA/DVA sensitivities at 10,000 Monte Carlo paths (526× for 200 trades including kernel compilation). For 200 trades, AADC computes prices and all sensitivities in 20.8 seconds; bump & revalue is estimated at 3.8 hours (533 pricing runs of 129.8 s each, spread over 5 threads). The speedup scales with the number of risk factors because AADC cost is O(M) while bump & revalue is O(N × M).
AADC reproduces the plain double-precision pricing to machine precision (relative difference 1.1e-13 or less on the same 10,000 paths). Across 30 sampled sensitivities, bump & revalue with a 1e-8 bump agrees with the AADC adjoint to a median relative difference of 1.0e-6 (worst 2.0e-4, on a sensitivity of size 1.3e-4), and its accuracy depends on bump-size selection. AADC eliminates the subtractive cancellation inherent in finite differences.
AADC is a drop-in replacement: change 'double' to 'aadc::idouble' in the valuation path. No business logic changes are required. Integration steps: replace types, record tape with startRecording()/stopRecording(), mark inputs/outputs, then call forward()+reverse() for all sensitivities.
No. When only market data changes (rate curves, survival probabilities), AADC reuses the compiled kernel with zero compilation overhead. For 50 trades at 10K paths, run time drops from 8.2 s (cold start, including compilation) to 5.8 s (warm). Only structural changes (new trade types) require recompilation.
The benchmark computes sensitivities to 532 risk factors: r0 (Hull-White initial rate), sigma (volatility), 250 mean-reversion curve points (50yr horizon, 0.2yr step), 140 counterparty survival curve points, and 140 company survival curve points (14,000-day horizon, 100-day step).
At 10,000 paths on 5 threads, AADC evaluation (prices plus all 532 sensitivities) takes 0.98 s for 5 trades, 5.9 s for 50 trades and 20.8 s for 200 trades. Kernel compilation is a one-time cost (1.2 s to 5.5 s) reused across market data updates. On the same core count, the full AADC evaluation costs 0.62× (5 trades) to 0.80× (200 trades) of one plain pricing run.
AADC supports three main XVA workflows: (A) End-of-day full risk reports computing all 532 sensitivities at 0.6-0.8× primal cost + compilation, (B) Intraday market data updates with kernel reuse (no recompilation), and (C) New trade additions requiring only recompile + evaluation.