Hull-White · CSA Collateral · CVA/DVA · MatLogica Benchmark

XVA Sensitivities
AADC vs Bump & Revalue

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.

665–864×
Speedup, Same 5 Threads
532
Risk Factors (O(1))
20.8 s
200 Trades, All Greeks
≤1.2e-13
Kernel vs Primal
Intel Xeon Platinum 8280L · AVX2 · 5 threads · 10,000 MC paths · Hull-White 1-factor · 30yr horizon · 122 pricing times

Benchmark Configuration

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.

Performance: AADC vs Bump & Revalue

Full sensitivity set — 532 risk factors, all computed simultaneously

BUMP & REVALUE

Finite Differences

O(N × M)

N+1 full MC re-simulations for N risk factors. Each sensitivity requires a complete Monte Carlo reprice.

AADC

Adjoint Algorithmic Differentiation

O(M)

1 forward + 2 reverse sweeps (CVA, DVA) per MC path. All 532 gradients computed simultaneously — cost independent of N.

Head-to-Head: Wall-Clock Time (532 Sensitivities)

Log scale — includes all CVA + DVA Greeks

AADC Timing Breakdown

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.

Bump & Revalue Timing

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.

Scaling: Cost Ratio vs 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).

Accuracy Validation

AADC produces mathematically exact derivatives — to machine precision

AADC

≤ 1.2e-13 Relative difference vs primal
  • Exact derivatives via chain rule
  • No bump-size parameter to tune
  • No subtractive cancellation
  • Machine-precision accuracy

Bump & Revalue

1.0e-6 Median difference vs adjoint
  • Approximation via finite differences
  • Accuracy depends on bump size (1e-8)
  • Subtractive cancellation in floating-point
  • 30 sampled sensitivities: median 1.0e-6, worst 2.0e-4 vs AADC

Sensitivity Agreement

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

Workflow Scenarios

A

End-of-Day Full Risk Report

Compute CVA/DVA + all 532 sensitivities for the entire portfolio.

B&R 533× primal cost
AADC 0.6–0.8× primal + compilation
B

Intraday Market Data Update

Rate curves or survival probabilities change — re-compute CVA + sensitivities.

B&R 533 MC simulations
AADC Kernel reused (0s compilation)
C

New Trade Added

A new trade of a type already in the portfolio is added.

B&R Full re-computation
AADC Recompile + evaluation

Integration: Drop-in Replacement

aadc::idouble replaces double directly — no templates required

BEFORE Standard C++ (double)
double rate = 0.05;
double price = notional * exp(-rate * T);
double cva = max(price, 0.0) * (1.0 - survival_prob);
↓
AFTER AADC drop-in (idouble)
aadc::idouble rate = 0.05;
aadc::idouble price = notional * exp(-rate * T);
aadc::idouble cva = max(price, 0.0) * (1.0 - survival_prob);
1

Replace types

double → aadc::idouble in the valuation path

2

Record tape

startRecording() / stopRecording() around computation

3

Mark I/O

markAsInput() for each risk factor, markAsOutput() for outputs

4

Compute Greeks

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.

Code Examples from the Benchmark

Full source from the benchmark — same business logic runs with double or aadc::idouble

IDENTICAL XVAProblem<T> — same code for double and 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.

O(N × M) bumpAndRevalue() — 533 full MC re-simulations
// 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.

COMPILE ONCE compileAADFunction() — record tape + mark all 532 risk factors
// 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.

O(M) aADExecution() — multi-threaded AVX forward + reverse sweeps
// 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.

Summary of Changes Required

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

Conclusion

O(1)

Sensitivity Cost

AADC reverse sweeps — cost independent of N risk factors

665–864×

Speedup

Over bump & revalue for 532 parameters, same 5 threads

≤1.2e-13

Accuracy

Kernel vs plain pricing; finite differences agree to a median 1.0e-6

20.8s

200 Trades

Prices + all 532 sensitivities, 10,000 paths, 5 threads

Head-to-Head Comparison

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 Inventory

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

About This Benchmark

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.

Intel × MatLogica XVA White Paper XVA Pricing Application for Financial Services (PDF)
Intel Developer: AADC for Python & C++ Analytics Record-once replay-many paradigm — Intel Developer Zone (Aug 2024)
MatLogica Benchmark · Hull-White 1-factor · CSA Collateral · CVA/DVA · All timings from actual runs

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.

Frequently Asked Questions

What speedup does AADC achieve over bump & revalue for XVA sensitivities?

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).

How accurate are AADC sensitivities compared to bump & revalue?

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.

What is the integration effort to switch from bump & revalue to AADC?

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.

Does AADC need to recompile when market data changes?

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.

What risk factors are included in this XVA benchmark?

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).

How does AADC XVA performance scale with portfolio size?

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.

What production workflows does AADC XVA support?

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.