diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54af9a0..ce766cd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,18 +1,33 @@ -# Workflow to run against the corresponding QuantLib release, -# testing if the build and tests are working fine -# Note: In addition to pushes/pull requests, this workflow -# can also be executed manually, and the repositories / branches -# for QuantLib and XAD can be provided in this case. +############################################################################## +# +# QuantLib-Risks-Cpp CI Workflow +# +# Tests QuantLib-Risks-Cpp with XAD automatic differentiation, and optionally +# with Forge JIT acceleration on Linux and Windows. +# +# Jobs: +# - xad-linux: XAD tests (C++17, C++20, AAD ON/OFF) +# - xad-win: XAD tests (C++17, C++20, AAD ON/OFF) +# - xad-macos: XAD tests (C++17, C++20, AAD ON/OFF) +# - xad-linux-std-classes: XAD with QL_USE_STD_CLASSES +# - forge-linux: XAD + Forge JIT tests (C++17, Release/Debug) +# - forge-windows: XAD + Forge JIT tests (C++17, Release/Debug) +# +# Copyright (C) 2025 Xcelerit Computing Limited +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +############################################################################## + name: CI -on: - repository_dispatch: - types: [xad-ci-trigger] + +on: push: pull_request: workflow_dispatch: inputs: ql_repo: - description: Quantlib repository in / format + description: QuantLib repository in / format required: true default: lballabio/QuantLib ql_branch: @@ -27,15 +42,41 @@ on: description: Branch or tag for XAD repository required: true default: main - schedule: - - cron: '02 5 * * *' # 5:02 every day + forge_repo: + description: Forge repository in / format + required: true + default: da-roth/forge + forge_branch: + description: Branch or tag for Forge repository + required: true + default: main + xad_forge_repo: + description: xad-forge repository in / format + required: true + default: da-roth/xad-forge + xad_forge_branch: + description: Branch or tag for xad-forge repository + required: true + default: main + env: ql_repo: ${{ github.event.inputs.ql_repo || 'lballabio/QuantLib' }} ql_branch: ${{ github.event.inputs.ql_branch || 'master' }} xad_repo: ${{ github.event.inputs.xad_repo || 'auto-differentiation/xad' }} xad_branch: ${{ github.event.inputs.xad_branch || 'main' }} -jobs: + forge_repo: ${{ github.event.inputs.forge_repo || 'da-roth/forge' }} + forge_branch: ${{ github.event.inputs.forge_branch || 'main' }} + xad_forge_repo: ${{ github.event.inputs.xad_forge_repo || 'da-roth/xad-forge' }} + xad_forge_branch: ${{ github.event.inputs.xad_forge_branch || 'main' }} +jobs: + ############################################################################## + # Linux - XAD + # + # Note on disable_aad naming: This uses the original QuantLib-Risks-Cpp + # convention where QLRISKS_DISABLE_AAD=OFF means AAD is ENABLED (not disabled). + # The confusing double-negative is preserved for compatibility. + ############################################################################## xad-linux: strategy: fail-fast: false @@ -44,87 +85,97 @@ jobs: cxx: ["17", "20"] runs-on: ubuntu-latest container: ghcr.io/lballabio/quantlib-devenv:rolling + steps: - - uses: actions/checkout@v4 - with: - repository: ${{ env.ql_repo }} - ref: ${{ env.ql_branch }} - path: QuantLib - - uses: actions/checkout@v4 - with: - repository: ${{ env.xad_repo }} - ref: ${{ env.xad_branch }} - path: xad - - uses: actions/checkout@v4 - with: - path: QuantLib-Risks-Cpp - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2.12 - with: - key: linux-${{ matrix.disable_aad }} - max-size: 650M - - name: Setup - run: | - apt-get update \ - && apt install -y ccache ninja-build - - name: Configure - run: | - rm -rf ${{ github.workspace }}/install - cd QuantLib - mkdir build - cd build - cmake -G Ninja -DBOOST_ROOT=/usr \ - -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} \ - -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../../xad;$(pwd)/../../QuantLib-Risks-Cpp" \ - -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ - -DQL_NULL_AS_FUNCTIONS=ON \ - -DCMAKE_INSTALL_PREFIX=$(pwd)/../../install \ - .. - - name: Compile - run: | - cd QuantLib/build - cmake --build . - - name: Test QuantLib - run: | - cd QuantLib/build - ./test-suite/quantlib-test-suite --log_level=message - - name: Test QuantLib-Risks - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - cd QuantLib/build - ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message - - name: Install - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - cd QuantLib/build - cmake --install . - - name: Test Install - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - mkdir installtest - cp QuantLib-Risks-Cpp/Examples/AdjointSwap/AdjointSwapXAD.cpp installtest - cd installtest - echo "cmake_minimum_required(VERSION 3.15.2)" > CMakeLists.txt - echo "project(QlTest LANGUAGES CXX)" >> CMakeLists.txt - echo "find_package(QuantLib-Risks REQUIRED)" >> CMakeLists.txt - echo "add_executable(AdjointSwapXAD AdjointSwapXAD.cpp)" >> CMakeLists.txt - echo "target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib)" >> CMakeLists.txt - echo "target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17)" >> CMakeLists.txt - mkdir build - cd build - cmake -G Ninja -DBOOST_ROOT=/usr \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=$(pwd)/../../install \ - .. - cmake --build . - ./AdjointSwapXAD + - uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + + - uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: linux-xad-${{ matrix.cxx }}-${{ matrix.disable_aad }} + max-size: 650M + + - name: Setup + run: | + apt-get update && apt install -y ccache ninja-build + + - name: Configure + run: | + rm -rf ${{ github.workspace }}/install + cd QuantLib + mkdir build + cd build + cmake -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} \ + -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../../xad;$(pwd)/../../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DCMAKE_INSTALL_PREFIX=$(pwd)/../../install \ + .. + - name: Compile + run: | + cd QuantLib/build + cmake --build . + - name: Test QuantLib + run: | + cd QuantLib/build + ./test-suite/quantlib-test-suite --log_level=message + - name: Test QuantLib-Risks + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + cd QuantLib/build + ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message + - name: Install + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + cd QuantLib/build + cmake --install . + + - name: Test Install + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + mkdir installtest + cp QuantLib-Risks-Cpp/Examples/AdjointSwap/AdjointSwapXAD.cpp installtest + cd installtest + echo "cmake_minimum_required(VERSION 3.15.2)" > CMakeLists.txt + echo "project(QlTest LANGUAGES CXX)" >> CMakeLists.txt + echo "find_package(QuantLib-Risks REQUIRED)" >> CMakeLists.txt + echo "add_executable(AdjointSwapXAD AdjointSwapXAD.cpp)" >> CMakeLists.txt + echo "target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib)" >> CMakeLists.txt + echo "target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17)" >> CMakeLists.txt + mkdir build + cd build + cmake -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=$(pwd)/../../install \ + .. + cmake --build . + ./AdjointSwapXAD + + ############################################################################## + # Windows - XAD + ############################################################################## xad-win: strategy: fail-fast: false @@ -132,100 +183,115 @@ jobs: disable_aad: ["ON", "OFF"] cxx: ["17", "20"] runs-on: windows-2022 + env: vsvarsall: C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat + steps: - - uses: actions/checkout@v3 - with: - repository: ${{ env.ql_repo }} - ref: ${{ env.ql_branch }} - path: QuantLib - - uses: actions/checkout@v3 - with: - repository: ${{ env.xad_repo }} - ref: ${{ env.xad_branch }} - path: xad - - uses: actions/checkout@v3 - with: - path: QuantLib-Risks-Cpp - - name: sccache - uses: hendrikmuhs/ccache-action@v1.2.12 - with: - key: windows-${{ matrix.disable_aad }} - variant: sccache - max-size: 650M - - name: Setup - run: | - choco install -y ninja - $Url = "https://downloads.sourceforge.net/project/boost/boost-binaries/1.84.0/boost_1_84_0-msvc-14.3-64.exe" - (New-Object System.Net.WebClient).DownloadFile($Url, "$RUNNER_TEMP\boost.exe") - Start-Process -Wait -FilePath "$RUNNER_TEMP\boost.exe" "/SILENT","/SP-","/SUPPRESSMSGBOXES","/DIR=C:\local\boost" - - name: Configure - env: - BOOST_ROOT: C:\local\boost - shell: cmd - run: | - cd QuantLib - mkdir build - cd build - call "${{ env.vsvarsall }}" amd64 - cmake .. -G Ninja -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} ^ - -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} ^ - -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ^ - -DCMAKE_BUILD_TYPE=Release ^ - -DQL_EXTERNAL_SUBDIRECTORIES="${{ github.workspace }}/xad;${{ github.workspace }}/QuantLib-Risks-Cpp" ^ - -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ - -DQL_NULL_AS_FUNCTIONS=ON ^ - -DXAD_STATIC_MSVC_RUNTIME=ON ^ - -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install - - name: Build - shell: cmd - run: | - cd QuantLib\build - call "${{ env.vsvarsall }}" amd64 - cmake --build . - - name: Test QuantLib - shell: cmd - run: | - cd QuantLib\build - call "${{ env.vsvarsall }}" amd64 - .\test-suite\quantlib-test-suite --log_level=message - - name: Test QuantLib-Risks - if: ${{ matrix.disable_aad == 'OFF' }} - shell: cmd - run: | - cd QuantLib\build - call "${{ env.vsvarsall }}" amd64 - .\QuantLib-Risks-Cpp\test-suite\quantlib-risks-test-suite --log_level=message - - name: Install - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - cd QuantLib/build - cmake --install . - - name: Test Install - if: ${{ matrix.disable_aad == 'OFF' }} - env: - BOOST_ROOT: C:\local\boost - shell: cmd - run: | - mkdir installtest - copy QuantLib-Risks-Cpp\Examples\AdjointSwap\AdjointSwapXAD.cpp installtest - cd installtest - echo cmake_minimum_required(VERSION 3.15.2) > CMakeLists.txt - echo project(QlTest LANGUAGES CXX) >> CMakeLists.txt - echo find_package(QuantLib-Risks REQUIRED) >> CMakeLists.txt - echo add_executable(AdjointSwapXAD AdjointSwapXAD.cpp) >> CMakeLists.txt - echo target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib) >> CMakeLists.txt - echo set_target_properties(AdjointSwapXAD PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreaded) >> CMakeLists.txt - echo target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17) >> CMakeLists.txt - mkdir build - cd build - call "${{ env.vsvarsall }}" amd64 - cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install .. - cmake --build . - AdjointSwapXAD.exe + - uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + - uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + - name: sccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: windows-xad-${{ matrix.cxx }}-${{ matrix.disable_aad }} + variant: sccache + max-size: 650M + + - name: Setup + run: | + choco install -y ninja + $Url = "https://downloads.sourceforge.net/project/boost/boost-binaries/1.86.0/boost_1_86_0-msvc-14.3-64.exe" + (New-Object System.Net.WebClient).DownloadFile($Url, "$RUNNER_TEMP\boost.exe") + Start-Process -Wait -FilePath "$RUNNER_TEMP\boost.exe" "/SILENT","/SP-","/SUPPRESSMSGBOXES","/DIR=C:\local\boost" + + - name: Configure + env: + BOOST_ROOT: C:\local\boost + shell: cmd + run: | + cd QuantLib + mkdir build + cd build + call "${{ env.vsvarsall }}" amd64 + cmake .. -G Ninja -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} ^ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} ^ + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DQL_EXTERNAL_SUBDIRECTORIES="${{ github.workspace }}/xad;${{ github.workspace }}/QuantLib-Risks-Cpp" ^ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ + -DQL_NULL_AS_FUNCTIONS=ON ^ + -DXAD_STATIC_MSVC_RUNTIME=ON ^ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install + + - name: Build + shell: cmd + run: | + cd QuantLib\build + call "${{ env.vsvarsall }}" amd64 + cmake --build . + + - name: Test QuantLib + shell: cmd + run: | + cd QuantLib\build + call "${{ env.vsvarsall }}" amd64 + .\test-suite\quantlib-test-suite --log_level=message + + - name: Test QuantLib-Risks + if: ${{ matrix.disable_aad == 'OFF' }} + shell: cmd + run: | + cd QuantLib\build + call "${{ env.vsvarsall }}" amd64 + .\QuantLib-Risks-Cpp\test-suite\quantlib-risks-test-suite --log_level=message + + - name: Install + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + cd QuantLib/build + cmake --install . + + - name: Test Install + if: ${{ matrix.disable_aad == 'OFF' }} + env: + BOOST_ROOT: C:\local\boost + shell: cmd + run: | + mkdir installtest + copy QuantLib-Risks-Cpp\Examples\AdjointSwap\AdjointSwapXAD.cpp installtest + cd installtest + echo cmake_minimum_required(VERSION 3.15.2) > CMakeLists.txt + echo project(QlTest LANGUAGES CXX) >> CMakeLists.txt + echo find_package(QuantLib-Risks REQUIRED) >> CMakeLists.txt + echo add_executable(AdjointSwapXAD AdjointSwapXAD.cpp) >> CMakeLists.txt + echo target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib) >> CMakeLists.txt + echo set_target_properties(AdjointSwapXAD PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreaded) >> CMakeLists.txt + echo target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17) >> CMakeLists.txt + mkdir build + cd build + call "${{ env.vsvarsall }}" amd64 + cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install .. + cmake --build . + AdjointSwapXAD.exe + + ############################################################################## + # macOS - XAD + # Note: Forge not supported on macOS ARM yet + ############################################################################## xad-macos: strategy: fail-fast: false @@ -233,134 +299,372 @@ jobs: disable_aad: ["ON", "OFF"] cxx: ["17", "20"] runs-on: macos-latest + steps: - - uses: actions/checkout@v4 - with: - repository: ${{ env.ql_repo }} - ref: ${{ env.ql_branch }} - path: QuantLib - - uses: actions/checkout@v4 - with: - repository: ${{ env.xad_repo }} - ref: ${{ env.xad_branch }} - path: xad - - uses: actions/checkout@v4 - with: - path: QuantLib-Risks-Cpp - - name: Setup - run: | - brew install boost - brew install ninja - brew install ccache - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2.12 - with: - key: macos-${{ matrix.disable_aad }} - max-size: 650M - - name: Configure - run: | - cd QuantLib - mkdir build - cd build - cmake -G Ninja -DBOOST_ROOT=/usr \ - -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} \ - -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DQL_EXTERNAL_SUBDIRECTORIES="${{ github.workspace }}/xad;${{ github.workspace }}/QuantLib-Risks-Cpp" \ - -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ - -DQL_NULL_AS_FUNCTIONS=ON \ - -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install \ - .. - - name: Compile - run: | - cd QuantLib/build - cmake --build . - - name: Test QuantLib - run: | - cd QuantLib/build - ./test-suite/quantlib-test-suite --log_level=message - - name: Test QuantLib-Risks - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - cd QuantLib/build - ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message - - name: Install - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - cd QuantLib/build - cmake --install . - - name: Test Install - if: ${{ matrix.disable_aad == 'OFF' }} - run: | - mkdir installtest - cp QuantLib-Risks-Cpp/Examples/AdjointSwap/AdjointSwapXAD.cpp installtest - cd installtest - echo "cmake_minimum_required(VERSION 3.15.2)" > CMakeLists.txt - echo "project(QlTest LANGUAGES CXX)" >> CMakeLists.txt - echo "find_package(QuantLib-Risks REQUIRED)" >> CMakeLists.txt - echo "add_executable(AdjointSwapXAD AdjointSwapXAD.cpp)" >> CMakeLists.txt - echo "target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib)" >> CMakeLists.txt - echo "target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17)" >> CMakeLists.txt - mkdir build - cd build - cmake -G Ninja -DBOOST_ROOT=/usr \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install \ - .. - cmake --build . - ./AdjointSwapXAD + - uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + + - uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + - name: Setup + run: | + brew install boost ninja ccache + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: macos-xad-${{ matrix.cxx }}-${{ matrix.disable_aad }} + max-size: 650M + + - name: Configure + run: | + cd QuantLib + mkdir build + cd build + cmake -G Ninja \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} \ + -DQLRISKS_DISABLE_AAD=${{ matrix.disable_aad }} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQL_EXTERNAL_SUBDIRECTORIES="${{ github.workspace }}/xad;${{ github.workspace }}/QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install \ + .. + + - name: Compile + run: | + cd QuantLib/build + cmake --build . + + - name: Test QuantLib + run: | + cd QuantLib/build + ./test-suite/quantlib-test-suite --log_level=message + + - name: Test QuantLib-Risks + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + cd QuantLib/build + ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message + + - name: Install + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + cd QuantLib/build + cmake --install . + + - name: Test Install + if: ${{ matrix.disable_aad == 'OFF' }} + run: | + mkdir installtest + cp QuantLib-Risks-Cpp/Examples/AdjointSwap/AdjointSwapXAD.cpp installtest + cd installtest + echo "cmake_minimum_required(VERSION 3.15.2)" > CMakeLists.txt + echo "project(QlTest LANGUAGES CXX)" >> CMakeLists.txt + echo "find_package(QuantLib-Risks REQUIRED)" >> CMakeLists.txt + echo "add_executable(AdjointSwapXAD AdjointSwapXAD.cpp)" >> CMakeLists.txt + echo "target_link_libraries(AdjointSwapXAD PRIVATE QuantLib::QuantLib)" >> CMakeLists.txt + echo "target_compile_features(AdjointSwapXAD PUBLIC cxx_std_17)" >> CMakeLists.txt + mkdir build + cd build + cmake -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install \ + .. + cmake --build . + ./AdjointSwapXAD + + ############################################################################## + # Linux - XAD with QL_USE_STD_CLASSES + ############################################################################## xad-linux-std-classes: + runs-on: ubuntu-latest + container: ghcr.io/lballabio/quantlib-devenv:rolling + + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + + - uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: | + apt-get update && apt install -y ccache ninja-build + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: linux-xad-std-classes + max-size: 650M + + - name: Configure + run: | + cd QuantLib + mkdir build + cd build + cmake -G Ninja -DBOOST_ROOT=/usr \ + -DQL_USE_STD_CLASSES=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../../xad;$(pwd)/../../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + .. + + - name: Compile + run: | + cd QuantLib/build + cmake --build . + + - name: Test QuantLib + run: | + cd QuantLib/build + ./test-suite/quantlib-test-suite --log_level=message + + - name: Test QuantLib-Risks + run: | + cd QuantLib/build + ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message + + ############################################################################## + # Linux - XAD + Forge JIT + # + # Forge accelerates AAD tape computations via JIT compilation, so these jobs + # only make sense with AAD enabled (QLRISKS_DISABLE_AAD=OFF). + # Tests C++17 and C++20 to match original XAD job coverage. + ############################################################################## + forge-linux: strategy: fail-fast: false + matrix: + build_type: ["Release", "Debug"] + cxx: ["17", "20"] runs-on: ubuntu-latest container: ghcr.io/lballabio/quantlib-devenv:rolling + steps: - - uses: actions/checkout@v4 - with: - repository: ${{ env.ql_repo }} - ref: ${{ env.ql_branch }} - path: QuantLib - - uses: actions/checkout@v4 - with: - repository: ${{ env.xad_repo }} - ref: ${{ env.xad_branch }} - path: xad - - uses: actions/checkout@v4 - with: - path: QuantLib-Risks-Cpp - - name: Setup - run: | - apt-get update \ - && apt install -y ccache ninja-build \ - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2.12 - with: - key: linux-std-classes - max-size: 650M - - name: Configure - run: | - cd QuantLib - mkdir build - cd build - cmake -G Ninja -DBOOST_ROOT=/usr \ - -DQL_USE_STD_CLASSES=ON \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../../xad;$(pwd)/../../QuantLib-Risks-Cpp" \ - -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ - -DQL_NULL_AS_FUNCTIONS=ON \ - .. - - name: Compile - run: | - cd QuantLib/build - cmake --build . - - name: Test QuantLib - run: | - cd QuantLib/build - ./test-suite/quantlib-test-suite --log_level=message - - name: Test QuantLib-Risks - run: | - cd QuantLib/build - ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + + - name: Checkout Forge + uses: actions/checkout@v4 + with: + repository: ${{ env.forge_repo }} + ref: ${{ env.forge_branch }} + path: forge + + - name: Checkout xad-forge + uses: actions/checkout@v4 + with: + repository: ${{ env.xad_forge_repo }} + ref: ${{ env.xad_forge_branch }} + path: xad-forge + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: linux-forge-${{ matrix.build_type }} + max-size: 650M + + - name: Setup + run: | + apt-get update && apt-get install -y ninja-build ccache + + - name: Build Forge C API + run: | + cd forge + cmake -B build -S api/c \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_INSTALL_PREFIX=$(pwd)/../install + cmake --build build --config ${{ matrix.build_type }} + cmake --install build --config ${{ matrix.build_type }} + + - name: Configure QuantLib with XAD + Forge + run: | + cd QuantLib + mkdir build + cd build + cmake -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DXAD_WARNINGS_PARANOID=OFF \ + -DXAD_ENABLE_JIT=ON \ + -DCMAKE_PREFIX_PATH=$(pwd)/../../install \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../../xad;$(pwd)/../../xad-forge;$(pwd)/../../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DQL_BUILD_TEST_SUITE=OFF \ + -DQL_BUILD_EXAMPLES=OFF \ + -DQL_BUILD_BENCHMARK=OFF \ + -DQLRISKS_DISABLE_AAD=OFF \ + -DQLRISKS_ENABLE_FORGE=ON \ + -DQLRISKS_USE_FORGE_CAPI=ON \ + -DXAD_FORGE_USE_CAPI=ON \ + -DQLRISKS_BUILD_TEST_SUITE=ON \ + -DQLRISKS_ENABLE_FORGE_TESTS=ON \ + .. + + - name: Build + run: | + cd QuantLib/build + cmake --build . + + - name: Test QuantLib-Risks + run: | + cd QuantLib/build + ./QuantLib-Risks-Cpp/test-suite/quantlib-risks-test-suite --log_level=message + + ############################################################################## + # Windows - XAD + Forge JIT + # + # Forge accelerates AAD tape computations via JIT compilation, so these jobs + # only make sense with AAD enabled (QLRISKS_DISABLE_AAD=OFF). + # Tests C++17 and C++20 to match original XAD job coverage. + ############################################################################## + forge-windows: + strategy: + fail-fast: false + matrix: + build_type: ["Release", "Debug"] + cxx: ["17", "20"] + runs-on: windows-2022 + + env: + VSVARSALL: C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat + + steps: + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.ql_repo }} + ref: ${{ env.ql_branch }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.xad_repo }} + ref: ${{ env.xad_branch }} + path: xad + + - name: Checkout Forge + uses: actions/checkout@v4 + with: + repository: ${{ env.forge_repo }} + ref: ${{ env.forge_branch }} + path: forge + + - name: Checkout xad-forge + uses: actions/checkout@v4 + with: + repository: ${{ env.xad_forge_repo }} + ref: ${{ env.xad_forge_branch }} + path: xad-forge + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: choco install -y ninja + + - name: Setup Boost + run: | + $Url = "https://downloads.sourceforge.net/project/boost/boost-binaries/1.86.0/boost_1_86_0-msvc-14.3-64.exe" + (New-Object System.Net.WebClient).DownloadFile($Url, "$RUNNER_TEMP\boost.exe") + Start-Process -Wait -FilePath "$RUNNER_TEMP\boost.exe" "/SILENT","/SP-","/SUPPRESSMSGBOXES","/DIR=C:\local\boost" + echo "BOOST_ROOT=C:\local\boost" >> $env:GITHUB_ENV + + - name: Build Forge C API + shell: cmd + run: | + cd forge + call "%VSVARSALL%" amd64 + cmake -B build -S api/c -G Ninja ^ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ^ + -DFORGE_CAPI_BUILD_TESTS=OFF ^ + -DFORGE_CAPI_USE_STATIC_RUNTIME=ON ^ + -DCMAKE_INSTALL_PREFIX="%cd%\..\install" + cmake --build build --config ${{ matrix.build_type }} + cmake --install build --config ${{ matrix.build_type }} + + - name: Configure QuantLib with XAD + Forge + shell: cmd + run: | + cd QuantLib + call "%VSVARSALL%" amd64 + cmake -B build -G Ninja ^ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ^ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx }} ^ + -DXAD_WARNINGS_PARANOID=OFF ^ + -DXAD_ENABLE_JIT=ON ^ + -DXAD_STATIC_MSVC_RUNTIME=ON ^ + -DCMAKE_PREFIX_PATH="%cd%\..\install" ^ + -DQL_EXTERNAL_SUBDIRECTORIES="%cd%\..\xad;%cd%\..\xad-forge;%cd%\..\QuantLib-Risks-Cpp" ^ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ + -DQL_NULL_AS_FUNCTIONS=ON ^ + -DQL_BUILD_TEST_SUITE=OFF ^ + -DQL_BUILD_EXAMPLES=OFF ^ + -DQL_BUILD_BENCHMARK=OFF ^ + -DQLRISKS_DISABLE_AAD=OFF ^ + -DQLRISKS_ENABLE_FORGE=ON ^ + -DQLRISKS_USE_FORGE_CAPI=ON ^ + -DXAD_FORGE_USE_CAPI=ON ^ + -DQLRISKS_BUILD_TEST_SUITE=ON ^ + -DQLRISKS_ENABLE_FORGE_TESTS=ON + + - name: Build + shell: cmd + run: | + cd QuantLib\build + call "%VSVARSALL%" amd64 + cmake --build . --config ${{ matrix.build_type }} + + - name: Test QuantLib-Risks + shell: cmd + run: | + cd QuantLib\build + set PATH=%cd%\..\..\install\bin;%PATH% + QuantLib-Risks-Cpp\test-suite\quantlib-risks-test-suite.exe --log_level=message diff --git a/.github/workflows/ql-benchmarks.yaml b/.github/workflows/ql-benchmarks.yaml new file mode 100644 index 0000000..7529e81 --- /dev/null +++ b/.github/workflows/ql-benchmarks.yaml @@ -0,0 +1,922 @@ +############################################################################## +# +# QuantLib-Risks Benchmarks +# +# Comprehensive benchmark comparing FD vs XAD vs Forge JIT +# +# Structure: +# - Two jobs: Linux and Windows +# - Each job builds 3 versions on SAME hardware for fair comparison: +# 1. Plain double QuantLib (for FD) +# 2. XAD (JIT disabled) for XAD tape AAD +# 3. XAD + Forge (JIT enabled) for JIT/JIT-AVX AAD +# +# Jobs: +# - Linux, QL + XAD + Forge: Full benchmark suite on Linux +# - Windows, QL + XAD + Forge: Full benchmark suite on Windows +# +# Dependencies: +# - QuantLib (lballabio/QuantLib) +# - XAD (auto-differentiation/xad) +# - QuantLib-Risks-Cpp (this repo) +# - xad-forge (da-roth/xad-forge) [Forge builds] +# - Forge C API (da-roth/forge) [Forge builds] +# +# Copyright (C) 2025 Xcelerit Computing Limited +# SPDX-License-Identifier: AGPL-3.0-or-later +# +############################################################################## + +name: QL Benchmarks + +on: + push: + branches: + - main + - forge + pull_request: + branches: + - main + - forge + workflow_dispatch: + +env: + QL_REPO: lballabio/QuantLib + QL_BRANCH: master + XAD_REPO: auto-differentiation/xad + XAD_BRANCH: main + FORGE_REPO: da-roth/forge + FORGE_BRANCH: main + XAD_FORGE_REPO: da-roth/xad-forge + XAD_FORGE_BRANCH: main + +jobs: + ############################################################################## + # Linux, QL + XAD + Forge + # Comprehensive benchmark: FD (plain double) vs XAD vs Forge JIT + ############################################################################## + linux-benchmark: + name: Linux, QL + XAD + Forge + runs-on: ubuntu-latest + container: + image: ghcr.io/lballabio/quantlib-devenv:rolling + + steps: + - name: Hardware Info + run: | + echo "===== CPU =====" + lscpu | grep -E "^(Model name|CPU\(s\)|Thread|Core|Socket|CPU max MHz)" + echo "===== Memory =====" + free -h + echo "===== SIMD =====" + cat /proc/cpuinfo | grep flags | head -1 | tr ' ' '\n' | grep -E "^(sse|avx)" | sort -u | tr '\n' ' ' + echo "" + + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.QL_REPO }} + ref: ${{ env.QL_BRANCH }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_REPO }} + ref: ${{ env.XAD_BRANCH }} + path: xad + + - name: Checkout Forge + uses: actions/checkout@v4 + with: + repository: ${{ env.FORGE_REPO }} + ref: ${{ env.FORGE_BRANCH }} + path: forge + + - name: Checkout xad-forge + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_FORGE_REPO }} + ref: ${{ env.XAD_FORGE_BRANCH }} + path: xad-forge + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: | + apt-get update + apt-get install -y ninja-build cmake + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: linux-benchmark + max-size: 1G + + # ========================================================================= + # Build Forge C API (needed for JIT builds) + # ========================================================================= + - name: Build Forge C API + run: | + cd forge + cmake -B build -S api/c -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DFORGE_CAPI_BUILD_TESTS=OFF \ + -DCMAKE_INSTALL_PREFIX=$(pwd)/../install + cmake --build build + cmake --install build + + # ========================================================================= + # Benchmark 1: XAD + Forge (JIT enabled) - Run first (longest build) + # ========================================================================= + - name: Configure QuantLib (XAD + Forge) + run: | + cd QuantLib + cmake -B build-forge -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DXAD_WARNINGS_PARANOID=OFF \ + -DXAD_ENABLE_JIT=ON \ + -DCMAKE_PREFIX_PATH=$(pwd)/../install \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../xad;$(pwd)/../xad-forge;$(pwd)/../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DQL_BUILD_TEST_SUITE=OFF \ + -DQL_BUILD_EXAMPLES=OFF \ + -DQL_BUILD_BENCHMARK=OFF \ + -DQLRISKS_DISABLE_AAD=OFF \ + -DQLRISKS_ENABLE_FORGE=ON \ + -DQLRISKS_USE_FORGE_CAPI=ON \ + -DXAD_FORGE_USE_CAPI=ON \ + -DQLRISKS_BUILD_BENCHMARK_AAD=ON + + - name: Build Forge benchmark (JIT on) + run: | + cd QuantLib/build-forge + cmake --build . --target benchmark_aad + + - name: Run Forge Benchmark (JIT on) + run: | + echo "===== FORGE BENCHMARK (XAD + Forge JIT) =====" + cd QuantLib/build-forge + export LD_LIBRARY_PATH=$(pwd)/../../install/lib:$LD_LIBRARY_PATH + ./QuantLib-Risks-Cpp/test-suite/benchmark-aad --all 2>&1 | tee forge_results.txt + + - name: Run XAD-Split vs JIT Diagnostic + run: | + echo "===== DIAGNOSTIC: XAD-Split vs Forge Derivative Comparison =====" + cd QuantLib/build-forge + export LD_LIBRARY_PATH=$(pwd)/../../install/lib:$LD_LIBRARY_PATH + ./QuantLib-Risks-Cpp/test-suite/benchmark-aad --diagnose --diagnose-paths=100 2>&1 | tee diagnostic_results.txt + + # ========================================================================= + # Benchmark 2: Plain double QuantLib (for FD benchmark) + # ========================================================================= + - name: Configure QuantLib (plain double) + run: | + cd QuantLib + cmake -B build-double -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQL_BUILD_TEST_SUITE=OFF \ + -DQL_BUILD_EXAMPLES=OFF \ + -DQL_BUILD_BENCHMARK=OFF \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../QuantLib-Risks-Cpp" \ + -DQLRISKS_DISABLE_AAD=ON \ + -DQLRISKS_BUILD_BENCHMARK_FD=ON + + - name: Build FD benchmark (plain double) + run: | + cd QuantLib/build-double + cmake --build . --target benchmark_fd + + - name: Run FD Benchmark (plain double) + run: | + echo "===== FD BENCHMARK (plain double QuantLib) =====" + cd QuantLib/build-double + ./QuantLib-Risks-Cpp/test-suite/benchmark-fd --all 2>&1 | tee fd_results.txt + + # ========================================================================= + # Benchmark 3: XAD (JIT disabled) for XAD tape AAD + # ========================================================================= + - name: Configure QuantLib (XAD, JIT off) + run: | + cd QuantLib + cmake -B build-xad -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DXAD_WARNINGS_PARANOID=OFF \ + -DXAD_ENABLE_JIT=OFF \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../xad;$(pwd)/../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DQL_BUILD_TEST_SUITE=OFF \ + -DQL_BUILD_EXAMPLES=OFF \ + -DQL_BUILD_BENCHMARK=OFF \ + -DQLRISKS_DISABLE_AAD=OFF \ + -DQLRISKS_BUILD_BENCHMARK_AAD=ON + + - name: Build XAD benchmark (JIT off) + run: | + cd QuantLib/build-xad + cmake --build . --target benchmark_aad + + - name: Run XAD Benchmark (JIT off) + run: | + echo "===== XAD BENCHMARK (XAD + XAD-Split, JIT disabled) =====" + cd QuantLib/build-xad + ./QuantLib-Risks-Cpp/test-suite/benchmark-aad --all --xad-only 2>&1 | tee xad_results.txt + + # ========================================================================= + # Combine and display results + # ========================================================================= + - name: Generate Combined Report + run: | + echo "================================================================================" + echo " COMBINED BENCHMARK RESULTS" + echo "================================================================================" + echo "" + echo "All benchmarks ran on the same hardware for fair comparison." + echo "" + + # Create a Python script to parse and format results + cat > format_results.py << 'PYEOF' + import sys + import re + from collections import defaultdict + + def parse_line(line): + """Parse a line like 'METHOD_CONFIG:10=1.5,0.1,1[,fixed];100=5.2,0.2,1[,fixed]'""" + match = re.match(r'^(\w+)_(\w+):(.+)$', line.strip()) + if not match: + return None + method, config, data = match.groups() + results = {} + for item in data.split(';'): + if '=' in item: + paths, vals = item.split('=') + parts = vals.split(',') + mean, std, enabled = parts[0], parts[1], parts[2] + fixed = float(parts[3]) if len(parts) > 3 else 0.0 + if enabled == '1' and float(mean) > 0: + results[int(paths)] = {'mean': float(mean), 'fixed': fixed} + return method, config, results + + def parse_phases_line(line): + """Parse JIT_PHASES_CONFIG:p1,p2,p3""" + match = re.match(r'^JIT_PHASES_(\w+):(.+)$', line.strip()) + if not match: + return None + config, data = match.groups() + parts = data.split(',') + if len(parts) >= 3: + return config, { + 'phase1': float(parts[0]), + 'phase2': float(parts[1]), + 'phase3': float(parts[2]) + } + return None + + def format_num(n): + """Format number with appropriate precision""" + if n >= 1000: + return f"{n:.0f}" + elif n >= 100: + return f"{n:.1f}" + elif n >= 10: + return f"{n:.1f}" + else: + return f"{n:.2f}" + + def format_paths(p): + """Format path count: 100, 1K, 10K, 100K""" + if p >= 1000: + return f"{p//1000}K" + return str(p) + + def parse_validation_line(line): + """Parse a line like 'VALIDATE_CONFIG_METHOD:pv;sens1,sens2,...'""" + match = re.match(r'^VALIDATE_(\w+)_(\w+):(.+)$', line.strip()) + if not match: + return None + config, method, data = match.groups() + parts = data.split(';') + if len(parts) != 2: + return None + pv = float(parts[0]) + sens = [float(x) for x in parts[1].split(',') if x] + return config, method, pv, sens + + def compare_sensitivities(sens1, sens2, tol_pct): + """Compare two sensitivity vectors, return (matching_count, total, max_rel_diff)""" + if len(sens1) != len(sens2): + return 0, max(len(sens1), len(sens2)), float('inf') + matching = 0 + max_diff = 0.0 + for s1, s2 in zip(sens1, sens2): + if abs(s1) < 1e-12 and abs(s2) < 1e-12: + matching += 1 + continue + base = max(abs(s1), abs(s2)) + rel_diff = abs(s1 - s2) / base if base > 1e-12 else 0.0 + max_diff = max(max_diff, rel_diff) + if rel_diff <= tol_pct / 100.0: + matching += 1 + return matching, len(sens1), max_diff * 100.0 # return as percentage + + # Read all result files + data = defaultdict(lambda: defaultdict(dict)) # data[config][method][paths] = {mean, fixed} + phases = {} # phases[config] = {phase1, phase2, phase3} + validation = defaultdict(dict) # validation[config][method] = (pv, sens) + + for filepath in sys.argv[1:]: + # Skip XADSPLIT from forge_results.txt (use xad_results.txt instead for fair comparison) + is_forge_results = 'forge_results' in filepath + try: + with open(filepath) as f: + for line in f: + result = parse_line(line) + if result: + method, config, results = result + # Use XADSPLIT from xad_results.txt, not forge_results.txt + if is_forge_results and method == 'XADSPLIT': + continue + for paths, vals in results.items(): + data[config][method][paths] = vals + phase_result = parse_phases_line(line) + if phase_result: + config, phase_data = phase_result + phases[config] = phase_data + val_result = parse_validation_line(line) + if val_result: + config, method, pv, sens = val_result + validation[config][method] = (pv, sens) + except FileNotFoundError: + pass + + # Print tables for each config + configs = ['LITE', 'LITEEXT', 'PRODUCTION'] + config_names = { + 'LITE': 'Lite (1Y×1Y, 9 inputs)', + 'LITEEXT': 'Lite-Extended (5Y×5Y, 14 inputs)', + 'PRODUCTION': 'Production (5Y×5Y dual-curve, 45 inputs)' + } + + # Column display names (asterisk indicates methods with setup cost) + method_display = {'FD': 'FD', 'XAD': 'XAD', 'XADSPLIT': 'XAD-Split*', 'JIT': 'Forge*', 'JITAVX': 'Forge-AVX2*'} + col_width = 12 # Width for each numeric column + + for config in configs: + if config not in data: + continue + + print(f"\n### {config_names.get(config, config)}\n") + methods = data[config] + all_paths = sorted(set(p for m in methods.values() for p in m.keys())) + + if not all_paths: + continue + + # Build header (no JIT-Setup column in main table) + method_order = ['FD', 'XAD', 'XADSPLIT', 'JIT', 'JITAVX'] + present_methods = [m for m in method_order if m in methods] + + header = f"{'Paths':>6}" + for m in present_methods: + header += f"{method_display[m]:>{col_width}}" + print(header) + + # Separator line + sep = "-" * len(header) + print(sep) + + # Data rows + for paths in all_paths: + row = f"{format_paths(paths):>6}" + for m in present_methods: + val = methods[m].get(paths) + if val: + row += f"{format_num(val['mean']):>{col_width}}" + else: + row += f"{'-':>{col_width}}" + print(row) + + # Print JIT phase breakdown table if available + if config in phases: + p = phases[config] + total = p['phase1'] + p['phase2'] + p['phase3'] + print(f"\n*Forge JIT setup (one-time costs, independent of paths):\n") + print(f" {'Phase':<35} {'Time (ms)':>10}") + print(f" {'-'*45}") + print(f" {'1. Curve bootstrap (XAD)':<35} {format_num(p['phase1']):>10}") + print(f" {'2. Jacobian (XAD adjoints)':<35} {format_num(p['phase2']):>10}") + print(f" {'3. JIT graph record + compile':<35} {format_num(p['phase3']):>10}") + print(f" {'-'*45}") + print(f" {'Total setup':<35} {format_num(total):>10}") + + # Print validation table if we have validation data for this config + if config in validation and validation[config]: + val_methods = validation[config] + print(f"\nValidation (at 10K paths):\n") + print(f" {'Method':<10} {'PV':>14} {'Sens':>6} {'vs FD':>8} {'FD MaxDiff':>12} {'vs XAD':>8} {'XAD MaxDiff':>12}") + print(f" {'-'*72}") + + method_order = ['FD', 'XAD', 'XADSPLIT', 'JIT', 'JITAVX'] + fd_sens = val_methods.get('FD', (None, []))[1] + xad_sens = val_methods.get('XAD', (None, []))[1] + + # Display names for validation table (without asterisk) + val_display = {'FD': 'FD', 'XAD': 'XAD', 'XADSPLIT': 'XAD-Split', 'JIT': 'Forge', 'JITAVX': 'Forge-AVX2'} + + for m in method_order: + if m not in val_methods: + continue + pv, sens = val_methods[m] + + # Compare vs FD (1% tolerance) + if m == 'FD': + vs_fd = '-' + fd_maxdiff = '-' + elif fd_sens: + match, total, maxd = compare_sensitivities(sens, fd_sens, 1.0) + vs_fd = f"{match}/{total}" + fd_maxdiff = f"{maxd:.2f}%" + else: + vs_fd = '-' + fd_maxdiff = '-' + + # Compare vs XAD (0.01% tolerance) + if m in ('FD', 'XAD'): + vs_xad = '-' + xad_maxdiff = '-' + elif xad_sens: + match, total, maxd = compare_sensitivities(sens, xad_sens, 0.01) + vs_xad = f"{match}/{total}" + xad_maxdiff = f"{maxd:.4f}%" + else: + vs_xad = '-' + xad_maxdiff = '-' + + print(f" {val_display[m]:<10} {pv:>14.6f} {len(sens):>6} {vs_fd:>8} {fd_maxdiff:>12} {vs_xad:>8} {xad_maxdiff:>12}") + + # Determine which methods are present across all configs + all_methods = set() + for config in configs: + if config in data: + all_methods.update(data[config].keys()) + + print("\nAll times in milliseconds (ms).") + has_jit = 'JIT' in all_methods or 'JITAVX' in all_methods + has_xadsplit = 'XADSPLIT' in all_methods + if has_jit and has_xadsplit: + print("*Forge uses the Forge JIT compiler to generate native code (scalar, 1 path at a time).") + print(" Forge-AVX2 uses AVX2 vectorization (4 paths at a time). See setup breakdown above.") + print(" XAD-Split computes Jacobian separately and re-records a small tape per path (no JIT).") + elif has_xadsplit: + print("*XAD-Split computes the Jacobian separately and re-records a small tape per path,") + print(" avoiding one large tape that records all MC paths.") + elif has_jit: + print("*Forge uses the Forge JIT compiler (scalar, 1 path at a time).") + print(" Forge-AVX2 uses AVX2 vectorization (4 paths at a time). See setup breakdown above.") + PYEOF + + python3 format_results.py \ + QuantLib/build-double/fd_results.txt \ + QuantLib/build-xad/xad_results.txt \ + QuantLib/build-forge/forge_results.txt + + echo "" + echo "================================================================================" + echo "" + echo "Raw data (for parsing):" + echo "" + grep -E "^(FD_|VALIDATE_)" QuantLib/build-double/fd_results.txt 2>/dev/null || true + grep -E "^(XAD_|XADSPLIT_|VALIDATE_)" QuantLib/build-xad/xad_results.txt 2>/dev/null || true + grep -E "^(JIT_|JITAVX_|JIT_PHASES_|VALIDATE_)" QuantLib/build-forge/forge_results.txt 2>/dev/null || true + + - name: Upload Results + uses: actions/upload-artifact@v4 + with: + name: linux-benchmark-results + path: | + QuantLib/build-double/fd_results.txt + QuantLib/build-xad/xad_results.txt + QuantLib/build-forge/forge_results.txt + + ############################################################################## + # Windows, QL + XAD + Forge + # Comprehensive benchmark: FD (plain double) vs XAD vs Forge JIT + ############################################################################## + windows-benchmark: + name: Windows, QL + XAD + Forge + runs-on: windows-2022 + + env: + VSVARSALL: C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat + + steps: + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.QL_REPO }} + ref: ${{ env.QL_BRANCH }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_REPO }} + ref: ${{ env.XAD_BRANCH }} + path: xad + + - name: Checkout Forge + uses: actions/checkout@v4 + with: + repository: ${{ env.FORGE_REPO }} + ref: ${{ env.FORGE_BRANCH }} + path: forge + + - name: Checkout xad-forge + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_FORGE_REPO }} + ref: ${{ env.XAD_FORGE_BRANCH }} + path: xad-forge + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: choco install -y ninja + + - name: Setup Boost + run: | + $Url = "https://downloads.sourceforge.net/project/boost/boost-binaries/1.86.0/boost_1_86_0-msvc-14.3-64.exe" + (New-Object System.Net.WebClient).DownloadFile($Url, "$RUNNER_TEMP\boost.exe") + Start-Process -Wait -FilePath "$RUNNER_TEMP\boost.exe" "/SILENT","/SP-","/SUPPRESSMSGBOXES","/DIR=C:\local\boost" + echo "BOOST_ROOT=C:\local\boost" >> $env:GITHUB_ENV + + # ========================================================================= + # Build Forge C API + # ========================================================================= + - name: Build Forge C API + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd forge + cmake -B build -S api/c -G Ninja ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DFORGE_CAPI_BUILD_TESTS=OFF ^ + -DFORGE_CAPI_USE_STATIC_RUNTIME=ON ^ + -DCMAKE_INSTALL_PREFIX=%CD%\..\install + cmake --build build + cmake --install build + + # ========================================================================= + # Benchmark 1: XAD + Forge (JIT enabled) - Run first (longest build) + # ========================================================================= + - name: Configure QuantLib (XAD + Forge) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib + cmake -B build-forge -G Ninja ^ + -DCMAKE_CXX_STANDARD=17 ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DXAD_WARNINGS_PARANOID=OFF ^ + -DXAD_ENABLE_JIT=ON ^ + -DXAD_STATIC_MSVC_RUNTIME=ON ^ + -DCMAKE_PREFIX_PATH=%CD%\..\install ^ + -DQL_EXTERNAL_SUBDIRECTORIES="%CD%\..\xad;%CD%\..\xad-forge;%CD%\..\QuantLib-Risks-Cpp" ^ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ + -DQL_NULL_AS_FUNCTIONS=ON ^ + -DQL_BUILD_TEST_SUITE=OFF ^ + -DQL_BUILD_EXAMPLES=OFF ^ + -DQL_BUILD_BENCHMARK=OFF ^ + -DQLRISKS_DISABLE_AAD=OFF ^ + -DQLRISKS_ENABLE_FORGE=ON ^ + -DQLRISKS_USE_FORGE_CAPI=ON ^ + -DXAD_FORGE_USE_CAPI=ON ^ + -DQLRISKS_BUILD_BENCHMARK_AAD=ON + + - name: Build Forge benchmark (JIT on) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib\build-forge + cmake --build . --target benchmark_aad + + - name: Run Forge Benchmark (JIT on) + shell: cmd + run: | + echo ===== FORGE BENCHMARK (XAD + Forge JIT) ===== + cd QuantLib\build-forge + set PATH=%CD%\..\..\install\bin;%PATH% + QuantLib-Risks-Cpp\test-suite\benchmark-aad.exe --all > forge_results.txt 2>&1 + type forge_results.txt + + - name: Run XAD-Split vs JIT Diagnostic + shell: cmd + run: | + echo ===== DIAGNOSTIC: XAD-Split vs Forge Derivative Comparison ===== + cd QuantLib\build-forge + set PATH=%CD%\..\..\install\bin;%PATH% + QuantLib-Risks-Cpp\test-suite\benchmark-aad.exe --diagnose --diagnose-paths=100 > diagnostic_results.txt 2>&1 + type diagnostic_results.txt + + # ========================================================================= + # Benchmark 2: Plain double QuantLib (for FD benchmark) + # ========================================================================= + - name: Configure QuantLib (plain double) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib + cmake -B build-double -G Ninja ^ + -DCMAKE_CXX_STANDARD=17 ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DQL_BUILD_TEST_SUITE=OFF ^ + -DQL_BUILD_EXAMPLES=OFF ^ + -DQL_BUILD_BENCHMARK=OFF ^ + -DQL_EXTERNAL_SUBDIRECTORIES="%CD%\..\QuantLib-Risks-Cpp" ^ + -DQLRISKS_DISABLE_AAD=ON ^ + -DQLRISKS_BUILD_BENCHMARK_FD=ON + + - name: Build FD benchmark (plain double) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib\build-double + cmake --build . --target benchmark_fd + + - name: Run FD Benchmark (plain double) + shell: cmd + run: | + echo ===== FD BENCHMARK (plain double QuantLib) ===== + cd QuantLib\build-double + QuantLib-Risks-Cpp\test-suite\benchmark-fd.exe --all > fd_results.txt 2>&1 + type fd_results.txt + + # ========================================================================= + # Benchmark 3: XAD (JIT disabled) for XAD tape AAD + # ========================================================================= + - name: Configure QuantLib (XAD, JIT off) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib + cmake -B build-xad -G Ninja ^ + -DCMAKE_CXX_STANDARD=17 ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DXAD_WARNINGS_PARANOID=OFF ^ + -DXAD_ENABLE_JIT=OFF ^ + -DXAD_STATIC_MSVC_RUNTIME=ON ^ + -DQL_EXTERNAL_SUBDIRECTORIES="%CD%\..\xad;%CD%\..\QuantLib-Risks-Cpp" ^ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ + -DQL_NULL_AS_FUNCTIONS=ON ^ + -DQL_BUILD_TEST_SUITE=OFF ^ + -DQL_BUILD_EXAMPLES=OFF ^ + -DQL_BUILD_BENCHMARK=OFF ^ + -DQLRISKS_DISABLE_AAD=OFF ^ + -DQLRISKS_BUILD_BENCHMARK_AAD=ON + + - name: Build XAD benchmark (JIT off) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib\build-xad + cmake --build . --target benchmark_aad + + - name: Run XAD Benchmark (JIT off) + shell: cmd + run: | + echo ===== XAD BENCHMARK (XAD + XAD-Split, JIT disabled) ===== + cd QuantLib\build-xad + QuantLib-Risks-Cpp\test-suite\benchmark-aad.exe --all --xad-only > xad_results.txt 2>&1 + type xad_results.txt + + # ========================================================================= + # Combine and display results + # ========================================================================= + - name: Generate Combined Report + shell: pwsh + run: | + Write-Host "================================================================================" + Write-Host " COMBINED BENCHMARK RESULTS (Windows)" + Write-Host "================================================================================" + Write-Host "" + Write-Host "All benchmarks ran on the same hardware for fair comparison." + Write-Host "" + + $script = @' + import sys + import re + from collections import defaultdict + + def parse_line(line): + match = re.match(r'^(\w+)_(\w+):(.+)$', line.strip()) + if not match: + return None + method, config, data = match.groups() + results = {} + for item in data.split(';'): + if '=' in item: + paths, vals = item.split('=') + parts = vals.split(',') + mean, std, enabled = parts[0], parts[1], parts[2] + fixed = float(parts[3]) if len(parts) > 3 else 0.0 + if enabled == '1' and float(mean) > 0: + results[int(paths)] = {'mean': float(mean), 'fixed': fixed} + return method, config, results + + def parse_phases_line(line): + match = re.match(r'^JIT_PHASES_(\w+):(.+)$', line.strip()) + if not match: + return None + config, data = match.groups() + parts = data.split(',') + if len(parts) >= 3: + return config, {'phase1': float(parts[0]), 'phase2': float(parts[1]), 'phase3': float(parts[2])} + return None + + def format_num(n): + if n >= 1000: return f"{n:.0f}" + elif n >= 100: return f"{n:.1f}" + elif n >= 10: return f"{n:.1f}" + else: return f"{n:.2f}" + + def format_paths(p): + if p >= 1000: return f"{p//1000}K" + return str(p) + + def parse_validation_line(line): + match = re.match(r'^VALIDATE_(\w+)_(\w+):(.+)$', line.strip()) + if not match: + return None + config, method, data = match.groups() + parts = data.split(';') + if len(parts) != 2: + return None + pv = float(parts[0]) + sens = [float(x) for x in parts[1].split(',') if x] + return config, method, pv, sens + + def compare_sensitivities(sens1, sens2, tol_pct): + if len(sens1) != len(sens2): + return 0, max(len(sens1), len(sens2)), float('inf') + matching = 0 + max_diff = 0.0 + for s1, s2 in zip(sens1, sens2): + if abs(s1) < 1e-12 and abs(s2) < 1e-12: + matching += 1 + continue + base = max(abs(s1), abs(s2)) + rel_diff = abs(s1 - s2) / base if base > 1e-12 else 0.0 + max_diff = max(max_diff, rel_diff) + if rel_diff <= tol_pct / 100.0: + matching += 1 + return matching, len(sens1), max_diff * 100.0 + + data = defaultdict(lambda: defaultdict(dict)) + phases = {} + validation = defaultdict(dict) + for filepath in sys.argv[1:]: + # Skip XADSPLIT from forge_results.txt (use xad_results.txt instead) + is_forge_results = 'forge_results' in filepath + try: + with open(filepath) as f: + for line in f: + result = parse_line(line) + if result: + method, config, results = result + if is_forge_results and method == 'XADSPLIT': + continue + for paths, vals in results.items(): + data[config][method][paths] = vals + phase_result = parse_phases_line(line) + if phase_result: + config, phase_data = phase_result + phases[config] = phase_data + val_result = parse_validation_line(line) + if val_result: + config, method, pv, sens = val_result + validation[config][method] = (pv, sens) + except: pass + + configs = ['LITE', 'LITEEXT', 'PRODUCTION'] + names = {'LITE': 'Lite (1Y×1Y, 9 inputs)', 'LITEEXT': 'Lite-Extended (5Y×5Y, 14 inputs)', 'PRODUCTION': 'Production (5Y×5Y dual-curve, 45 inputs)'} + method_display = {'FD': 'FD', 'XAD': 'XAD', 'XADSPLIT': 'XAD-Split*', 'JIT': 'Forge*', 'JITAVX': 'Forge-AVX2*'} + col_width = 12 + + for config in configs: + if config not in data: continue + print(f"\n### {names.get(config, config)}\n") + methods = data[config] + all_paths = sorted(set(p for m in methods.values() for p in m.keys())) + if not all_paths: continue + + method_order = ['FD', 'XAD', 'XADSPLIT', 'JIT', 'JITAVX'] + present = [m for m in method_order if m in methods] + + header = f"{'Paths':>6}" + for m in present: + header += f"{method_display[m]:>{col_width}}" + print(header) + print("-" * len(header)) + + for paths in all_paths: + row = f"{format_paths(paths):>6}" + for m in present: + v = methods[m].get(paths) + if v: + row += f"{format_num(v['mean']):>{col_width}}" + else: + row += f"{'-':>{col_width}}" + print(row) + + if config in phases: + p = phases[config] + total = p['phase1'] + p['phase2'] + p['phase3'] + print(f"\n*Forge JIT setup (one-time costs, independent of paths):\n") + print(f" {'Phase':<35} {'Time (ms)':>10}") + print(f" {'-'*45}") + print(f" {'1. Curve bootstrap (XAD)':<35} {format_num(p['phase1']):>10}") + print(f" {'2. Jacobian (XAD adjoints)':<35} {format_num(p['phase2']):>10}") + print(f" {'3. JIT graph record + compile':<35} {format_num(p['phase3']):>10}") + print(f" {'-'*45}") + print(f" {'Total setup':<35} {format_num(total):>10}") + + if config in validation and validation[config]: + val_methods = validation[config] + print(f"\nValidation (at 10K paths):\n") + print(f" {'Method':<10} {'PV':>14} {'Sens':>6} {'vs FD':>8} {'FD MaxDiff':>12} {'vs XAD':>8} {'XAD MaxDiff':>12}") + print(f" {'-'*72}") + method_order = ['FD', 'XAD', 'XADSPLIT', 'JIT', 'JITAVX'] + val_display = {'FD': 'FD', 'XAD': 'XAD', 'XADSPLIT': 'XAD-Split', 'JIT': 'Forge', 'JITAVX': 'Forge-AVX2'} + fd_sens = val_methods.get('FD', (None, []))[1] + xad_sens = val_methods.get('XAD', (None, []))[1] + for m in method_order: + if m not in val_methods: continue + pv, sens = val_methods[m] + if m == 'FD': + vs_fd, fd_maxdiff = '-', '-' + elif fd_sens: + match, total, maxd = compare_sensitivities(sens, fd_sens, 1.0) + vs_fd, fd_maxdiff = f"{match}/{total}", f"{maxd:.2f}%" + else: + vs_fd, fd_maxdiff = '-', '-' + if m in ('FD', 'XAD'): + vs_xad, xad_maxdiff = '-', '-' + elif xad_sens: + match, total, maxd = compare_sensitivities(sens, xad_sens, 0.01) + vs_xad, xad_maxdiff = f"{match}/{total}", f"{maxd:.4f}%" + else: + vs_xad, xad_maxdiff = '-', '-' + print(f" {val_display[m]:<10} {pv:>14.6f} {len(sens):>6} {vs_fd:>8} {fd_maxdiff:>12} {vs_xad:>8} {xad_maxdiff:>12}") + + # Determine which methods are present across all configs + all_methods = set() + for config in configs: + if config in data: + all_methods.update(data[config].keys()) + + print("\nAll times in milliseconds (ms).") + has_jit = 'JIT' in all_methods or 'JITAVX' in all_methods + has_xadsplit = 'XADSPLIT' in all_methods + if has_jit and has_xadsplit: + print("*Forge uses the Forge JIT compiler to generate native code (scalar, 1 path at a time).") + print(" Forge-AVX2 uses AVX2 vectorization (4 paths at a time). See setup breakdown above.") + print(" XAD-Split computes Jacobian separately and re-records a small tape per path (no JIT).") + elif has_xadsplit: + print("*XAD-Split computes the Jacobian separately and re-records a small tape per path,") + print(" avoiding one large tape that records all MC paths.") + elif has_jit: + print("*Forge uses the Forge JIT compiler (scalar, 1 path at a time).") + print(" Forge-AVX2 uses AVX2 vectorization (4 paths at a time). See setup breakdown above.") + '@ + + $script | Out-File -FilePath format_results.py -Encoding utf8 + python format_results.py ` + QuantLib/build-double/fd_results.txt ` + QuantLib/build-xad/xad_results.txt ` + QuantLib/build-forge/forge_results.txt + + Write-Host "" + Write-Host "================================================================================" + + - name: Upload Results + uses: actions/upload-artifact@v4 + with: + name: windows-benchmark-results + path: | + QuantLib/build-double/fd_results.txt + QuantLib/build-xad/xad_results.txt + QuantLib/build-forge/forge_results.txt diff --git a/.github/workflows/ql-type-overhead.yaml b/.github/workflows/ql-type-overhead.yaml new file mode 100644 index 0000000..ad7e4c5 --- /dev/null +++ b/.github/workflows/ql-type-overhead.yaml @@ -0,0 +1,197 @@ +############################################################################## +# +# Type Overhead Benchmark: double vs xad::AReal +# +# Measures the passive-mode overhead of xad::AReal vs plain double +# for pricing-only workloads (no derivatives, no tape recording). +# +# Both types use the same templated priceSwaption() code, compiled into +# a single binary for a fair comparison. +# +# Only requires QuantLib + XAD (no Forge). +# +# Copyright (C) 2025 Xcelerit Computing Limited +# SPDX-License-Identifier: AGPL-3.0-or-later +# +############################################################################## + +name: Type Overhead Benchmark + +on: + push: + branches: + - main + - forge + pull_request: + branches: + - main + - forge + workflow_dispatch: + +env: + QL_REPO: lballabio/QuantLib + QL_BRANCH: master + XAD_REPO: auto-differentiation/xad + XAD_BRANCH: main + +jobs: + ############################################################################## + # Linux + ############################################################################## + linux: + name: Linux, double vs AReal + runs-on: ubuntu-latest + container: + image: ghcr.io/lballabio/quantlib-devenv:rolling + + steps: + - name: Hardware Info + run: | + echo "===== CPU =====" + lscpu | grep -E "^(Model name|CPU\(s\)|Thread|Core|Socket|CPU max MHz)" + echo "===== Memory =====" + free -h + + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.QL_REPO }} + ref: ${{ env.QL_BRANCH }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_REPO }} + ref: ${{ env.XAD_BRANCH }} + path: xad + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: | + apt-get update + apt-get install -y ninja-build cmake + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2.12 + with: + key: linux-type-overhead + max-size: 1G + + - name: Configure QuantLib (XAD) + run: | + cd QuantLib + cmake -B build -G Ninja -DBOOST_ROOT=/usr \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DXAD_WARNINGS_PARANOID=OFF \ + -DXAD_ENABLE_JIT=OFF \ + -DQL_EXTERNAL_SUBDIRECTORIES="$(pwd)/../xad;$(pwd)/../QuantLib-Risks-Cpp" \ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks \ + -DQL_NULL_AS_FUNCTIONS=ON \ + -DQL_BUILD_TEST_SUITE=OFF \ + -DQL_BUILD_EXAMPLES=OFF \ + -DQL_BUILD_BENCHMARK=OFF \ + -DQLRISKS_DISABLE_AAD=OFF \ + -DQLRISKS_BUILD_BENCHMARK_OVERHEAD=ON + + - name: Build + run: | + cd QuantLib/build + cmake --build . --target benchmark_overhead + + - name: Run Type Overhead Benchmark + run: | + cd QuantLib/build + ./QuantLib-Risks-Cpp/test-suite/benchmark-overhead --all 2>&1 | tee overhead_results.txt + + - name: Upload Results + uses: actions/upload-artifact@v4 + with: + name: linux-overhead-results + path: QuantLib/build/overhead_results.txt + + ############################################################################## + # Windows + ############################################################################## + windows: + name: Windows, double vs AReal + runs-on: windows-2022 + + env: + VSVARSALL: C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat + + steps: + - name: Checkout QuantLib + uses: actions/checkout@v4 + with: + repository: ${{ env.QL_REPO }} + ref: ${{ env.QL_BRANCH }} + path: QuantLib + + - name: Checkout XAD + uses: actions/checkout@v4 + with: + repository: ${{ env.XAD_REPO }} + ref: ${{ env.XAD_BRANCH }} + path: xad + + - name: Checkout QuantLib-Risks-Cpp + uses: actions/checkout@v4 + with: + path: QuantLib-Risks-Cpp + + - name: Setup + run: choco install -y ninja + + - name: Setup Boost + run: | + $Url = "https://downloads.sourceforge.net/project/boost/boost-binaries/1.86.0/boost_1_86_0-msvc-14.3-64.exe" + (New-Object System.Net.WebClient).DownloadFile($Url, "$RUNNER_TEMP\boost.exe") + Start-Process -Wait -FilePath "$RUNNER_TEMP\boost.exe" "/SILENT","/SP-","/SUPPRESSMSGBOXES","/DIR=C:\local\boost" + echo "BOOST_ROOT=C:\local\boost" >> $env:GITHUB_ENV + + - name: Configure QuantLib (XAD) + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib + cmake -B build -G Ninja ^ + -DCMAKE_CXX_STANDARD=17 ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DXAD_WARNINGS_PARANOID=OFF ^ + -DXAD_ENABLE_JIT=OFF ^ + -DXAD_STATIC_MSVC_RUNTIME=ON ^ + -DQL_EXTERNAL_SUBDIRECTORIES="%CD%\..\xad;%CD%\..\QuantLib-Risks-Cpp" ^ + -DQL_EXTRA_LINK_LIBRARIES=QuantLib-Risks ^ + -DQL_NULL_AS_FUNCTIONS=ON ^ + -DQL_BUILD_TEST_SUITE=OFF ^ + -DQL_BUILD_EXAMPLES=OFF ^ + -DQL_BUILD_BENCHMARK=OFF ^ + -DQLRISKS_DISABLE_AAD=OFF ^ + -DQLRISKS_BUILD_BENCHMARK_OVERHEAD=ON + + - name: Build + shell: cmd + run: | + call "%VSVARSALL%" x64 + cd QuantLib\build + cmake --build . --target benchmark_overhead + + - name: Run Type Overhead Benchmark + shell: cmd + run: | + cd QuantLib\build + QuantLib-Risks-Cpp\test-suite\benchmark-overhead.exe --all > overhead_results.txt 2>&1 + type overhead_results.txt + + - name: Upload Results + uses: actions/upload-artifact@v4 + with: + name: windows-overhead-results + path: QuantLib\build\overhead_results.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index f7e42c5..0280bb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ ############################################################################## -# +# # # This file is part of QuantLib-Risks, an adaptor module to enable using XAD with # QuantLib. XAD is a fast and comprehensive C++ library for @@ -19,17 +19,72 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# +# ############################################################################## option(QLRISKS_DISABLE_AAD "Disable using XAD for QuantLib's Real, allowing to run samples with double" OFF) +option(QLRISKS_ENABLE_FORGE "Enable Forge JIT backend via xad-forge" OFF) +option(QLRISKS_USE_FORGE_CAPI "Use Forge C API instead of C++ API for binary compatibility" OFF) add_subdirectory(ql) if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() add_subdirectory(Examples) -if(NOT QLRISKS_DISABLE_AAD) - # the test suite is not supporting double - add_subdirectory(test-suite) + +############################################################################## +# QLRisks-Forge integration via xad-forge +# NOTE: This must be defined BEFORE test-suite so tests can link to it +############################################################################## + +if(QLRISKS_ENABLE_FORGE) + message(STATUS "QLRisks-Forge: Looking for xad-forge...") + + # Pass through the C API option to xad-forge + set(XAD_FORGE_USE_CAPI ${QLRISKS_USE_FORGE_CAPI} CACHE BOOL "" FORCE) + + # Option 1: Check if xad-forge was added as subdirectory + if(TARGET xad-forge) + message(STATUS "QLRisks-Forge: Found xad-forge target (subdirectory mode)") + set(XAD_FORGE_FOUND TRUE) + endif() + + # Option 2: Try find_package for pre-built xad-forge + if(NOT XAD_FORGE_FOUND) + find_package(xad-forge CONFIG QUIET) + if(xad-forge_FOUND) + message(STATUS "QLRisks-Forge: Found xad-forge package (pre-built mode)") + set(XAD_FORGE_FOUND TRUE) + endif() + endif() + + if(XAD_FORGE_FOUND) + # Create qlrisks-forge as an INTERFACE library wrapping xad-forge + add_library(qlrisks-forge INTERFACE) + add_library(QLRisks::forge ALIAS qlrisks-forge) + + target_link_libraries(qlrisks-forge INTERFACE + XADForge::xad-forge + ) + + target_compile_definitions(qlrisks-forge INTERFACE QLRISKS_HAS_FORGE=1) + + message(STATUS "QLRisks-Forge: Configured with xad-forge") + else() + message(STATUS "QLRisks-Forge: xad-forge not found - ForgeBackend will not be available") + message(STATUS "QLRisks-Forge: To enable, either:") + message(STATUS " 1. Add xad-forge as subdirectory") + message(STATUS " 2. Set CMAKE_PREFIX_PATH to xad-forge installation") + endif() +endif() + +############################################################################## +# Test suite (after QLRisks::forge is defined) +############################################################################## + +# Add test-suite if: +# 1. AAD is enabled (for XAD-based tests and benchmarks), OR +# 2. FD benchmark is requested (works with plain double) +if(NOT QLRISKS_DISABLE_AAD OR QLRISKS_BUILD_BENCHMARK_FD) + add_subdirectory(test-suite) endif() \ No newline at end of file diff --git a/README.md b/README.md index 66c66f0..5e12c89 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ This repository contains integration headers, examples, and tests required for this integration. It is not usable stand-alone. +## JIT Compilation Support + +XAD is optimized for computing sensitivities efficiently in a single evaluation pass using adjoint mode. For workflows that require repeated evaluation across many scenarios—such as Monte Carlo simulations, XVA calculations, regulatory stress testing, or scenario-based risk analysis—XAD also supports recording computations into a [`JITGraph`](https://auto-differentiation.github.io/xad/ref/jit/) that can be compiled and re-evaluated efficiently using a JIT backend. The [xad-forge](https://github.com/da-roth/xad-forge) library provides Forge-based backends for this purpose, including vectorized AVX execution. See the [xad-forge README](https://github.com/da-roth/xad-forge#when-to-use-jit) for guidance on when JIT compilation is beneficial. + +The repository includes a [swaption benchmark](.github/workflows/ql-benchmarks.yaml) that demonstrates a hybrid workflow: curve bootstrapping with XAD's tape, followed by Monte Carlo pricing with JIT-compiled evaluation, comparing tape-based, JIT, and JIT-AVX performance. + ## Getting Started For detailed build instructions with [XAD](https://auto-differentiation.github.io) and [QuantLib](https://www.quantlib.org), please refer to the [XAD documentation site](https://auto-differentiation.github.io/quantlib-risks/cxx/). diff --git a/ql/CMakeLists.txt b/ql/CMakeLists.txt index 0b4de4c..48eae2f 100644 --- a/ql/CMakeLists.txt +++ b/ql/CMakeLists.txt @@ -36,10 +36,10 @@ if(NOT QLRISKS_DISABLE_AAD) else() target_compile_definitions(QuantLib-Risks INTERFACE QLRISKS_DISABLE_AAD=1) endif() +target_link_libraries(QuantLib-Risks INTERFACE XAD::xad) if(MSVC) target_compile_options(QuantLib-Risks INTERFACE /bigobj) endif() -target_link_libraries(QuantLib-Risks INTERFACE XAD::xad) set_target_properties(QuantLib-Risks PROPERTIES EXPORT_NAME QuantLib-Risks ) diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index c96f8ae..2b133ca 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -1,3 +1,6 @@ +option(QLRISKS_BUILD_TEST_SUITE "Build the QuantLib-Risks test suite" OFF) +option(QLRISKS_ENABLE_FORGE_TESTS "Enable Forge JIT tests (requires Forge)" OFF) + set(QLRISKS_TEST_SOURCES americanoption_xad.cpp barrieroption_xad.cpp @@ -9,14 +12,25 @@ set(QLRISKS_TEST_SOURCES forwardrateagreement_xad.cpp hestonmodel_xad.cpp swap_xad.cpp - + utilities_xad.cpp quantlibtestsuite_xad.cpp ) +# Forge JIT tests - require Forge's native code backend +# Tests QuantLib-specific JIT functionality (swaption pricing pipeline) +set(QLRISKS_FORGE_TESTS_ENABLED FALSE) +if(QLRISKS_ENABLE_FORGE_TESTS AND TARGET QLRisks::forge) + message(STATUS "QLRisks test-suite: Adding Forge JIT tests (native code backend)") + list(APPEND QLRISKS_TEST_SOURCES swaption_jit_pipeline_xad.cpp) + set(QLRISKS_FORGE_TESTS_ENABLED TRUE) +elseif(QLRISKS_ENABLE_FORGE_TESTS) + message(WARNING "QLRisks test-suite: QLRISKS_ENABLE_FORGE_TESTS=ON but QLRisks::forge not available") +endif() + set(QLRISKS_TEST_HEADERS utilities_xad.hpp) -if(QL_BUILD_TEST_SUITE) +if(QL_BUILD_TEST_SUITE OR QLRISKS_BUILD_TEST_SUITE) add_executable(QuantLib-Risks_test_suite ${QLRISKS_TEST_SOURCES} ${QLRISKS_TEST_HEADERS}) set_target_properties(QuantLib-Risks_test_suite PROPERTIES OUTPUT_NAME "quantlib-risks-test-suite") if (NOT Boost_USE_STATIC_LIBS) @@ -25,8 +39,99 @@ if(QL_BUILD_TEST_SUITE) target_link_libraries(QuantLib-Risks_test_suite PRIVATE ql_library ${QL_THREAD_LIBRARIES}) + + # ONLY link to Forge if Forge tests are actually enabled + # This is important because linking Forge brings in AVX2-compiled code + # which can cause ODR violations if not properly isolated + if(QLRISKS_FORGE_TESTS_ENABLED) + message(STATUS "QLRisks test-suite: Linking QLRisks::forge (Forge tests enabled)") + target_link_libraries(QuantLib-Risks_test_suite PRIVATE QLRisks::forge) + target_compile_definitions(QuantLib-Risks_test_suite PRIVATE QLRISKS_HAS_FORGE=1) + else() + message(STATUS "QLRisks test-suite: NOT linking Forge (Forge tests disabled)") + endif() + if (QL_INSTALL_TEST_SUITE) install(TARGETS QuantLib-Risks_test_suite RUNTIME DESTINATION ${QL_INSTALL_BINDIR}) endif() add_test(NAME QuantLib-Risks_test_suite COMMAND QuantLib-Risks_test_suite --log_level=message) +endif() + +# ============================================================================= +# Benchmark - Split FD/AAD executables for fair comparison +# ============================================================================= + +# FD benchmark - Finite Differences using plain double QuantLib +# This is built WITHOUT XAD to ensure fair FD vs AAD comparison +option(QLRISKS_BUILD_BENCHMARK_FD "Build FD benchmark (plain double, no XAD)" OFF) +if(QLRISKS_BUILD_BENCHMARK_FD) + message(STATUS "QLRisks: Building FD benchmark (plain double)") + add_executable(benchmark_fd + benchmark_fd.cpp + benchmark_common.hpp + benchmark_pricing.hpp + PlatformInfo.hpp + ) + set_target_properties(benchmark_fd PROPERTIES + OUTPUT_NAME "benchmark-fd") + target_link_libraries(benchmark_fd PRIVATE + ql_library + ${QL_THREAD_LIBRARIES}) + + if (QL_INSTALL_TEST_SUITE) + install(TARGETS benchmark_fd RUNTIME DESTINATION ${QL_INSTALL_BINDIR}) + endif() +endif() + +# AAD benchmark - XAD tape-based AAD and optionally Forge JIT +# Built with XAD-enabled QuantLib, optionally links Forge for JIT benchmarks +option(QLRISKS_BUILD_BENCHMARK_AAD "Build AAD benchmark (XAD tape, optionally Forge JIT)" OFF) +if(QLRISKS_BUILD_BENCHMARK_AAD) + message(STATUS "QLRisks: Building AAD benchmark") + add_executable(benchmark_aad + benchmark_aad.cpp + benchmark_common.hpp + benchmark_pricing.hpp + PlatformInfo.hpp + ) + set_target_properties(benchmark_aad PROPERTIES + OUTPUT_NAME "benchmark-aad") + target_link_libraries(benchmark_aad PRIVATE + ql_library + ${QL_THREAD_LIBRARIES}) + + # Link Forge if available for JIT benchmarks + if(TARGET QLRisks::forge) + message(STATUS "QLRisks AAD benchmark: Linking QLRisks::forge (JIT benchmarks enabled)") + target_link_libraries(benchmark_aad PRIVATE QLRisks::forge) + target_compile_definitions(benchmark_aad PRIVATE QLRISKS_HAS_FORGE=1) + else() + message(STATUS "QLRisks AAD benchmark: Building without Forge (XAD tape only)") + endif() + + if (QL_INSTALL_TEST_SUITE) + install(TARGETS benchmark_aad RUNTIME DESTINATION ${QL_INSTALL_BINDIR}) + endif() +endif() + +# Type overhead benchmark - Compares double vs AReal pricing-only +# Built with XAD-enabled QuantLib (needs AReal available) +option(QLRISKS_BUILD_BENCHMARK_OVERHEAD "Build type overhead benchmark (double vs AReal pricing)" OFF) +if(QLRISKS_BUILD_BENCHMARK_OVERHEAD) + message(STATUS "QLRisks: Building type overhead benchmark (double vs AReal)") + add_executable(benchmark_overhead + benchmark_type_overhead.cpp + benchmark_common.hpp + benchmark_pricing.hpp + PlatformInfo.hpp + ) + set_target_properties(benchmark_overhead PROPERTIES + OUTPUT_NAME "benchmark-overhead") + target_link_libraries(benchmark_overhead PRIVATE + ql_library + ${QL_THREAD_LIBRARIES}) + + if (QL_INSTALL_TEST_SUITE) + install(TARGETS benchmark_overhead RUNTIME DESTINATION ${QL_INSTALL_BINDIR}) + endif() endif() \ No newline at end of file diff --git a/test-suite/PlatformInfo.hpp b/test-suite/PlatformInfo.hpp new file mode 100644 index 0000000..a8d201f --- /dev/null +++ b/test-suite/PlatformInfo.hpp @@ -0,0 +1,212 @@ +/******************************************************************************* + + Platform Information Utilities + + This header provides cross-platform functions for detecting and reporting + system information such as CPU, memory, OS, compiler, and SIMD capabilities. + + Adapted from XAD's PlatformInfo.hpp for QuantLib-Risks benchmarks. + + Copyright (C) 2010-2025 Xcelerit Computing Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) +#include +#endif +#endif + +namespace platform_info +{ + +/// Get CPU brand string (e.g., "Intel Core i7-9700K") +inline std::string getCpuInfo() +{ +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + char brand[49] = {0}; + unsigned int regs[4]; + +#ifdef _WIN32 + __cpuid(reinterpret_cast(regs), 0x80000000); +#else + __get_cpuid(0x80000000, ®s[0], ®s[1], ®s[2], ®s[3]); +#endif + + if (regs[0] >= 0x80000004) + { + for (unsigned int i = 0; i < 3; ++i) + { +#ifdef _WIN32 + __cpuid(reinterpret_cast(regs), 0x80000002 + i); +#else + __get_cpuid(0x80000002 + i, ®s[0], ®s[1], ®s[2], ®s[3]); +#endif + std::memcpy(brand + i * 16, regs, 16); + } + std::string result(brand); + size_t start = result.find_first_not_of(' '); + if (start != std::string::npos) + result = result.substr(start); + return result; + } +#endif + return "Unknown CPU"; +} + +/// Get OS/platform information (e.g., "Windows 10.0 (Build 19041)" or "Linux 5.4.0") +inline std::string getPlatformInfo() +{ +#ifdef _WIN32 + typedef LONG(WINAPI * RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + HMODULE hMod = GetModuleHandleW(L"ntdll.dll"); + if (hMod) + { + auto RtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion"); + if (RtlGetVersion) + { + RTL_OSVERSIONINFOW rovi = {0}; + rovi.dwOSVersionInfoSize = sizeof(rovi); + if (RtlGetVersion(&rovi) == 0) + { + std::ostringstream oss; + oss << "Windows " << rovi.dwMajorVersion << "." << rovi.dwMinorVersion << " (Build " + << rovi.dwBuildNumber << ")"; + return oss.str(); + } + } + } + return "Windows"; +#else + struct utsname buf; + if (uname(&buf) == 0) + { + std::ostringstream oss; + oss << buf.sysname << " " << buf.release; + return oss.str(); + } + return "Unknown"; +#endif +} + +/// Get total system memory (e.g., "16 GB") +inline std::string getMemoryInfo() +{ +#ifdef _WIN32 + MEMORYSTATUSEX memInfo; + memInfo.dwLength = sizeof(MEMORYSTATUSEX); + if (GlobalMemoryStatusEx(&memInfo)) + { + double gb = static_cast(memInfo.ullTotalPhys) / (1024.0 * 1024.0 * 1024.0); + std::ostringstream oss; + oss << std::fixed << std::setprecision(0) << gb << " GB"; + return oss.str(); + } +#else + long pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + if (pages > 0 && page_size > 0) + { + double gb = static_cast(pages) * page_size / (1024.0 * 1024.0 * 1024.0); + std::ostringstream oss; + oss << std::fixed << std::setprecision(0) << gb << " GB"; + return oss.str(); + } +#endif + return "Unknown"; +} + +/// Get compiler information (e.g., "GCC 11.2.0" or "MSVC 19.29 (Release)") +inline std::string getCompilerInfo() +{ +#if defined(_MSC_VER) + std::ostringstream oss; + oss << "MSVC " << _MSC_VER / 100 << "." << _MSC_VER % 100; +#if defined(_DEBUG) + oss << " (Debug)"; +#else + oss << " (Release)"; +#endif + return oss.str(); +#elif defined(__clang__) + std::ostringstream oss; + oss << "Clang " << __clang_major__ << "." << __clang_minor__ << "." << __clang_patchlevel__; + return oss.str(); +#elif defined(__GNUC__) + std::ostringstream oss; + oss << "GCC " << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__; + return oss.str(); +#else + return "Unknown Compiler"; +#endif +} + +/// Get supported SIMD instruction sets (e.g., "SSE3, SSE4.1, SSE4.2, AVX, AVX2") +inline std::string getSimdInfo() +{ +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + unsigned int regs[4]; + std::vector features; + +#ifdef _WIN32 + __cpuid(reinterpret_cast(regs), 1); +#else + __get_cpuid(1, ®s[0], ®s[1], ®s[2], ®s[3]); +#endif + + if (regs[2] & (1 << 0)) features.push_back("SSE3"); + if (regs[2] & (1 << 19)) features.push_back("SSE4.1"); + if (regs[2] & (1 << 20)) features.push_back("SSE4.2"); + if (regs[2] & (1 << 28)) features.push_back("AVX"); + +#ifdef _WIN32 + __cpuidex(reinterpret_cast(regs), 7, 0); +#else + __get_cpuid_count(7, 0, ®s[0], ®s[1], ®s[2], ®s[3]); +#endif + + if (regs[1] & (1 << 5)) features.push_back("AVX2"); + if (regs[1] & (1 << 16)) features.push_back("AVX512F"); + + if (features.empty()) + return "None detected"; + + std::ostringstream oss; + for (size_t i = 0; i < features.size(); ++i) + { + if (i > 0) oss << ", "; + oss << features[i]; + } + return oss.str(); +#else + return "N/A (non-x86)"; +#endif +} + +} // namespace platform_info diff --git a/test-suite/benchmark_aad.cpp b/test-suite/benchmark_aad.cpp new file mode 100644 index 0000000..f8ee41a --- /dev/null +++ b/test-suite/benchmark_aad.cpp @@ -0,0 +1,2111 @@ +/******************************************************************************* + * + * QuantLib-Risks Swaption Benchmark - AAD Runner + * + * AAD benchmarks using XAD tape, Forge JIT, and Forge JIT-AVX. + * This executable is compiled WITH XAD (and optionally Forge). + * + * Usage: + * ./benchmark_aad [--lite|--lite-extended|--production|--all] [--quick] [--xad-only] + * + * Output format is designed to be parsed and combined with FD results. + * + * Copyright (C) 2025 Xcelerit Computing Limited + * SPDX-License-Identifier: AGPL-3.0-or-later + * + ******************************************************************************/ + +#include "benchmark_common.hpp" +#include "benchmark_pricing.hpp" + +// XAD includes +#include + +// Forge JIT backends (conditionally included) +#if defined(QLRISKS_HAS_FORGE) +#include +#include +#endif + +#include +#include +#include + +using namespace benchmark; +using Clock = std::chrono::high_resolution_clock; +using DurationMs = std::chrono::duration; + +// Use QuantLib's Real which is xad::AReal via qlrisks.hpp +using RealAD = QuantLib::Real; +using tape_type = RealAD::tape_type; + +// ============================================================================ +// XAD Tape-based AAD Benchmark (unified single/dual-curve) +// ============================================================================ + +template +void runXADBenchmarkT(const BenchmarkConfig& config, const LMMSetup& setup, + Size nrTrails, size_t warmup, size_t bench, + double& mean, double& stddev, + ValidationResult* validation = nullptr) +{ + std::vector times; + + for (size_t iter = 0; iter < warmup + bench; ++iter) + { + auto t_start = Clock::now(); + + tape_type tape; + + // Register forecasting curve inputs + std::vector depositRates(config.numDeposits); + std::vector swapRatesAD(config.numSwaps); + for (Size idx = 0; idx < config.numDeposits; ++idx) + depositRates[idx] = config.depoRates[idx]; + for (Size idx = 0; idx < config.numSwaps; ++idx) + swapRatesAD[idx] = config.swapRates[idx]; + + // Register discounting curve inputs (OIS) for dual-curve + std::vector oisDepoRates; + std::vector oisSwapRatesAD; + if constexpr (UseDualCurve) + { + oisDepoRates.resize(config.numOisDeposits); + oisSwapRatesAD.resize(config.numOisSwaps); + for (Size idx = 0; idx < config.numOisDeposits; ++idx) + oisDepoRates[idx] = config.oisDepoRates[idx]; + for (Size idx = 0; idx < config.numOisSwaps; ++idx) + oisSwapRatesAD[idx] = config.oisSwapRates[idx]; + } + + tape.registerInputs(depositRates); + tape.registerInputs(swapRatesAD); + if constexpr (UseDualCurve) + { + tape.registerInputs(oisDepoRates); + tape.registerInputs(oisSwapRatesAD); + } + tape.newRecording(); + + // Price using appropriate function + RealAD price; + if constexpr (UseDualCurve) + price = priceSwaptionDualCurve( + config, setup, depositRates, swapRatesAD, oisDepoRates, oisSwapRatesAD, nrTrails); + else + price = priceSwaption(config, setup, depositRates, swapRatesAD, nrTrails); + + // Compute adjoints + tape.registerOutput(price); + derivative(price) = 1.0; + tape.computeAdjoints(); + + auto t_end = Clock::now(); + + if (iter >= warmup) + { + times.push_back(DurationMs(t_end - t_start).count()); + } + + // Capture validation data on first iteration (before clearing tape) + if (validation && iter == 0) + { + validation->method = "XAD"; + validation->pv = value(price); + validation->sensitivities.resize(config.numMarketQuotes()); + Size q = 0; + for (Size idx = 0; idx < config.numDeposits; ++idx) + validation->sensitivities[q++] = derivative(depositRates[idx]); + for (Size idx = 0; idx < config.numSwaps; ++idx) + validation->sensitivities[q++] = derivative(swapRatesAD[idx]); + if constexpr (UseDualCurve) + { + for (Size idx = 0; idx < config.numOisDeposits; ++idx) + validation->sensitivities[q++] = derivative(oisDepoRates[idx]); + for (Size idx = 0; idx < config.numOisSwaps; ++idx) + validation->sensitivities[q++] = derivative(oisSwapRatesAD[idx]); + } + } + + tape.clearAll(); + } + + mean = computeMean(times); + stddev = computeStddev(times); +} + +// Convenience wrappers for backward compatibility +inline void runXADBenchmark(const BenchmarkConfig& config, const LMMSetup& setup, + Size nrTrails, size_t warmup, size_t bench, + double& mean, double& stddev, + ValidationResult* validation = nullptr) +{ + runXADBenchmarkT(config, setup, nrTrails, warmup, bench, mean, stddev, validation); +} + +inline void runXADBenchmarkDualCurve(const BenchmarkConfig& config, const LMMSetup& setup, + Size nrTrails, size_t warmup, size_t bench, + double& mean, double& stddev, + ValidationResult* validation = nullptr) +{ + runXADBenchmarkT(config, setup, nrTrails, warmup, bench, mean, stddev, validation); +} + +// ============================================================================ +// Chain Rule Helper (used by XAD-Split and JIT methods) +// ============================================================================ + +inline void applyChainRule(const double* __restrict jacobian, + const double* __restrict derivatives, + double* __restrict result, + std::size_t numIntermediates, + std::size_t numInputs) +{ + for (std::size_t j = 0; j < numInputs; ++j) + result[j] = 0.0; + + for (std::size_t i = 0; i < numIntermediates; ++i) + { + const double deriv_i = derivatives[i]; + const double* jac_row = jacobian + i * numInputs; + for (std::size_t j = 0; j < numInputs; ++j) + { + result[j] += deriv_i * jac_row[j]; + } + } +} + +// ============================================================================ +// JIT Helper Structures and Functions +// ============================================================================ + +// Result of Phase 1: Curve bootstrap and Jacobian computation +struct CurveSetupResult { + Array initRates; // Forward rates from LMM process + RealAD swapRate; // Fair swap rate + std::vector intermediates; // All intermediates (for tape) + std::vector jacobian; // Jacobian matrix (row-major) + ext::shared_ptr process; // LMM process for path evolution + Size numIntermediates; // Number of intermediates + Size numMarketQuotes; // Number of market inputs + // Additional intermediates for dual-curve (OIS discount factors as doubles) + std::vector oisDiscountFactors; +}; + +// Phase 1 for Single-Curve: Build curve, extract intermediates, compute Jacobian +CurveSetupResult buildSingleCurveAndJacobian( + const BenchmarkConfig& config, + const LMMSetup& setup, + tape_type& tape) +{ + CurveSetupResult result; + result.numMarketQuotes = config.numMarketQuotes(); + result.numIntermediates = config.size + 1; // forward rates + swap rate + + // Register inputs + std::vector depositRates(config.numDeposits); + std::vector swapRatesAD(config.numSwaps); + for (Size idx = 0; idx < config.numDeposits; ++idx) + depositRates[idx] = config.depoRates[idx]; + for (Size idx = 0; idx < config.numSwaps; ++idx) + swapRatesAD[idx] = config.swapRates[idx]; + + tape.registerInputs(depositRates); + tape.registerInputs(swapRatesAD); + tape.newRecording(); + + // Build curve and get intermediates + RelinkableHandle euriborTS; + auto euribor6m = ext::make_shared(euriborTS); + euribor6m->addFixing(Date(2, September, 2005), 0.04); + + std::vector> instruments; + for (Size idx = 0; idx < config.numDeposits; ++idx) + { + auto depoQuote = ext::make_shared(depositRates[idx]); + instruments.push_back(ext::make_shared( + Handle(depoQuote), config.depoTenors[idx], setup.fixingDays, + setup.calendar, ModifiedFollowing, true, setup.dayCounter)); + } + for (Size idx = 0; idx < config.numSwaps; ++idx) + { + auto swapQuote = ext::make_shared(swapRatesAD[idx]); + instruments.push_back(ext::make_shared( + Handle(swapQuote), config.swapTenors[idx], + setup.calendar, Annual, Unadjusted, Thirty360(Thirty360::BondBasis), + euribor6m)); + } + + auto yieldCurve = ext::make_shared>( + setup.settlementDate, instruments, setup.dayCounter); + yieldCurve->enableExtrapolation(); + + // Extract zero rates + std::vector curveDates; + std::vector zeroRates; + curveDates.push_back(setup.settlementDate); + zeroRates.push_back(yieldCurve->zeroRate(setup.settlementDate, setup.dayCounter, Continuous).rate()); + Date endDate = setup.settlementDate + config.curveEndYears * Years; + curveDates.push_back(endDate); + zeroRates.push_back(yieldCurve->zeroRate(endDate, setup.dayCounter, Continuous).rate()); + + std::vector zeroRates_ql; + for (const auto& r : zeroRates) zeroRates_ql.push_back(r); + + // Build LMM process + RelinkableHandle termStructure; + ext::shared_ptr index(new Euribor6M(termStructure)); + index->addFixing(Date(2, September, 2005), 0.04); + termStructure.linkTo(ext::make_shared(curveDates, zeroRates_ql, setup.dayCounter)); + + result.process = ext::make_shared(config.size, index); + result.process->setCovarParam(ext::shared_ptr( + new LfmCovarianceProxy( + ext::make_shared( + result.process->fixingTimes(), 0.291, 1.483, 0.116, 0.00001), + ext::make_shared(config.size, 0.5)))); + + ext::shared_ptr fwdSwap( + new VanillaSwap(Swap::Receiver, 1.0, + setup.schedule, 0.05, setup.dayCounter, + setup.schedule, index, 0.0, index->dayCounter())); + fwdSwap->setPricingEngine(ext::make_shared( + index->forwardingTermStructure())); + result.swapRate = fwdSwap->fairRate(); + + // Extract intermediates (forward rates + swap rate) + result.initRates = result.process->initialValues(); + result.intermediates.resize(result.numIntermediates); + for (Size k = 0; k < config.size; ++k) + result.intermediates[k] = result.initRates[k]; + result.intermediates[config.size] = result.swapRate; + + // Register intermediates as outputs + tape.registerOutputs(result.intermediates); + + // Compute Jacobian: d(intermediates) / d(market inputs) + result.jacobian.resize(result.numIntermediates * result.numMarketQuotes); + for (Size i = 0; i < result.numIntermediates; ++i) + { + tape.clearDerivatives(); + derivative(result.intermediates[i]) = 1.0; + tape.computeAdjoints(); + for (Size j = 0; j < config.numDeposits; ++j) + result.jacobian[i * result.numMarketQuotes + j] = derivative(depositRates[j]); + for (Size j = 0; j < config.numSwaps; ++j) + result.jacobian[i * result.numMarketQuotes + config.numDeposits + j] = derivative(swapRatesAD[j]); + } + + tape.deactivate(); + + return result; +} + +// Phase 1 for Dual-Curve: Build both curves, extract intermediates, compute Jacobian +CurveSetupResult buildDualCurveAndJacobian( + const BenchmarkConfig& config, + const LMMSetup& setup, + tape_type& tape) +{ + CurveSetupResult result; + result.numMarketQuotes = config.numMarketQuotes(); + // Intermediates: forward rates + swap rate + OIS discount factors + result.numIntermediates = config.size + 1 + config.size; // 2*size + 1 + + // Register forecasting curve inputs + std::vector depositRates(config.numDeposits); + std::vector swapRatesAD(config.numSwaps); + for (Size idx = 0; idx < config.numDeposits; ++idx) + depositRates[idx] = config.depoRates[idx]; + for (Size idx = 0; idx < config.numSwaps; ++idx) + swapRatesAD[idx] = config.swapRates[idx]; + + // Register discounting curve inputs (OIS) + std::vector oisDepoRates(config.numOisDeposits); + std::vector oisSwapRatesAD(config.numOisSwaps); + for (Size idx = 0; idx < config.numOisDeposits; ++idx) + oisDepoRates[idx] = config.oisDepoRates[idx]; + for (Size idx = 0; idx < config.numOisSwaps; ++idx) + oisSwapRatesAD[idx] = config.oisSwapRates[idx]; + + tape.registerInputs(depositRates); + tape.registerInputs(swapRatesAD); + tape.registerInputs(oisDepoRates); + tape.registerInputs(oisSwapRatesAD); + tape.newRecording(); + + // Build FORECASTING curve (Euribor deposits + swaps) + RelinkableHandle euriborTS; + auto euribor6m = ext::make_shared(euriborTS); + euribor6m->addFixing(Date(2, September, 2005), 0.04); + + std::vector> forecastingInstruments; + for (Size idx = 0; idx < config.numDeposits; ++idx) + { + auto depoQuote = ext::make_shared(depositRates[idx]); + forecastingInstruments.push_back(ext::make_shared( + Handle(depoQuote), config.depoTenors[idx], setup.fixingDays, + setup.calendar, ModifiedFollowing, true, setup.dayCounter)); + } + for (Size idx = 0; idx < config.numSwaps; ++idx) + { + auto swapQuote = ext::make_shared(swapRatesAD[idx]); + forecastingInstruments.push_back(ext::make_shared( + Handle(swapQuote), config.swapTenors[idx], + setup.calendar, Annual, Unadjusted, Thirty360(Thirty360::BondBasis), + euribor6m)); + } + + auto forecastingCurve = ext::make_shared>( + setup.settlementDate, forecastingInstruments, setup.dayCounter); + forecastingCurve->enableExtrapolation(); + euriborTS.linkTo(forecastingCurve); + + // Build DISCOUNTING curve (OIS deposits + swaps) + RelinkableHandle oisTS; + auto eonia = ext::make_shared(oisTS); + + std::vector> discountingInstruments; + for (Size idx = 0; idx < config.numOisDeposits; ++idx) + { + auto oisDepoQuote = ext::make_shared(oisDepoRates[idx]); + discountingInstruments.push_back(ext::make_shared( + Handle(oisDepoQuote), config.oisDepoTenors[idx], setup.fixingDays, + setup.calendar, ModifiedFollowing, true, Actual360())); + } + for (Size idx = 0; idx < config.numOisSwaps; ++idx) + { + auto oisSwapQuote = ext::make_shared(oisSwapRatesAD[idx]); + discountingInstruments.push_back(ext::make_shared( + 2, config.oisSwapTenors[idx], Handle(oisSwapQuote), eonia)); + } + + auto discountingCurve = ext::make_shared>( + setup.settlementDate, discountingInstruments, setup.dayCounter); + discountingCurve->enableExtrapolation(); + oisTS.linkTo(discountingCurve); + + // Extract zero rates for LMM from forecasting curve + std::vector curveDates; + std::vector zeroRates; + curveDates.push_back(setup.settlementDate); + zeroRates.push_back(forecastingCurve->zeroRate(setup.settlementDate, setup.dayCounter, Continuous).rate()); + Date endDate = setup.settlementDate + config.curveEndYears * Years; + curveDates.push_back(endDate); + zeroRates.push_back(forecastingCurve->zeroRate(endDate, setup.dayCounter, Continuous).rate()); + + std::vector zeroRates_ql; + for (const auto& r : zeroRates) zeroRates_ql.push_back(r); + + // Build LMM process using forecasting curve + RelinkableHandle termStructure; + ext::shared_ptr index(new Euribor6M(termStructure)); + index->addFixing(Date(2, September, 2005), 0.04); + termStructure.linkTo(ext::make_shared(curveDates, zeroRates_ql, setup.dayCounter)); + + result.process = ext::make_shared(config.size, index); + result.process->setCovarParam(ext::shared_ptr( + new LfmCovarianceProxy( + ext::make_shared( + result.process->fixingTimes(), 0.291, 1.483, 0.116, 0.00001), + ext::make_shared(config.size, 0.5)))); + + // Get swap rate (using OIS curve for discounting) + ext::shared_ptr fwdSwap( + new VanillaSwap(Swap::Receiver, 1.0, + setup.schedule, 0.05, setup.dayCounter, + setup.schedule, index, 0.0, index->dayCounter())); + fwdSwap->setPricingEngine(ext::make_shared( + Handle(discountingCurve))); + result.swapRate = fwdSwap->fairRate(); + + // Extract intermediates: + // [0, size-1]: forward rates from LMM process + // [size]: swap rate + // [size+1, 2*size]: OIS discount factors at accrual end times + result.initRates = result.process->initialValues(); + result.intermediates.resize(result.numIntermediates); + + // Forward rates + for (Size k = 0; k < config.size; ++k) + result.intermediates[k] = result.initRates[k]; + + // Swap rate + result.intermediates[config.size] = result.swapRate; + + // OIS discount factors + for (Size k = 0; k < config.size; ++k) + { + Time t = setup.accrualEnd[k]; + result.intermediates[config.size + 1 + k] = discountingCurve->discount(t); + } + + // Register intermediates as outputs + tape.registerOutputs(result.intermediates); + + // Compute Jacobian via XAD adjoints + result.jacobian.resize(result.numIntermediates * result.numMarketQuotes); + + for (Size i = 0; i < result.numIntermediates; ++i) + { + tape.clearDerivatives(); + derivative(result.intermediates[i]) = 1.0; + tape.computeAdjoints(); + + Size col = 0; + // Forecasting inputs + for (Size j = 0; j < config.numDeposits; ++j) + result.jacobian[i * result.numMarketQuotes + col++] = derivative(depositRates[j]); + for (Size j = 0; j < config.numSwaps; ++j) + result.jacobian[i * result.numMarketQuotes + col++] = derivative(swapRatesAD[j]); + // Discounting inputs + for (Size j = 0; j < config.numOisDeposits; ++j) + result.jacobian[i * result.numMarketQuotes + col++] = derivative(oisDepoRates[j]); + for (Size j = 0; j < config.numOisSwaps; ++j) + result.jacobian[i * result.numMarketQuotes + col++] = derivative(oisSwapRatesAD[j]); + } + + tape.deactivate(); + + return result; +} + +// ============================================================================ +// Shared Payoff Recording - used by both JIT and XAD-Split +// ============================================================================ + +// Variables holder for payoff computation (templated on AD type) +template +struct PayoffVariables { + std::vector initRates; + ADType swapRate; + std::vector oisDiscounts; // Empty for single-curve + std::vector randoms; // For JIT only; XAD-Split uses plain doubles +}; + +// Custom LMM evolve function using LOCAL arrays instead of process's mutable members. +// This fixes XAD-Split tape recording issues caused by mutable state in process->evolve(). +// Implements the same predictor-corrector scheme as LiborForwardModelProcess::evolve(). +template +void evolveLMM( + std::vector& asset, + const ext::shared_ptr& process, + double t0, + double dt, + Size numFactors, + const RandomsType& randoms, + Size randomOffset) +{ + const Size size = asset.size(); + const Size m = process->nextIndexReset(t0); + const ADType sdt = ADType(std::sqrt(dt)); + + // Get covariance parameters (these are constant, not AD) + Matrix diff = process->covarParam()->diffusion(t0, Array()); + Matrix covariance = process->covarParam()->covariance(t0, Array()); + + // Get accrual periods (constants, not AD) + const std::vector