mirror of
https://github.com/zama-ai/tfhe-rs.git
synced 2026-01-09 22:57:59 -05:00
chore(ci): check for performance regression and create report
After running performances regression benchmarks, a performance changes checking is executed. It will fetch results data with an external tool then it will look for anomaly in changes. Finally it will produce a report as an issue comment with any anomaly display in a Markdown array. A folded section of the report message contains all the results from the benchmark. Note that a fully custom benchmark triggered from an issue comment would not generate a report. In addition HPU performance regression benchmark is not supported yet.
This commit is contained in:
138
.github/workflows/benchmark_perf_regression.yml
vendored
138
.github/workflows/benchmark_perf_regression.yml
vendored
@@ -41,7 +41,12 @@ jobs:
|
|||||||
slab-backend: ${{ steps.set_slab_details.outputs.backend }}
|
slab-backend: ${{ steps.set_slab_details.outputs.backend }}
|
||||||
slab-profile: ${{ steps.set_slab_details.outputs.profile }}
|
slab-profile: ${{ steps.set_slab_details.outputs.profile }}
|
||||||
hardware-name: ${{ steps.get_hardware_name.outputs.name }}
|
hardware-name: ${{ steps.get_hardware_name.outputs.name }}
|
||||||
|
tfhe-backend: ${{ steps.set_regression_details.outputs.tfhe-backend }}
|
||||||
|
selected-regression-profile: ${{ steps.set_regression_details.outputs.selected-profile }}
|
||||||
custom-env: ${{ steps.get_custom_env.outputs.custom_env }}
|
custom-env: ${{ steps.get_custom_env.outputs.custom_env }}
|
||||||
|
permissions:
|
||||||
|
# Needed to react to benchmark command in issue comment
|
||||||
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout tfhe-rs repo
|
- name: Checkout tfhe-rs repo
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
@@ -49,23 +54,27 @@ jobs:
|
|||||||
persist-credentials: 'false'
|
persist-credentials: 'false'
|
||||||
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
||||||
|
|
||||||
- name: Generate cpu benchmarks command from label
|
- name: Generate CPU benchmarks command from label
|
||||||
if: (github.event_name == 'pull_request' && contains(github.event.label.name, 'bench-perfs-cpu'))
|
if: (github.event_name == 'pull_request' && contains(github.event.label.name, 'bench-perfs-cpu'))
|
||||||
run: |
|
run: |
|
||||||
echo "DEFAULT_BENCH_OPTIONS=--backend cpu" >> "${GITHUB_ENV}"
|
echo "DEFAULT_BENCH_OPTIONS=--backend cpu" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Generate cpu benchmarks command from label
|
- name: Generate GPU benchmarks command from label
|
||||||
if: (github.event_name == 'pull_request' && contains(github.event.label.name, 'bench-perfs-gpu'))
|
if: (github.event_name == 'pull_request' && contains(github.event.label.name, 'bench-perfs-gpu'))
|
||||||
run: |
|
run: |
|
||||||
echo "DEFAULT_BENCH_OPTIONS=--backend gpu" >> "${GITHUB_ENV}"
|
echo "DEFAULT_BENCH_OPTIONS=--backend gpu" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
# TODO add support for HPU backend
|
# TODO add support for HPU backend
|
||||||
|
|
||||||
|
- name: Install Python requirements
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -r ci/perf_regression/requirements.txt
|
||||||
|
|
||||||
- name: Generate cargo commands and env from label
|
- name: Generate cargo commands and env from label
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
python3 ci/perf_regression.py parse_profile --issue-comment "/bench ${DEFAULT_BENCH_OPTIONS}"
|
python3 ci/perf_regression/perf_regression.py parse_profile --issue-comment "/bench ${DEFAULT_BENCH_OPTIONS}"
|
||||||
echo "COMMANDS=$(cat ci/perf_regression_generated_commands.json)" >> "${GITHUB_ENV}"
|
echo "COMMANDS=$(cat ci/perf_regression/perf_regression_generated_commands.json)" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Dump issue comment into file # To avoid possible code-injection
|
- name: Dump issue comment into file # To avoid possible code-injection
|
||||||
if: github.event_name == 'issue_comment'
|
if: github.event_name == 'issue_comment'
|
||||||
@@ -77,8 +86,15 @@ jobs:
|
|||||||
- name: Generate cargo commands and env
|
- name: Generate cargo commands and env
|
||||||
if: github.event_name == 'issue_comment'
|
if: github.event_name == 'issue_comment'
|
||||||
run: |
|
run: |
|
||||||
python3 ci/perf_regression.py parse_profile --issue-comment "$(cat dumped_comment.txt)"
|
python3 ci/perf_regression/perf_regression.py parse_profile --issue-comment "$(cat dumped_comment.txt)"
|
||||||
echo "COMMANDS=$(cat ci/perf_regression_generated_commands.json)" >> "${GITHUB_ENV}"
|
echo "COMMANDS=$(cat ci/perf_regression/perf_regression_generated_commands.json)" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Acknowledge issue comment
|
||||||
|
if: github.event_name == 'issue_comment'
|
||||||
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
|
with:
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: +1
|
||||||
|
|
||||||
- name: Set commands output
|
- name: Set commands output
|
||||||
id: set_commands
|
id: set_commands
|
||||||
@@ -88,8 +104,8 @@ jobs:
|
|||||||
- name: Set Slab details outputs
|
- name: Set Slab details outputs
|
||||||
id: set_slab_details
|
id: set_slab_details
|
||||||
run: |
|
run: |
|
||||||
echo "backend=$(cat ci/perf_regression_slab_backend_config.txt)" >> "${GITHUB_OUTPUT}"
|
echo "backend=$(cat ci/perf_regression/perf_regression_slab_backend_config.txt)" >> "${GITHUB_OUTPUT}"
|
||||||
echo "profile=$(cat ci/perf_regression_slab_profile_config.txt)" >> "${GITHUB_OUTPUT}"
|
echo "profile=$(cat ci/perf_regression/perf_regression_slab_profile_config.txt)" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Get hardware name
|
- name: Get hardware name
|
||||||
id: get_hardware_name
|
id: get_hardware_name
|
||||||
@@ -97,10 +113,16 @@ jobs:
|
|||||||
HARDWARE_NAME=$(python3 ci/hardware_finder.py "${{ steps.set_slab_details.outputs.backend }}" "${{ steps.set_slab_details.outputs.profile }}");
|
HARDWARE_NAME=$(python3 ci/hardware_finder.py "${{ steps.set_slab_details.outputs.backend }}" "${{ steps.set_slab_details.outputs.profile }}");
|
||||||
echo "name=${HARDWARE_NAME}" >> "${GITHUB_OUTPUT}"
|
echo "name=${HARDWARE_NAME}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Set regression details outputs
|
||||||
|
id: set_regression_details
|
||||||
|
run: |
|
||||||
|
echo "tfhe-backend=$(cat ci/perf_regression/perf_regression_tfhe_rs_backend_config.txt)" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "selected-profile=$(cat ci/perf_regression/perf_regression_selected_profile_config.txt)" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Get custom env vars
|
- name: Get custom env vars
|
||||||
id: get_custom_env
|
id: get_custom_env
|
||||||
run: |
|
run: |
|
||||||
echo "custom_env=$(cat ci/perf_regression_custom_env.sh)" >> "${GITHUB_OUTPUT}"
|
echo "custom_env=$(cat ci/perf_regression/perf_regression_custom_env.sh)" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
setup-instance:
|
setup-instance:
|
||||||
name: benchmark_perf_regression/setup-instance
|
name: benchmark_perf_regression/setup-instance
|
||||||
@@ -134,7 +156,6 @@ jobs:
|
|||||||
- name: Checkout tfhe-rs repo
|
- name: Checkout tfhe-rs repo
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: 'false'
|
persist-credentials: 'false'
|
||||||
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
||||||
|
|
||||||
@@ -154,14 +175,15 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
timeout-minutes: 720 # 12 hours
|
timeout-minutes: 720 # 12 hours
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
command: ${{ fromJson(needs.prepare-benchmarks.outputs.commands) }}
|
command: ${{ fromJson(needs.prepare-benchmarks.outputs.commands) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout tfhe-rs repo with tags
|
- name: Checkout tfhe-rs repo
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # Needed to get commit hash
|
||||||
persist-credentials: 'false'
|
persist-credentials: 'false'
|
||||||
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
||||||
|
|
||||||
@@ -222,7 +244,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run regression benchmarks
|
- name: Run regression benchmarks
|
||||||
run: |
|
run: |
|
||||||
make BENCH_CUSTOM_COMMAND="${BENCH_COMMAND}" bench_custom
|
make BENCH_CUSTOM_COMMAND="${BENCH_COMMAND}" bench_custom
|
||||||
env:
|
env:
|
||||||
BENCH_COMMAND: ${{ matrix.command }}
|
BENCH_COMMAND: ${{ matrix.command }}
|
||||||
|
|
||||||
@@ -231,6 +253,7 @@ jobs:
|
|||||||
python3 ./ci/benchmark_parser.py target/criterion "${RESULTS_FILENAME}" \
|
python3 ./ci/benchmark_parser.py target/criterion "${RESULTS_FILENAME}" \
|
||||||
--database tfhe_rs \
|
--database tfhe_rs \
|
||||||
--hardware "${HARDWARE_NAME}" \
|
--hardware "${HARDWARE_NAME}" \
|
||||||
|
--backend "${TFHE_BACKEND}" \
|
||||||
--project-version "${COMMIT_HASH}" \
|
--project-version "${COMMIT_HASH}" \
|
||||||
--branch "${REF_NAME}" \
|
--branch "${REF_NAME}" \
|
||||||
--commit-date "${COMMIT_DATE}" \
|
--commit-date "${COMMIT_DATE}" \
|
||||||
@@ -238,15 +261,18 @@ jobs:
|
|||||||
--walk-subdirs \
|
--walk-subdirs \
|
||||||
--name-suffix regression \
|
--name-suffix regression \
|
||||||
--bench-type "${BENCH_TYPE}"
|
--bench-type "${BENCH_TYPE}"
|
||||||
|
|
||||||
|
echo "RESULTS_FILE_SHA=$(sha256sum "${RESULTS_FILENAME}" | cut -d " " -f1)" >> "${GITHUB_ENV}"
|
||||||
env:
|
env:
|
||||||
REF_NAME: ${{ github.ref_name }}
|
|
||||||
BENCH_TYPE: ${{ env.__TFHE_RS_BENCH_TYPE }}
|
|
||||||
HARDWARE_NAME: ${{ needs.prepare-benchmarks.outputs.hardware-name }}
|
HARDWARE_NAME: ${{ needs.prepare-benchmarks.outputs.hardware-name }}
|
||||||
|
TFHE_BACKEND: ${{ needs.prepare-benchmarks.outputs.tfhe-backend }}
|
||||||
|
REF_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
|
BENCH_TYPE: ${{ env.__TFHE_RS_BENCH_TYPE }}
|
||||||
|
|
||||||
- name: Upload parsed results artifact
|
- name: Upload parsed results artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}_regression
|
name: ${{ github.sha }}_regression_${{ env.RESULTS_FILE_SHA }} # RESULT_FILE_SHA is needed to avoid collision between matrix.command runs
|
||||||
path: ${{ env.RESULTS_FILENAME }}
|
path: ${{ env.RESULTS_FILENAME }}
|
||||||
|
|
||||||
- name: Send data to Slab
|
- name: Send data to Slab
|
||||||
@@ -258,9 +284,81 @@ jobs:
|
|||||||
JOB_SECRET: ${{ secrets.JOB_SECRET }}
|
JOB_SECRET: ${{ secrets.JOB_SECRET }}
|
||||||
SLAB_URL: ${{ secrets.SLAB_URL }}
|
SLAB_URL: ${{ secrets.SLAB_URL }}
|
||||||
|
|
||||||
|
check-regressions:
|
||||||
|
name: benchmark_perf_regression/check-regressions
|
||||||
|
needs: [ prepare-benchmarks, regression-benchmarks ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed to write a comment in a pull-request
|
||||||
|
pull-requests: write
|
||||||
|
# Needed to set up Python dependencies
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
REF_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout tfhe-rs repo
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
with:
|
||||||
|
persist-credentials: 'false'
|
||||||
|
token: ${{ secrets.REPO_CHECKOUT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install recent Python
|
||||||
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Fetch data
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -r ci/data_extractor/requirements.txt
|
||||||
|
python3 ci/data_extractor/src/data_extractor.py regression_data \
|
||||||
|
--generate-regression-json \
|
||||||
|
--regression-profiles ci/regression.toml \
|
||||||
|
--regression-selected-profile "${REGRESSION_PROFILE}" \
|
||||||
|
--backend "${TFHE_BACKEND}" \
|
||||||
|
--hardware "${HARDWARE_NAME}" \
|
||||||
|
--branch "${REF_NAME}" \
|
||||||
|
--time-span-days 60
|
||||||
|
env:
|
||||||
|
REGRESSION_PROFILE: ${{ needs.prepare-benchmarks.outputs.selected-regression-profile }}
|
||||||
|
TFHE_BACKEND: ${{ needs.prepare-benchmarks.outputs.tfhe-backend }}
|
||||||
|
HARDWARE_NAME: ${{ needs.prepare-benchmarks.outputs.hardware-name }}
|
||||||
|
DATA_EXTRACTOR_DATABASE_HOST: ${{ secrets.DATABASE_HOST }}
|
||||||
|
DATA_EXTRACTOR_DATABASE_USER: ${{ secrets.DATABASE_USER }}
|
||||||
|
DATA_EXTRACTOR_DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Generate regression report
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -r ci/perf_regression/requirements.txt
|
||||||
|
python3 ci/perf_regression/perf_regression.py check_regression \
|
||||||
|
--results-file regression_data.json \
|
||||||
|
--generate-report
|
||||||
|
|
||||||
|
- name: Write report in pull-request
|
||||||
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||||
|
body-path: ci/perf_regression/regression_report.md
|
||||||
|
|
||||||
|
comment-on-failure:
|
||||||
|
name: benchmark_perf_regression/comment-on-failure
|
||||||
|
needs: [ prepare-benchmarks, setup-instance, regression-benchmarks, check-regressions ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ failure() && github.event_name == 'issue_comment' }}
|
||||||
|
continue-on-error: true
|
||||||
|
permissions:
|
||||||
|
# Needed to write a comment in a pull-request
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Write failure message
|
||||||
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
:x: Performance regression benchmark failed ([workflow run](${{ env.ACTION_RUN_URL }}))
|
||||||
|
|
||||||
slack-notify:
|
slack-notify:
|
||||||
name: benchmark_perf_regression/slack-notify
|
name: benchmark_perf_regression/slack-notify
|
||||||
needs: [ prepare-benchmarks, setup-instance, regression-benchmarks ]
|
needs: [ prepare-benchmarks, setup-instance, regression-benchmarks, check-regressions ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -268,10 +366,8 @@ jobs:
|
|||||||
- name: Send message
|
- name: Send message
|
||||||
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661
|
uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661
|
||||||
env:
|
env:
|
||||||
SLACK_COLOR: ${{ needs.regression-benchmarks.result }}
|
SLACK_COLOR: failure
|
||||||
SLACK_MESSAGE: "Performance regression benchmarks finished with status: ${{ needs.regression-benchmarks.result }}. (${{ env.ACTION_RUN_URL }})"
|
SLACK_MESSAGE: "Performance regression benchmarks failed. (${{ env.ACTION_RUN_URL }})"
|
||||||
|
|
||||||
# TODO Add job for regression calculation
|
|
||||||
|
|
||||||
teardown-instance:
|
teardown-instance:
|
||||||
name: benchmark_perf_regression/teardown-instance
|
name: benchmark_perf_regression/teardown-instance
|
||||||
|
|||||||
42
Makefile
42
Makefile
@@ -1336,28 +1336,28 @@ print_doc_bench_parameters:
|
|||||||
bench_integer: install_rs_check_toolchain
|
bench_integer: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-bench \
|
--bench integer \
|
||||||
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_signed_integer # Run benchmarks for signed integer
|
.PHONY: bench_signed_integer # Run benchmarks for signed integer
|
||||||
bench_signed_integer: install_rs_check_toolchain
|
bench_signed_integer: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-signed-bench \
|
--bench integer-signed \
|
||||||
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_gpu # Run benchmarks for integer on GPU backend
|
.PHONY: bench_integer_gpu # Run benchmarks for integer on GPU backend
|
||||||
bench_integer_gpu: install_rs_check_toolchain
|
bench_integer_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-bench \
|
--bench integer \
|
||||||
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_signed_integer_gpu # Run benchmarks for signed integer on GPU backend
|
.PHONY: bench_signed_integer_gpu # Run benchmarks for signed integer on GPU backend
|
||||||
bench_signed_integer_gpu: install_rs_check_toolchain
|
bench_signed_integer_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-signed-bench \
|
--bench integer-signed \
|
||||||
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_hpu # Run benchmarks for integer on HPU backend
|
.PHONY: bench_integer_hpu # Run benchmarks for integer on HPU backend
|
||||||
@@ -1366,28 +1366,28 @@ bench_integer_hpu: install_rs_check_toolchain
|
|||||||
export V80_PCIE_DEV=${V80_PCIE_DEV}; \
|
export V80_PCIE_DEV=${V80_PCIE_DEV}; \
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-bench \
|
--bench integer \
|
||||||
--features=integer,internal-keycache,pbs-stats,hpu,hpu-v80 -p tfhe-benchmark -- --quick
|
--features=integer,internal-keycache,pbs-stats,hpu,hpu-v80 -p tfhe-benchmark -- --quick
|
||||||
|
|
||||||
.PHONY: bench_integer_compression # Run benchmarks for unsigned integer compression
|
.PHONY: bench_integer_compression # Run benchmarks for unsigned integer compression
|
||||||
bench_integer_compression: install_rs_check_toolchain
|
bench_integer_compression: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench glwe_packing_compression-integer-bench \
|
--bench integer-glwe_packing_compression \
|
||||||
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_compression_gpu
|
.PHONY: bench_integer_compression_gpu
|
||||||
bench_integer_compression_gpu: install_rs_check_toolchain
|
bench_integer_compression_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench glwe_packing_compression-integer-bench \
|
--bench integer-glwe_packing_compression \
|
||||||
--features=integer,internal-keycache,gpu,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,gpu,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_zk_gpu
|
.PHONY: bench_integer_zk_gpu
|
||||||
bench_integer_zk_gpu: install_rs_check_toolchain
|
bench_integer_zk_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench zk-pke-bench \
|
--bench integer-zk-pke \
|
||||||
--features=integer,internal-keycache,gpu,pbs-stats,zk-pok -p tfhe-benchmark --
|
--features=integer,internal-keycache,gpu,pbs-stats,zk-pok -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_multi_bit # Run benchmarks for unsigned integer using multi-bit parameters
|
.PHONY: bench_integer_multi_bit # Run benchmarks for unsigned integer using multi-bit parameters
|
||||||
@@ -1395,7 +1395,7 @@ bench_integer_multi_bit: install_rs_check_toolchain
|
|||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) \
|
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-bench \
|
--bench integer \
|
||||||
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_signed_integer_multi_bit # Run benchmarks for signed integer using multi-bit parameters
|
.PHONY: bench_signed_integer_multi_bit # Run benchmarks for signed integer using multi-bit parameters
|
||||||
@@ -1403,7 +1403,7 @@ bench_signed_integer_multi_bit: install_rs_check_toolchain
|
|||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) \
|
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-signed-bench \
|
--bench integer-signed \
|
||||||
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_multi_bit_gpu # Run benchmarks for integer on GPU backend using multi-bit parameters
|
.PHONY: bench_integer_multi_bit_gpu # Run benchmarks for integer on GPU backend using multi-bit parameters
|
||||||
@@ -1411,7 +1411,7 @@ bench_integer_multi_bit_gpu: install_rs_check_toolchain
|
|||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT \
|
||||||
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-bench \
|
--bench integer \
|
||||||
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_signed_integer_multi_bit_gpu # Run benchmarks for signed integer on GPU backend using multi-bit parameters
|
.PHONY: bench_signed_integer_multi_bit_gpu # Run benchmarks for signed integer on GPU backend using multi-bit parameters
|
||||||
@@ -1419,14 +1419,14 @@ bench_signed_integer_multi_bit_gpu: install_rs_check_toolchain
|
|||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=MULTI_BIT \
|
||||||
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
__TFHE_RS_BENCH_OP_FLAVOR=$(BENCH_OP_FLAVOR) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench integer-signed-bench \
|
--bench integer-signed \
|
||||||
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
--features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark --
|
||||||
|
|
||||||
.PHONY: bench_integer_zk # Run benchmarks for integer encryption with ZK proofs
|
.PHONY: bench_integer_zk # Run benchmarks for integer encryption with ZK proofs
|
||||||
bench_integer_zk: install_rs_check_toolchain
|
bench_integer_zk: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench zk-pke-bench \
|
--bench integer-zk-pke \
|
||||||
--features=integer,internal-keycache,zk-pok,nightly-avx512,pbs-stats \
|
--features=integer,internal-keycache,zk-pok,nightly-avx512,pbs-stats \
|
||||||
-p tfhe-benchmark --
|
-p tfhe-benchmark --
|
||||||
|
|
||||||
@@ -1454,56 +1454,56 @@ bench_boolean: install_rs_check_toolchain
|
|||||||
bench_ks: install_rs_check_toolchain
|
bench_ks: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench ks-bench \
|
--bench core_crypto-ks \
|
||||||
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_ks_gpu # Run benchmarks for keyswitch on GPU backend
|
.PHONY: bench_ks_gpu # Run benchmarks for keyswitch on GPU backend
|
||||||
bench_ks_gpu: install_rs_check_toolchain
|
bench_ks_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench ks-bench \
|
--bench core_crypto-ks \
|
||||||
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_pbs # Run benchmarks for PBS
|
.PHONY: bench_pbs # Run benchmarks for PBS
|
||||||
bench_pbs: install_rs_check_toolchain
|
bench_pbs: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench pbs-bench \
|
--bench core_crypto-pbs \
|
||||||
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_pbs_gpu # Run benchmarks for PBS on GPU backend
|
.PHONY: bench_pbs_gpu # Run benchmarks for PBS on GPU backend
|
||||||
bench_pbs_gpu: install_rs_check_toolchain
|
bench_pbs_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_FAST_BENCH=$(FAST_BENCH) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench pbs-bench \
|
--bench core_crypto-pbs \
|
||||||
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_ks_pbs # Run benchmarks for KS-PBS
|
.PHONY: bench_ks_pbs # Run benchmarks for KS-PBS
|
||||||
bench_ks_pbs: install_rs_check_toolchain
|
bench_ks_pbs: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench ks-pbs-bench \
|
--bench core_crypto-ks-pbs \
|
||||||
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_ks_pbs_gpu # Run benchmarks for KS-PBS on GPU backend
|
.PHONY: bench_ks_pbs_gpu # Run benchmarks for KS-PBS on GPU backend
|
||||||
bench_ks_pbs_gpu: install_rs_check_toolchain
|
bench_ks_pbs_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_PARAM_TYPE=$(BENCH_PARAM_TYPE) __TFHE_RS_PARAMS_SET=$(BENCH_PARAMS_SET) __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench ks-pbs-bench \
|
--bench core_crypto-ks-pbs \
|
||||||
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_pbs128 # Run benchmarks for PBS using FFT 128 bits
|
.PHONY: bench_pbs128 # Run benchmarks for PBS using FFT 128 bits
|
||||||
bench_pbs128: install_rs_check_toolchain
|
bench_pbs128: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench pbs128-bench \
|
--bench core_crypto-pbs128 \
|
||||||
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
.PHONY: bench_pbs128_gpu # Run benchmarks for PBS using FFT 128 bits on GPU
|
.PHONY: bench_pbs128_gpu # Run benchmarks for PBS using FFT 128 bits on GPU
|
||||||
bench_pbs128_gpu: install_rs_check_toolchain
|
bench_pbs128_gpu: install_rs_check_toolchain
|
||||||
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \
|
||||||
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \
|
||||||
--bench pbs128-bench \
|
--bench core_crypto-pbs128 \
|
||||||
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
--features=boolean,shortint,gpu,internal-keycache,nightly-avx512 -p tfhe-benchmark
|
||||||
|
|
||||||
bench_web_js_api_parallel_chrome: browser_path = "$(WEB_RUNNER_DIR)/chrome/chrome-linux64/chrome"
|
bench_web_js_api_parallel_chrome: browser_path = "$(WEB_RUNNER_DIR)/chrome/chrome-linux64/chrome"
|
||||||
|
|||||||
4
ci/data_extractor/requirements.txt
Normal file
4
ci/data_extractor/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
psycopg2-binary==2.9.9
|
||||||
|
py-markdown-table==1.3.0
|
||||||
|
svg.py==1.5.0
|
||||||
|
black
|
||||||
490
ci/data_extractor/src/benchmark_specs.py
Normal file
490
ci/data_extractor/src/benchmark_specs.py
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from exceptions import ParametersFormatNotSupported
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents different types of computation backends used in tfhe-rs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CPU = "cpu"
|
||||||
|
GPU = "gpu"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(backend_name):
|
||||||
|
match backend_name.lower():
|
||||||
|
case "cpu":
|
||||||
|
return Backend.CPU
|
||||||
|
case "gpu":
|
||||||
|
return Backend.GPU
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Layer(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents different types of layers used in tfhe-rs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
HLApi = "hlapi"
|
||||||
|
Integer = "integer"
|
||||||
|
Shortint = "shortint"
|
||||||
|
CoreCrypto = "core_crypto"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(layer_name):
|
||||||
|
match layer_name.lower():
|
||||||
|
case "hlapi":
|
||||||
|
return Layer.HLApi
|
||||||
|
case "integer":
|
||||||
|
return Layer.Integer
|
||||||
|
case "shortint":
|
||||||
|
return Layer.Shortint
|
||||||
|
case "core_crypto":
|
||||||
|
return Layer.CoreCrypto
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class RustType(enum.Enum):
|
||||||
|
"""
|
||||||
|
Represents different integer Rust types used in tfhe-rs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
FheUint2 = 2
|
||||||
|
FheUint4 = 4
|
||||||
|
FheUint8 = 8
|
||||||
|
FheUint16 = 16
|
||||||
|
FheUint32 = 32
|
||||||
|
FheUint64 = 64
|
||||||
|
FheUint128 = 128
|
||||||
|
FheUint256 = 256
|
||||||
|
|
||||||
|
|
||||||
|
ALL_RUST_TYPES = [
|
||||||
|
RustType.FheUint2,
|
||||||
|
RustType.FheUint4,
|
||||||
|
RustType.FheUint8,
|
||||||
|
RustType.FheUint16,
|
||||||
|
RustType.FheUint32,
|
||||||
|
RustType.FheUint64,
|
||||||
|
RustType.FheUint128,
|
||||||
|
RustType.FheUint256,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CoreCryptoOperation(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents different core crypto operations performed in tfhe-rs.
|
||||||
|
The values are the ones displayed in the public benchmarks documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
KeySwitch = "KS"
|
||||||
|
PBS = "PBS"
|
||||||
|
MultiBitPBS = "MB-PBS"
|
||||||
|
KeyswitchPBS = "KS - PBS"
|
||||||
|
KeySwitchMultiBitPBS = "KS - MB-PBS"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(operation_name):
|
||||||
|
match operation_name.lower():
|
||||||
|
case "keyswitch":
|
||||||
|
return CoreCryptoOperation.KeySwitch
|
||||||
|
case "pbs_mem_optimized":
|
||||||
|
return CoreCryptoOperation.PBS
|
||||||
|
case "multi_bit_pbs" | "multi_bit_deterministic_pbs":
|
||||||
|
return CoreCryptoOperation.MultiBitPBS
|
||||||
|
case "ks_pbs":
|
||||||
|
return CoreCryptoOperation.KeyswitchPBS
|
||||||
|
case "multi_bit_ks_pbs" | "multi_bit_deterministic_ks_pbs":
|
||||||
|
return CoreCryptoOperation.KeySwitchMultiBitPBS
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"core crypto operation '{operation_name}' not supported yet"
|
||||||
|
)
|
||||||
|
|
||||||
|
def display_name(self):
|
||||||
|
"""
|
||||||
|
Return the human-friendly name recorded for a given operation.
|
||||||
|
This name is parameter-independent.
|
||||||
|
|
||||||
|
:return: The name as recorded by tfhe-benchmark crate
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
match self:
|
||||||
|
case CoreCryptoOperation.KeySwitch:
|
||||||
|
return "ks"
|
||||||
|
case CoreCryptoOperation.PBS:
|
||||||
|
return "pbs"
|
||||||
|
case CoreCryptoOperation.MultiBitPBS:
|
||||||
|
return "pbs"
|
||||||
|
case CoreCryptoOperation.KeyswitchPBS:
|
||||||
|
return "ks-pbs"
|
||||||
|
case CoreCryptoOperation.KeySwitchMultiBitPBS:
|
||||||
|
return "ks-pbs"
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"display name for {self} not implemented yet"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SignFlavor(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents the sign of integer benchmarks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Signed = "signed"
|
||||||
|
Unsigned = "unsigned"
|
||||||
|
|
||||||
|
|
||||||
|
class OperandType(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents the type of operand use in a benchmark.
|
||||||
|
Ciphertext means encrypted-encrypted operation.
|
||||||
|
PlainText means encrypted-plaintext operation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CipherText = "CipherText" # As represented in the database
|
||||||
|
PlainText = "PlainText"
|
||||||
|
|
||||||
|
|
||||||
|
class PBSKind(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents the kind of parameter set used for Programmable Bootstrapping operation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Classical = "classical"
|
||||||
|
MultiBit = "multi_bit"
|
||||||
|
Any = "any" # Special variant used when user doesn't care about the PBS kind
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(pbs_name):
|
||||||
|
match pbs_name.lower():
|
||||||
|
case "classical":
|
||||||
|
return PBSKind.Classical
|
||||||
|
case "multi_bit":
|
||||||
|
return PBSKind.MultiBit
|
||||||
|
case "any":
|
||||||
|
return PBSKind.Any
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoiseDistribution(enum.StrEnum):
|
||||||
|
"""
|
||||||
|
Represents the noise distribution used in the parameter set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Gaussian = "gaussian"
|
||||||
|
TUniform = "tuniform"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(distrib_name):
|
||||||
|
match distrib_name.lower():
|
||||||
|
case "gaussian":
|
||||||
|
return NoiseDistribution.Gaussian
|
||||||
|
case "tuniform":
|
||||||
|
return NoiseDistribution.TUniform
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"noise distribution '{distrib_name}' not supported yet"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorFailureProbability(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Represents the error failure probability associated with a parameter set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TWO_MINUS_40 = 40
|
||||||
|
TWO_MINUS_64 = 64
|
||||||
|
TWO_MINUS_128 = 128
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_param_name(name):
|
||||||
|
parts = name.split("_")
|
||||||
|
for part in parts:
|
||||||
|
if not part.startswith("2M"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
match int(part.lstrip("2M")):
|
||||||
|
case 40:
|
||||||
|
return ErrorFailureProbability.TWO_MINUS_40
|
||||||
|
case 64:
|
||||||
|
return ErrorFailureProbability.TWO_MINUS_64
|
||||||
|
case 128:
|
||||||
|
return ErrorFailureProbability.TWO_MINUS_128
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"error failure probability '{part}' not supported yet"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Could not find p-fail value in '{name}'")
|
||||||
|
|
||||||
|
def to_str(self):
|
||||||
|
match self:
|
||||||
|
case ErrorFailureProbability.TWO_MINUS_40:
|
||||||
|
return "2M40"
|
||||||
|
case ErrorFailureProbability.TWO_MINUS_64:
|
||||||
|
return "2M64"
|
||||||
|
case ErrorFailureProbability.TWO_MINUS_128:
|
||||||
|
return "2M128"
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
f"error failure probability str conversion '{self}' not supported yet"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParamsDefinition:
|
||||||
|
"""
|
||||||
|
Represents a parameter definition for specific cryptographic settings.
|
||||||
|
|
||||||
|
The class `ParamsDefinition` is designed to parse and manage parameters derived from
|
||||||
|
a specified parameter name. It facilitates comparison, representation, and hashing of
|
||||||
|
parameter configurations. Parameters related to message size, carry size, noise characteristics,
|
||||||
|
failure probabilities, and other functionalities are extracted and stored for streamlined
|
||||||
|
usage across the system.
|
||||||
|
|
||||||
|
:param param_name: The raw name of the parameter set.
|
||||||
|
:type param_name: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, param_name: str):
|
||||||
|
self.message_size = None
|
||||||
|
self.carry_size = None
|
||||||
|
self.pbs_kind = None
|
||||||
|
self.grouping_factor = None
|
||||||
|
self.noise_distribution = None
|
||||||
|
self.atomic_pattern = None
|
||||||
|
self.p_fail = None
|
||||||
|
self.version = None
|
||||||
|
self.details = {}
|
||||||
|
|
||||||
|
self._parse_param_name(param_name)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
self.message_size == other.message_size
|
||||||
|
and self.carry_size == other.carry_size
|
||||||
|
and self.pbs_kind == other.pbs_kind
|
||||||
|
and self.grouping_factor == other.grouping_factor
|
||||||
|
and self.noise_distribution == other.noise_distribution
|
||||||
|
and self.atomic_pattern == other.atomic_pattern
|
||||||
|
and self.p_fail == other.p_fail
|
||||||
|
and self.version == other.version
|
||||||
|
and self.details == other.details
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.message_size < other.message_size
|
||||||
|
and self.carry_size < other.carry_size
|
||||||
|
and self.p_fail < other.p_fail
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(
|
||||||
|
(
|
||||||
|
self.message_size,
|
||||||
|
self.carry_size,
|
||||||
|
self.pbs_kind,
|
||||||
|
self.grouping_factor,
|
||||||
|
self.noise_distribution,
|
||||||
|
self.atomic_pattern,
|
||||||
|
self.p_fail,
|
||||||
|
self.version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ParamsDefinition(message_size={self.message_size}, carry_size={self.carry_size}, pbs_kind={self.pbs_kind}, grouping_factor={self.grouping_factor}, noise_distribution={self.noise_distribution}, atomic_pattern={self.atomic_pattern}, p_fail={self.p_fail}, version={self.version}, details={self.details})"
|
||||||
|
|
||||||
|
def _parse_param_name(self, param_name: str) -> None:
|
||||||
|
split_params = param_name.split("_")
|
||||||
|
|
||||||
|
if split_params[0].startswith("V"):
|
||||||
|
minor_version = split_params.pop(1)
|
||||||
|
major_version = split_params.pop(0).strip("V")
|
||||||
|
self.version = major_version + "_" + minor_version
|
||||||
|
|
||||||
|
# Use to know if a parameter set is a derivative of a compute one (e.g. compression parameters)
|
||||||
|
params_variation_parts = []
|
||||||
|
for part in split_params:
|
||||||
|
if part == "PARAM":
|
||||||
|
self.details["variation"] = "_".join(params_variation_parts)
|
||||||
|
break
|
||||||
|
|
||||||
|
params_variation_parts.append(part)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.p_fail = ErrorFailureProbability.from_param_name(param_name)
|
||||||
|
pfail_index = split_params.index(self.p_fail.to_str())
|
||||||
|
except ValueError or NotImplementedError:
|
||||||
|
# Default error probability may not be shown in the name
|
||||||
|
self.p_fail = ErrorFailureProbability.TWO_MINUS_128
|
||||||
|
pfail_index = None
|
||||||
|
|
||||||
|
if pfail_index:
|
||||||
|
noise_distribution_index = pfail_index - 1
|
||||||
|
self.noise_distribution = NoiseDistribution.from_str(
|
||||||
|
split_params[noise_distribution_index]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Default noise distribution may not be shown in the name
|
||||||
|
self.noise_distribution = NoiseDistribution.TUniform
|
||||||
|
noise_distribution_index = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.message_size = int(split_params[split_params.index("MESSAGE") + 1])
|
||||||
|
carry_size_index = split_params.index("CARRY") + 1
|
||||||
|
self.carry_size = int(split_params[carry_size_index])
|
||||||
|
self.atomic_pattern = "_".join(
|
||||||
|
split_params[carry_size_index + 1 : noise_distribution_index]
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# Might be a Boolean parameters set
|
||||||
|
raise ParametersFormatNotSupported(param_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if noise_distribution_index:
|
||||||
|
self.atomic_pattern = "_".join(
|
||||||
|
split_params[carry_size_index + 1 : noise_distribution_index]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.atomic_pattern = "_".join(split_params[carry_size_index + 1 :])
|
||||||
|
except ValueError:
|
||||||
|
# Might be a Boolean parameters set
|
||||||
|
raise ParametersFormatNotSupported(param_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.details["trailing_details"] = "_".join(split_params[pfail_index + 1 :])
|
||||||
|
except IndexError:
|
||||||
|
# No trailing details
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This is a multi-bit parameters set
|
||||||
|
self.grouping_factor = int(split_params[split_params.index("GROUP") + 1])
|
||||||
|
self.pbs_kind = PBSKind.MultiBit
|
||||||
|
except ValueError:
|
||||||
|
# This is a classical parameters set
|
||||||
|
self.pbs_kind = PBSKind.Classical
|
||||||
|
|
||||||
|
|
||||||
|
class BenchDetails:
|
||||||
|
"""
|
||||||
|
Represents the details of a benchmark test for different layers.
|
||||||
|
|
||||||
|
This class is designed to parse benchmark information, extract meaningful
|
||||||
|
details such as operation name, parameters, and relevant configuration
|
||||||
|
based on the layer type. It allows for comparison between different benchmark
|
||||||
|
details and outputs structured information for representation or hashing.
|
||||||
|
|
||||||
|
:param layer: The layer the benchmark pertains to.
|
||||||
|
:type layer: Layer
|
||||||
|
:param bench_full_name: Complete name of the benchmark operation.
|
||||||
|
:type bench_full_name: str
|
||||||
|
:param bit_size: The bit size associated with the benchmark.
|
||||||
|
:type bit_size: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, layer: Layer, bench_full_name: str, bit_size: int):
|
||||||
|
self.layer = layer
|
||||||
|
|
||||||
|
self.operation_name = None
|
||||||
|
self.bit_size = bit_size
|
||||||
|
self.params = None
|
||||||
|
# Only relevant for Integer layer
|
||||||
|
self.sign_flavor = None
|
||||||
|
# Only relevant for HLApi layer
|
||||||
|
self.rust_type = None
|
||||||
|
|
||||||
|
self.parse_test_name(bench_full_name)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"BenchDetails(layer={self.layer.value}, operation_name={self.operation_name}, bit_size={self.bit_size}, params={self.params}, {self.sign_flavor})"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
self.layer == other.layer
|
||||||
|
and self.operation_name == other.operation_name
|
||||||
|
and self.bit_size == other.bit_size
|
||||||
|
and self.params == other.params
|
||||||
|
and self.sign_flavor == other.sign_flavor
|
||||||
|
and self.rust_type == other.rust_type
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(
|
||||||
|
(
|
||||||
|
self.layer,
|
||||||
|
self.operation_name,
|
||||||
|
self.bit_size,
|
||||||
|
self.params,
|
||||||
|
self.rust_type,
|
||||||
|
self.sign_flavor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_test_name(self, name) -> None:
|
||||||
|
"""
|
||||||
|
Parse test name to split relevant parts.
|
||||||
|
|
||||||
|
:param name: The raw test name.
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
parts = name.split("::")
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if "PARAM" in part:
|
||||||
|
self.params = part.partition("_mean")[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
match self.layer:
|
||||||
|
case Layer.Integer:
|
||||||
|
op_name_index = 2 if parts[1] == "cuda" else 1
|
||||||
|
if parts[op_name_index] == "signed":
|
||||||
|
op_name_index += 1
|
||||||
|
self.sign_flavor = SignFlavor.Signed
|
||||||
|
self.operation_name = parts[op_name_index]
|
||||||
|
elif parts[op_name_index] == "unsigned":
|
||||||
|
# This is a pattern used by benchmark run on CUDA
|
||||||
|
op_name_index += 1
|
||||||
|
self.sign_flavor = SignFlavor.Unsigned
|
||||||
|
self.operation_name = parts[op_name_index]
|
||||||
|
else:
|
||||||
|
self.sign_flavor = SignFlavor.Unsigned
|
||||||
|
self.operation_name = parts[op_name_index]
|
||||||
|
self.params = parts[op_name_index + 1]
|
||||||
|
if "compression" in parts[op_name_index]:
|
||||||
|
self.rust_type = "_".join(
|
||||||
|
(parts[op_name_index], parts[-1].split("_")[0])
|
||||||
|
)
|
||||||
|
case Layer.CoreCrypto:
|
||||||
|
self.operation_name = parts[2] if parts[1] == "cuda" else parts[1]
|
||||||
|
case Layer.HLApi:
|
||||||
|
if parts[1] == "cuda":
|
||||||
|
self.operation_name = "::".join(parts[2:-1])
|
||||||
|
else:
|
||||||
|
self.operation_name = "::".join(parts[1:-1])
|
||||||
|
self.rust_type = parts[-1].partition("_mean")[0]
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"layer '{self.layer}' not supported yet for name parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_params_definition(self) -> ParamsDefinition:
|
||||||
|
"""
|
||||||
|
Returns the definition of parameters based on the current instance's parameters.
|
||||||
|
|
||||||
|
:return: A ParamsDefinition object that encapsulates the parameter definition.
|
||||||
|
:rtype: ParamsDefinition
|
||||||
|
"""
|
||||||
|
return ParamsDefinition(self.params)
|
||||||
43
ci/data_extractor/src/config.py
Normal file
43
ci/data_extractor/src/config.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from benchmark_specs import Backend, Layer, PBSKind
|
||||||
|
|
||||||
|
|
||||||
|
class UserConfig:
|
||||||
|
"""
|
||||||
|
Manages the configuration provided by the user input.
|
||||||
|
|
||||||
|
This class encapsulates the user-provided configuration data necessary for execution.
|
||||||
|
It sets up various attributes based on the input arguments, converting or transforming
|
||||||
|
values as needed to ensure compatibility and correct behavior within the system.
|
||||||
|
|
||||||
|
:param input_args: The input arguments provided by the user.
|
||||||
|
:type input_args: argparse.Namespace
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, input_args: argparse.Namespace):
|
||||||
|
self.output_file = input_args.output_file
|
||||||
|
|
||||||
|
self.database = input_args.database
|
||||||
|
self.backend = Backend.from_str(input_args.backend.lower())
|
||||||
|
self.hardware = input_args.hardware # This input is case-sensitive
|
||||||
|
|
||||||
|
self.head_branch = input_args.branch.lower()
|
||||||
|
self.base_branch = input_args.base_branch.lower()
|
||||||
|
|
||||||
|
self.project_version = input_args.project_version
|
||||||
|
|
||||||
|
self.bench_date = input_args.bench_date
|
||||||
|
self.time_span_days = input_args.time_span_days
|
||||||
|
|
||||||
|
self.layer = Layer.from_str(input_args.layer.lower())
|
||||||
|
self.pbs_kind = PBSKind.from_str(input_args.pbs_kind)
|
||||||
|
self.grouping_factor = input_args.grouping_factor
|
||||||
|
|
||||||
|
self.regression_selected_profile = input_args.regression_selected_profile
|
||||||
|
self.regression_profiles_path = (
|
||||||
|
pathlib.Path(input_args.regression_profiles)
|
||||||
|
if input_args.regression_profiles
|
||||||
|
else None
|
||||||
|
)
|
||||||
292
ci/data_extractor/src/connector.py
Normal file
292
ci/data_extractor/src/connector.py
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import configparser
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from benchmark_specs import BenchDetails, Layer, OperandType, PBSKind
|
||||||
|
from config import UserConfig
|
||||||
|
from exceptions import NoDataFound
|
||||||
|
|
||||||
|
|
||||||
|
class PostgreConfig:
|
||||||
|
"""
|
||||||
|
Represents the configuration to connect to a PostgreSQL database.
|
||||||
|
|
||||||
|
This class is designed to manage and load PostgreSQL database credentials
|
||||||
|
from a configuration file and override them with environment variables if available.
|
||||||
|
|
||||||
|
:param path: Path to the configuration file.
|
||||||
|
:type path: str, optional
|
||||||
|
|
||||||
|
:ivar host: Host address of the PostgreSQL database.
|
||||||
|
:type host: str
|
||||||
|
:ivar user: Username for connecting to the PostgreSQL database.
|
||||||
|
:type user: str
|
||||||
|
:ivar password: Password for connecting to the PostgreSQL database.
|
||||||
|
:type password: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path: str = None):
|
||||||
|
self.host = None
|
||||||
|
self.user = None
|
||||||
|
self.password = None
|
||||||
|
|
||||||
|
if path:
|
||||||
|
self._from_config_file(path)
|
||||||
|
|
||||||
|
self._override_with_env()
|
||||||
|
|
||||||
|
def _from_config_file(self, path):
|
||||||
|
"""
|
||||||
|
Parse configuration file containing credentials to Postgre database.
|
||||||
|
|
||||||
|
:param path: path to the configuration file as :class:`str`
|
||||||
|
|
||||||
|
:return: parsed file as :class:`configparser.ConfigParser`
|
||||||
|
"""
|
||||||
|
path = pathlib.Path(path)
|
||||||
|
conf = configparser.ConfigParser()
|
||||||
|
conf.read(path)
|
||||||
|
|
||||||
|
for option_name in ("user", "password", "host"):
|
||||||
|
try:
|
||||||
|
attr_value = conf.get("postgre", option_name)
|
||||||
|
except ValueError:
|
||||||
|
print(f"Cannot find mandatory option '{option_name}' in config file")
|
||||||
|
raise
|
||||||
|
|
||||||
|
setattr(self, option_name, attr_value)
|
||||||
|
|
||||||
|
def _override_with_env(self):
|
||||||
|
self.host = os.environ.get("DATA_EXTRACTOR_DATABASE_HOST", self.host)
|
||||||
|
self.user = os.environ.get("DATA_EXTRACTOR_DATABASE_USER", self.user)
|
||||||
|
self.password = os.environ.get(
|
||||||
|
"DATA_EXTRACTOR_DATABASE_PASSWORD", self.password
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PostgreConnector:
|
||||||
|
def __init__(self, conf: PostgreConfig):
|
||||||
|
"""
|
||||||
|
Initializes the class with a configuration object.
|
||||||
|
|
||||||
|
:param conf: The configuration object used to set up
|
||||||
|
the connection or relevant settings.
|
||||||
|
:type conf: PostgreConfig
|
||||||
|
"""
|
||||||
|
self.conf = conf
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._conn:
|
||||||
|
# Avoid dandling connection on server side.
|
||||||
|
self._conn.close()
|
||||||
|
|
||||||
|
def connect_to_database(self, dbname: str):
|
||||||
|
"""
|
||||||
|
Create a connection to a Postgre instance.
|
||||||
|
|
||||||
|
:param dbname: Name of the database to connect to.
|
||||||
|
:type dbname: str
|
||||||
|
"""
|
||||||
|
print("Connecting to database... ", end="", flush=True)
|
||||||
|
start_date = datetime.datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
dbname=dbname,
|
||||||
|
user=self.conf.user,
|
||||||
|
password=self.conf.password,
|
||||||
|
host=self.conf.host,
|
||||||
|
)
|
||||||
|
except psycopg2.Error as err:
|
||||||
|
print(f"Failed to connect to Postgre database")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
elapsed = (datetime.datetime.now() - start_date).total_seconds()
|
||||||
|
print(f"connected in {elapsed}s")
|
||||||
|
|
||||||
|
self._conn = conn
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close the connection to the database.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._conn.close()
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def fetch_benchmark_data(
|
||||||
|
self,
|
||||||
|
user_config: UserConfig,
|
||||||
|
operand_type: OperandType = None,
|
||||||
|
operation_filter: list = None,
|
||||||
|
layer: Layer = None,
|
||||||
|
branch: str = None,
|
||||||
|
name_suffix: str = "_mean_avx512",
|
||||||
|
last_value_only: bool = True,
|
||||||
|
) -> dict[BenchDetails, list[int]]:
|
||||||
|
"""
|
||||||
|
Fetches benchmark data from the database based on various filtering criteria.
|
||||||
|
|
||||||
|
Filters and constructs a query based on the provided user configuration and optional
|
||||||
|
parameters such as operand type, operation filters, layer, and others. It retrieves
|
||||||
|
specific benchmark metrics like test names, bit sizes, metric values, and their last
|
||||||
|
inserted values. The data is fetched for a given hardware, backend, branch, and project
|
||||||
|
version, or within a computed time range.
|
||||||
|
|
||||||
|
:param user_config: User's configuration specifying backend, hardware, branch, layer,
|
||||||
|
project version, PBS kind, benchmark date, and time span.
|
||||||
|
:type user_config: UserConfig
|
||||||
|
:param operand_type: Optional operand type filter.
|
||||||
|
:type operand_type: OperandType, optional
|
||||||
|
:param operation_filter: Optional list of operation name filters for partial test name matching.
|
||||||
|
:type operation_filter: list, optional
|
||||||
|
:param layer: Optional tfhe-rs layer filter
|
||||||
|
:type layer: Layer, optional
|
||||||
|
:param branch: Optional branch filter, defaulting to the user's head branch if not specified.
|
||||||
|
:type branch: str, optional
|
||||||
|
:param name_suffix: Suffix to match the test names, defaulting to "_mean_avx512".
|
||||||
|
:type name_suffix: str, optional
|
||||||
|
:param last_value_only: A flag indicating whether to fetch only the most recent metric value for each benchmark.
|
||||||
|
:type last_value_only: bool
|
||||||
|
|
||||||
|
:return: Fetched benchmark data filtered and formatted as per the query.
|
||||||
|
:rtype: dict[BenchDetails, list[int]]
|
||||||
|
"""
|
||||||
|
backend = user_config.backend
|
||||||
|
branch = branch.lower() if branch else user_config.head_branch
|
||||||
|
hardware = user_config.hardware
|
||||||
|
layer = layer if layer else user_config.layer
|
||||||
|
version = user_config.project_version
|
||||||
|
pbs_kind = user_config.pbs_kind
|
||||||
|
|
||||||
|
timestamp_range_end = user_config.bench_date
|
||||||
|
timestamp = datetime.datetime.fromisoformat(timestamp_range_end)
|
||||||
|
time_span_delta = datetime.timedelta(days=user_config.time_span_days)
|
||||||
|
timestamp_range_start = datetime.datetime.isoformat(timestamp - time_span_delta)
|
||||||
|
if layer == Layer.CoreCrypto:
|
||||||
|
# Before this date we used another name format that is not parsable by this tool.
|
||||||
|
cutoff_date = "2024-02-28T00:00:00"
|
||||||
|
timestamp_range_start = max(cutoff_date, timestamp_range_start)
|
||||||
|
|
||||||
|
filters = list()
|
||||||
|
|
||||||
|
filters.append(f"h.name = '{hardware}'")
|
||||||
|
filters.append(f"bk.name = '{backend}'")
|
||||||
|
filters.append(f"b.name = '{branch}'")
|
||||||
|
|
||||||
|
name_suffix = f"\\{name_suffix}"
|
||||||
|
if backend == "cpu":
|
||||||
|
filters.append(f"test.name LIKE '{layer}::%{name_suffix}'")
|
||||||
|
elif backend == "gpu":
|
||||||
|
filters.append(f"test.name LIKE '{layer}::cuda::%{name_suffix}'")
|
||||||
|
|
||||||
|
if version:
|
||||||
|
filters.append(f"pv.name = '{version}'")
|
||||||
|
else:
|
||||||
|
filters.append(f"m.insert_time >= '{timestamp_range_start}'")
|
||||||
|
filters.append(f"m.insert_time <= '{timestamp_range_end}'")
|
||||||
|
|
||||||
|
# First iteration to fetch only default operations
|
||||||
|
filters.append("test.name NOT SIMILAR TO '%(smart|unchecked)_%'")
|
||||||
|
match pbs_kind:
|
||||||
|
case PBSKind.Classical:
|
||||||
|
filters.append(
|
||||||
|
"p.crypto_parameters_alias NOT SIMILAR TO '%_MULTI_BIT_%'"
|
||||||
|
)
|
||||||
|
case PBSKind.MultiBit:
|
||||||
|
filters.append("p.crypto_parameters_alias SIMILAR TO '%_MULTI_BIT_%'")
|
||||||
|
case PBSKind.Any:
|
||||||
|
# No need to add a filter
|
||||||
|
pass
|
||||||
|
|
||||||
|
if operand_type:
|
||||||
|
filters.append(f"p.operand_type = '{operand_type.value}'")
|
||||||
|
|
||||||
|
if operation_filter:
|
||||||
|
conditions = [
|
||||||
|
f"test.name LIKE '%::{op_name}::%'" for op_name in operation_filter
|
||||||
|
]
|
||||||
|
filters.append("({})".format(" OR ".join(conditions)))
|
||||||
|
|
||||||
|
# Throughput is not supported yet
|
||||||
|
filters.append("test.name NOT SIMILAR TO '%::throughput::%'")
|
||||||
|
|
||||||
|
select_parts = (
|
||||||
|
"SELECT",
|
||||||
|
"test.name as test,",
|
||||||
|
"p.bit_size as bit_size,",
|
||||||
|
"m.value as value,",
|
||||||
|
"m.insert_time,",
|
||||||
|
"LAST_VALUE (value)",
|
||||||
|
"OVER (PARTITION BY test.name ORDER BY m.insert_time RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) last_value",
|
||||||
|
"FROM benchmark.metrics as m",
|
||||||
|
"LEFT JOIN benchmark.project_version as pv ON m.project_version_id = pv.id",
|
||||||
|
"LEFT JOIN benchmark.hardware as h ON m.hardware_id = h.id",
|
||||||
|
"LEFT JOIN benchmark.backend as bk ON m.backend_id = bk.id",
|
||||||
|
"LEFT JOIN benchmark.branch as b ON b.id = m.branch_id",
|
||||||
|
"LEFT JOIN benchmark.test as test ON test.id = m.test_id",
|
||||||
|
"LEFT JOIN benchmark.parameters as p ON p.id = m.parameters_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
sql_string = format(
|
||||||
|
"{} WHERE {} GROUP BY test, bit_size, value, m.insert_time ORDER BY m.insert_time DESC"
|
||||||
|
).format(" ".join(select_parts), " AND ".join(filters))
|
||||||
|
|
||||||
|
return self._fetch_data(
|
||||||
|
sql_string,
|
||||||
|
version,
|
||||||
|
timestamp_range_start,
|
||||||
|
timestamp_range_end,
|
||||||
|
hardware,
|
||||||
|
layer,
|
||||||
|
last_value_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_data(
|
||||||
|
self,
|
||||||
|
sql_string,
|
||||||
|
version,
|
||||||
|
timestamp_range_start,
|
||||||
|
timestamp_range_end,
|
||||||
|
hw,
|
||||||
|
layer: Layer,
|
||||||
|
last_value_only: bool,
|
||||||
|
) -> dict[BenchDetails, list[int]]:
|
||||||
|
with self._conn.cursor() as curs:
|
||||||
|
start = datetime.datetime.now()
|
||||||
|
print(f"Fetching data (hardware: {hw}, layer: {layer.value})...", end="")
|
||||||
|
|
||||||
|
curs.execute(sql_string)
|
||||||
|
lines = curs.fetchall()
|
||||||
|
|
||||||
|
end = datetime.datetime.now()
|
||||||
|
print(f"done in {(end - start).total_seconds()}s")
|
||||||
|
|
||||||
|
results = dict()
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
if version:
|
||||||
|
msg = f"no data found under commit hash '{version}'"
|
||||||
|
else:
|
||||||
|
msg = f"no data found in date range [{timestamp_range_start}, {timestamp_range_end}]"
|
||||||
|
raise NoDataFound(msg)
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
bit_width = line[1]
|
||||||
|
|
||||||
|
bench_details = BenchDetails(layer, line[0], bit_width)
|
||||||
|
value = line[-1] if last_value_only else line[-3]
|
||||||
|
try:
|
||||||
|
timings = results[bench_details]
|
||||||
|
if last_value_only:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
timings.append(value)
|
||||||
|
except KeyError:
|
||||||
|
results[bench_details] = [value]
|
||||||
|
|
||||||
|
return results
|
||||||
403
ci/data_extractor/src/data_extractor.py
Normal file
403
ci/data_extractor/src/data_extractor.py
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
"""
|
||||||
|
data_extractor
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Extract benchmarks results from Zama PostgreSQL instance.
|
||||||
|
It will output the filtered results in a file formatted as CSV.
|
||||||
|
|
||||||
|
PostgreSQL connection configuration can be passed through a configuration file or via environment variables.
|
||||||
|
When using the environment variables, make sure to set the following ones:
|
||||||
|
* DATA_EXTRACTOR_DATABASE_HOST
|
||||||
|
* DATA_EXTRACTOR_DATABASE_USER
|
||||||
|
* DATA_EXTRACTOR_DATABASE_PASSWORD
|
||||||
|
|
||||||
|
Note that if provided, environment variables will take precedence over the configuration file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import formatter
|
||||||
|
import sys
|
||||||
|
from formatter import (CSVFormatter, GenericFormatter, MarkdownFormatter,
|
||||||
|
SVGFormatter)
|
||||||
|
|
||||||
|
import config
|
||||||
|
import connector
|
||||||
|
import regression
|
||||||
|
from benchmark_specs import Backend, Layer, OperandType, PBSKind, RustType
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
parser.add_argument(
|
||||||
|
"output_file", help="File storing parsed results (with no extension)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c" "--config-file",
|
||||||
|
dest="config_file",
|
||||||
|
help="Location of configuration file containing credentials to connect to "
|
||||||
|
"PostgreSQL instance",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bench-date",
|
||||||
|
dest="bench_date",
|
||||||
|
default=datetime.datetime.now().isoformat(),
|
||||||
|
help=(
|
||||||
|
"Last insertion date to look for in the database,"
|
||||||
|
" formatted as ISO 8601 timestamp YYYY-MM-DDThh:mm:ss"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--database",
|
||||||
|
dest="database",
|
||||||
|
default="tfhe_rs",
|
||||||
|
help="Name of the database used to store results",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--hardware",
|
||||||
|
dest="hardware",
|
||||||
|
default="hpc7a.96xlarge",
|
||||||
|
help="Hardware reference used to perform benchmark",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--hardware-comp",
|
||||||
|
dest="hardware_comp",
|
||||||
|
help="Comma separated values of hardware to compare. "
|
||||||
|
"The first value would be chosen as baseline.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-V", "--project-version", dest="project_version", help="Commit hash reference"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b",
|
||||||
|
"--branch",
|
||||||
|
dest="branch",
|
||||||
|
default="main",
|
||||||
|
help="Git branch name on which benchmark was performed",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base-branch",
|
||||||
|
dest="base_branch",
|
||||||
|
default="main",
|
||||||
|
help="Git base branch name on which benchmark history can be fetched",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backend",
|
||||||
|
dest="backend",
|
||||||
|
choices=["cpu", "gpu"],
|
||||||
|
default="cpu",
|
||||||
|
help="Backend on which benchmarks have run",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tfhe-rs-layer",
|
||||||
|
dest="layer",
|
||||||
|
default="integer",
|
||||||
|
help="Layer of the tfhe-rs library to filter against",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pbs-kind",
|
||||||
|
dest="pbs_kind",
|
||||||
|
choices=["classical", "multi_bit", "any"],
|
||||||
|
default="classical",
|
||||||
|
help="Kind of PBS to look for",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--grouping-factor",
|
||||||
|
dest="grouping_factor",
|
||||||
|
type=int,
|
||||||
|
choices=[2, 3, 4],
|
||||||
|
help="Grouping factor used in multi-bit parameters set",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--time-span-days",
|
||||||
|
dest="time_span_days",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help="Numbers of days prior of `bench_date` we search for results in the database",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--regression-profiles",
|
||||||
|
dest="regression_profiles",
|
||||||
|
help="Path to file containing regression profiles formatted as TOML",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--regression-selected-profile",
|
||||||
|
dest="regression_selected_profile",
|
||||||
|
help="Regression profile to select from the regression profiles file to filter out database results",
|
||||||
|
)
|
||||||
|
exclusive_generation_group = parser.add_mutually_exclusive_group()
|
||||||
|
exclusive_generation_group.add_argument(
|
||||||
|
"--generate-markdown",
|
||||||
|
dest="generate_markdown",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate Markdown array",
|
||||||
|
)
|
||||||
|
exclusive_generation_group.add_argument(
|
||||||
|
"--generate-svg",
|
||||||
|
dest="generate_svg",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate SVG table formatted like ones in tfhe-rs documentation",
|
||||||
|
)
|
||||||
|
exclusive_generation_group.add_argument(
|
||||||
|
"--generate-svg-from-markdown",
|
||||||
|
dest="generate_svg_from_file",
|
||||||
|
help="Generate SVG table formatted like ones in tfhe-rs documentation from a Markdown table",
|
||||||
|
)
|
||||||
|
exclusive_generation_group.add_argument(
|
||||||
|
"--generate-regression-json",
|
||||||
|
dest="generate_regression_json",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate JSON file with regression data with all the results from base branch and the lastest results of the development branch",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_svg_from_file(
|
||||||
|
user_config: config.UserConfig, layer: Layer, input_file: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generates an SVG file based on a given formatted array in Markdown file.
|
||||||
|
|
||||||
|
:param user_config: An instance of the UserConfig class, used to manage
|
||||||
|
configuration details like backend, PBS kind, and output file paths.
|
||||||
|
:type user_config: config.UserConfig
|
||||||
|
:param layer: The layer information used in SVG formatting and
|
||||||
|
generation.
|
||||||
|
:type layer: Layer
|
||||||
|
:param input_file: File path of the input Markdown file to be converted to
|
||||||
|
SVG format.
|
||||||
|
:type input_file: str
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
utils.write_to_svg(
|
||||||
|
SVGFormatter(
|
||||||
|
layer,
|
||||||
|
user_config.backend,
|
||||||
|
user_config.pbs_kind,
|
||||||
|
).generate_svg_table_from_markdown_file(input_file),
|
||||||
|
user_config.output_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_hardware_comparison(
|
||||||
|
user_config: config.UserConfig,
|
||||||
|
layer: Layer,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Perform a hardware comparison by fetching benchmark data, computing
|
||||||
|
comparisons, and generating CSV outputs for each hardware configuration. It
|
||||||
|
outputs both raw data and gain-based analysis for comparison between
|
||||||
|
reference and target hardware.
|
||||||
|
|
||||||
|
:param user_config: An instance of the UserConfig class, used to manage
|
||||||
|
configuration details like backend, PBS kind, and output file paths.
|
||||||
|
:type user_config: config.UserConfig
|
||||||
|
:param layer: The layer object containing specific information required for
|
||||||
|
formatting and processing benchmark data.
|
||||||
|
:type layer: Layer
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for hw in hardware_list:
|
||||||
|
try:
|
||||||
|
res = conn.fetch_benchmark_data(user_config, operand_type)
|
||||||
|
except RuntimeError as err:
|
||||||
|
print(f"Failed to fetch benchmark data: {err}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
results.append(res)
|
||||||
|
|
||||||
|
output_filename = "".join(
|
||||||
|
[user_config.output_file, "_", hw, "_", operand_type.lower(), ".csv"]
|
||||||
|
)
|
||||||
|
csv_formatter = CSVFormatter(layer, user_config.backend, user_config.pbs_kind)
|
||||||
|
formatted_data = csv_formatter.format_data(
|
||||||
|
res, utils.convert_value_to_readable_text
|
||||||
|
)
|
||||||
|
utils.write_to_csv(
|
||||||
|
csv_formatter.generate_csv(formatted_data),
|
||||||
|
output_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
gains_results = formatter.compute_comparisons(*results)
|
||||||
|
reference_hardware = hardware_list[0]
|
||||||
|
for i, hw in enumerate(hardware_list[1:]):
|
||||||
|
output_filename = "".join(
|
||||||
|
[
|
||||||
|
user_config.output_file,
|
||||||
|
"_",
|
||||||
|
operand_type.lower(),
|
||||||
|
"_",
|
||||||
|
reference_hardware,
|
||||||
|
"_",
|
||||||
|
hw,
|
||||||
|
"_gains.csv",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
csv_formatter = CSVFormatter(layer, user_config.backend, user_config.pbs_kind)
|
||||||
|
formatted_data = csv_formatter.format_data(
|
||||||
|
gains_results[i],
|
||||||
|
utils.convert_gain_to_text,
|
||||||
|
)
|
||||||
|
utils.write_to_csv(
|
||||||
|
csv_formatter.generate_csv(formatted_data),
|
||||||
|
output_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_data_extraction(
|
||||||
|
user_config: config.UserConfig,
|
||||||
|
layer: Layer,
|
||||||
|
operand_type: OperandType,
|
||||||
|
output_filename: str,
|
||||||
|
generate_markdown: bool = False,
|
||||||
|
generate_svg: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Extracts, formats, and processes benchmark data for a specified operand type and
|
||||||
|
saves the results into various file formats such as CSV, Markdown, or SVG based
|
||||||
|
on user configuration.
|
||||||
|
|
||||||
|
:param user_config: An instance of the UserConfig class, used to manage
|
||||||
|
configuration details like backend, PBS kind, and output file paths.
|
||||||
|
:type user_config: config.UserConfig
|
||||||
|
:param layer: Layer object specifying the granularity and context of the
|
||||||
|
operand processing.
|
||||||
|
:type layer: Layer
|
||||||
|
:param operand_type: Type of operand data for which the benchmarks are
|
||||||
|
extracted and processed.
|
||||||
|
:type operand_type: OperandType
|
||||||
|
:param output_filename: The base filename for the output files where results
|
||||||
|
will be saved.
|
||||||
|
:type output_filename: str
|
||||||
|
:param generate_markdown: Boolean flag indicating whether to generate an
|
||||||
|
output file in Markdown (.md) format.
|
||||||
|
:type generate_markdown: bool
|
||||||
|
:param generate_svg: Boolean flag indicating whether to generate an output
|
||||||
|
file in SVG (.svg) format.
|
||||||
|
:type generate_svg: bool
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
res = conn.fetch_benchmark_data(user_config, operand_type)
|
||||||
|
except RuntimeError as err:
|
||||||
|
print(f"Failed to fetch benchmark data: {err}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
generic_formatter = GenericFormatter(
|
||||||
|
layer, user_config.backend, user_config.pbs_kind, user_config.grouping_factor
|
||||||
|
)
|
||||||
|
formatted_results = generic_formatter.format_data(
|
||||||
|
res,
|
||||||
|
utils.convert_value_to_readable_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
file_suffix = f"_{operand_type.lower()}"
|
||||||
|
filename = utils.append_suffix_to_filename(output_filename, file_suffix, ".csv")
|
||||||
|
|
||||||
|
utils.write_to_csv(
|
||||||
|
CSVFormatter(layer, user_config.backend, user_config.pbs_kind).generate_csv(
|
||||||
|
formatted_results
|
||||||
|
),
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
generic_arrays = generic_formatter.generate_array(
|
||||||
|
formatted_results,
|
||||||
|
operand_type,
|
||||||
|
excluded_types=[RustType.FheUint2, RustType.FheUint4, RustType.FheUint256],
|
||||||
|
)
|
||||||
|
|
||||||
|
for array in generic_arrays:
|
||||||
|
metadata_suffix = ""
|
||||||
|
if array.metadata:
|
||||||
|
for key, value in array.metadata.items():
|
||||||
|
metadata_suffix += f"_{key}_{value}"
|
||||||
|
|
||||||
|
current_suffix = file_suffix + metadata_suffix
|
||||||
|
|
||||||
|
if generate_markdown:
|
||||||
|
filename = utils.append_suffix_to_filename(
|
||||||
|
output_filename, current_suffix, ".md"
|
||||||
|
)
|
||||||
|
|
||||||
|
data_formatter = MarkdownFormatter(
|
||||||
|
layer, user_config.backend, user_config.pbs_kind
|
||||||
|
)
|
||||||
|
|
||||||
|
utils.write_to_markdown(
|
||||||
|
data_formatter.generate_markdown_array(array),
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
elif generate_svg:
|
||||||
|
filename = utils.append_suffix_to_filename(
|
||||||
|
output_filename, current_suffix, ".svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
data_formatter = SVGFormatter(
|
||||||
|
layer, user_config.backend, user_config.pbs_kind
|
||||||
|
)
|
||||||
|
|
||||||
|
utils.write_to_svg(
|
||||||
|
data_formatter.generate_svg_table(
|
||||||
|
array,
|
||||||
|
),
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parser.parse_args()
|
||||||
|
user_config = config.UserConfig(args)
|
||||||
|
layer = user_config.layer
|
||||||
|
|
||||||
|
if args.generate_svg_from_file:
|
||||||
|
generate_svg_from_file(user_config, layer, args.generate_svg_from_file)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
postgre_config = connector.PostgreConfig(args.config_file)
|
||||||
|
conn = connector.PostgreConnector(postgre_config)
|
||||||
|
conn.connect_to_database(user_config.database)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.generate_regression_json:
|
||||||
|
try:
|
||||||
|
regression.perform_regression_json_generation(conn, user_config)
|
||||||
|
except RuntimeError as err:
|
||||||
|
print(f"Failed to perform performance regression JSON: {err}")
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
hardware_list = (
|
||||||
|
args.hardware_comp.lower().split(",") if args.hardware_comp else None
|
||||||
|
)
|
||||||
|
|
||||||
|
for operand_type in (OperandType.CipherText, OperandType.PlainText):
|
||||||
|
if hardware_list:
|
||||||
|
perform_hardware_comparison(user_config, layer)
|
||||||
|
|
||||||
|
if args.generate_markdown:
|
||||||
|
print("Markdown generation is not supported with comparisons")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if layer == Layer.CoreCrypto and operand_type == OperandType.PlainText:
|
||||||
|
continue
|
||||||
|
|
||||||
|
perform_data_extraction(
|
||||||
|
user_config,
|
||||||
|
layer,
|
||||||
|
operand_type,
|
||||||
|
user_config.output_file,
|
||||||
|
generate_markdown=args.generate_markdown,
|
||||||
|
generate_svg=args.generate_svg,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
20
ci/data_extractor/src/exceptions.py
Normal file
20
ci/data_extractor/src/exceptions.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class NoDataFound(RuntimeError):
|
||||||
|
"""
|
||||||
|
Indicates that no data was found when an operation or search was performed.
|
||||||
|
|
||||||
|
This exception should be raised to signal that a requested operation could not
|
||||||
|
complete because the required data was unavailable. It is typically used in
|
||||||
|
cases where returning an empty result might not be appropriate, and an
|
||||||
|
explicit notice of failure is required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParametersFormatNotSupported(Exception):
|
||||||
|
"""
|
||||||
|
Exception raised for unsupported parameter formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, param_name):
|
||||||
|
super().__init__(f"Parameters format '{param_name}' not supported.")
|
||||||
940
ci/data_extractor/src/formatter.py
Normal file
940
ci/data_extractor/src/formatter.py
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
import collections
|
||||||
|
import copy
|
||||||
|
import enum
|
||||||
|
import pathlib
|
||||||
|
import xml.dom.minidom
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import svg
|
||||||
|
from benchmark_specs import (ALL_RUST_TYPES, Backend, BenchDetails,
|
||||||
|
CoreCryptoOperation, ErrorFailureProbability,
|
||||||
|
Layer, NoiseDistribution, OperandType, PBSKind,
|
||||||
|
RustType)
|
||||||
|
from py_markdown_table.markdown_table import markdown_table
|
||||||
|
|
||||||
|
|
||||||
|
def compute_comparisons(*results):
|
||||||
|
"""
|
||||||
|
Compute gains for data in ``results``. The first element is considered as the baseline.
|
||||||
|
|
||||||
|
:param results: :class:`list` of results
|
||||||
|
|
||||||
|
:return: :class:`list` of gains with length of ``results`` minus one (baseline is not sent back)
|
||||||
|
"""
|
||||||
|
gains = []
|
||||||
|
baseline = results[0]
|
||||||
|
|
||||||
|
for compared_results in results[1:]:
|
||||||
|
for key, base_value in baseline.items():
|
||||||
|
try:
|
||||||
|
compared_value = compared_results[key]
|
||||||
|
except KeyError:
|
||||||
|
# Ignore missing entries
|
||||||
|
continue
|
||||||
|
|
||||||
|
gain = round((base_value - compared_value) * 100 / base_value, 2)
|
||||||
|
compared_results[key] = gain
|
||||||
|
|
||||||
|
gains.append(compared_results)
|
||||||
|
|
||||||
|
return gains
|
||||||
|
|
||||||
|
|
||||||
|
class OperationDisplayName(enum.StrEnum):
|
||||||
|
Negation = "Negation (-)"
|
||||||
|
AddSub = "Add / Sub (+,-)"
|
||||||
|
Mul = "Mul (x)"
|
||||||
|
EqualNotEqual = "Equal / Not Equal (eq, ne)"
|
||||||
|
Comparisons = "Comparisons (ge, gt, le, lt)"
|
||||||
|
MaxMin = "Max / Min (max, min)"
|
||||||
|
Bitwise = "Bitwise operations (&, \\|, ^)"
|
||||||
|
Div = "Div (/)"
|
||||||
|
Rem = "Rem (%)"
|
||||||
|
DivRem = "Div / Rem (/, %)"
|
||||||
|
Shifts = "Left / Right Shifts (<<, >>)"
|
||||||
|
Rotates = "Left / Right Rotations (left_rotate, right_rotate)"
|
||||||
|
LeadingTrailing = "Leading / Trailing zeros/ones"
|
||||||
|
Log2 = "Log2"
|
||||||
|
Select = "Select"
|
||||||
|
|
||||||
|
|
||||||
|
class BenchArray:
|
||||||
|
def __init__(self, array, layer, metadata: dict = None):
|
||||||
|
self.array = array
|
||||||
|
self.layer = layer
|
||||||
|
self.metadata = metadata
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"BenchArray(layer={self.layer}, metadata={self.metadata})"
|
||||||
|
|
||||||
|
|
||||||
|
class GenericFormatter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
layer: Layer,
|
||||||
|
backend: Backend,
|
||||||
|
pbs_kind: PBSKind,
|
||||||
|
grouping_factor: int = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generic formatter for a given specified layer, backend, and PBS kind.
|
||||||
|
|
||||||
|
:param layer: Represents the layer configuration for the object.
|
||||||
|
:type layer: Layer
|
||||||
|
:param backend: Specifies the backend system to be used.
|
||||||
|
:type backend: Backend
|
||||||
|
:param pbs_kind: Specifies the type or kind of PBS (Private Backend System).
|
||||||
|
:type pbs_kind: PBSKind
|
||||||
|
:param grouping_factor: Specifies the multi-bit PBS grouping factor to use a filter.
|
||||||
|
:type grouping_factor: int, optional
|
||||||
|
"""
|
||||||
|
self.layer = layer
|
||||||
|
self.backend = backend
|
||||||
|
self.pbs_kind = pbs_kind
|
||||||
|
self.requested_grouping_factor = grouping_factor
|
||||||
|
|
||||||
|
def set_grouping_factor(self, grouping_factor: int):
|
||||||
|
"""
|
||||||
|
Sets the grouping factor for a computation or operation.
|
||||||
|
|
||||||
|
This method allows configuration of the requested grouping factor
|
||||||
|
value, which can affect how operations using multi-bit parameters set are processed.
|
||||||
|
|
||||||
|
:param grouping_factor: The desired grouping factor to use.
|
||||||
|
:type grouping_factor: int
|
||||||
|
"""
|
||||||
|
self.requested_grouping_factor = grouping_factor
|
||||||
|
|
||||||
|
def format_data(
|
||||||
|
self, data: dict[BenchDetails : list[int]], conversion_func: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Formats data based on the specified layer and applies a conversion function to
|
||||||
|
transform the data.
|
||||||
|
|
||||||
|
The method determines the data formatting logic by matching the
|
||||||
|
current layer of the object and invokes the appropriate specific layer-related
|
||||||
|
formatting function.
|
||||||
|
|
||||||
|
:param data: A dictionary where the keys are instances of `BenchDetails` and
|
||||||
|
the values are lists of integers, representing the benchmark data to be
|
||||||
|
formatted.
|
||||||
|
:type data: dict[BenchDetails : list[int]]
|
||||||
|
:param conversion_func: A callable function that will be applied to transform
|
||||||
|
the data values based on the specific layer requirements.
|
||||||
|
:type conversion_func: Callable
|
||||||
|
|
||||||
|
:return: The formatted data results after applying layer and conversion logic.
|
||||||
|
:rtype: Any
|
||||||
|
|
||||||
|
:raises NotImplementedError: Raised when the specified layer is unsupported.
|
||||||
|
"""
|
||||||
|
match self.layer:
|
||||||
|
case Layer.Integer:
|
||||||
|
return self._format_integer_data(data, conversion_func)
|
||||||
|
case Layer.CoreCrypto:
|
||||||
|
return self._format_core_crypto_data(data, conversion_func)
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(f"layer '{self.layer}' not supported yet")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_integer_data(data: dict[BenchDetails : list[int]], conversion_func):
|
||||||
|
formatted = collections.defaultdict(
|
||||||
|
lambda: {
|
||||||
|
2: "N/A",
|
||||||
|
8: "N/A",
|
||||||
|
16: "N/A",
|
||||||
|
32: "N/A",
|
||||||
|
64: "N/A",
|
||||||
|
128: "N/A",
|
||||||
|
256: "N/A",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for details, timings in data.items():
|
||||||
|
test_name = "_".join((details.sign_flavor.value, details.operation_name))
|
||||||
|
bit_width = details.bit_size
|
||||||
|
value = conversion_func(timings[-1])
|
||||||
|
|
||||||
|
if bit_width == 40:
|
||||||
|
# Ignore this width as it's not displayed publicly.
|
||||||
|
continue
|
||||||
|
|
||||||
|
formatted[test_name][bit_width] = value
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_core_crypto_data(data: dict[BenchDetails : list[int]], conversion_func):
|
||||||
|
params_set = set()
|
||||||
|
for details in data:
|
||||||
|
try:
|
||||||
|
params_set.add(details.get_params_definition())
|
||||||
|
except Exception:
|
||||||
|
# Might be a Boolean parameters set, ignoring
|
||||||
|
continue
|
||||||
|
|
||||||
|
params_set = sorted(params_set)
|
||||||
|
|
||||||
|
formatted = collections.defaultdict(
|
||||||
|
lambda: {params: "N/A" for params in params_set}
|
||||||
|
)
|
||||||
|
for details, timings in data.items():
|
||||||
|
try:
|
||||||
|
reduced_params = details.get_params_definition()
|
||||||
|
except Exception:
|
||||||
|
# Might be a Boolean parameters set, ignoring
|
||||||
|
continue
|
||||||
|
|
||||||
|
test_name = details.operation_name
|
||||||
|
value = conversion_func(timings[-1])
|
||||||
|
formatted[test_name][reduced_params] = value
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
def generate_array(
|
||||||
|
self,
|
||||||
|
data,
|
||||||
|
operand_type: OperandType = None,
|
||||||
|
excluded_types: list[RustType] = None,
|
||||||
|
) -> list[BenchArray]:
|
||||||
|
"""
|
||||||
|
Generates an array of `BenchArray` based on the specified layer and criteria.
|
||||||
|
|
||||||
|
This method takes input data and generates an array of `BenchArray` objects,
|
||||||
|
using the rules defined by the current `layer`. The behavior varies depending
|
||||||
|
on the active layer, and certain types can be explicitly excluded from the
|
||||||
|
generation process.
|
||||||
|
|
||||||
|
:param data: Input data to generate the array from.
|
||||||
|
:type data: Any
|
||||||
|
:param operand_type: Specifies the type of operand to guide the array generation.
|
||||||
|
Defaults to `None`.
|
||||||
|
:type operand_type: OperandType, optional
|
||||||
|
:param excluded_types: A list of `RustType` to exclude from array generation.
|
||||||
|
Defaults to `None`.
|
||||||
|
:type excluded_types: list[RustType], optional
|
||||||
|
|
||||||
|
:return: A list of generated `BenchArray` objects.
|
||||||
|
:rtype: list[BenchArray]
|
||||||
|
|
||||||
|
:raises NotImplementedError: If the current layer is not implemented.
|
||||||
|
"""
|
||||||
|
match self.layer:
|
||||||
|
case Layer.Integer:
|
||||||
|
return self._generate_unsigned_integer_array(
|
||||||
|
data, operand_type, excluded_types
|
||||||
|
)
|
||||||
|
case Layer.CoreCrypto:
|
||||||
|
return self._generate_core_crypto_showcase_arrays(data)
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _generate_unsigned_integer_array(
|
||||||
|
self,
|
||||||
|
data,
|
||||||
|
operand_type: OperandType = None,
|
||||||
|
excluded_types: list[RustType] = None,
|
||||||
|
):
|
||||||
|
match operand_type:
|
||||||
|
case OperandType.CipherText:
|
||||||
|
prefix = "unsigned"
|
||||||
|
case OperandType.PlainText:
|
||||||
|
prefix = "unsigned_scalar"
|
||||||
|
|
||||||
|
match self.backend:
|
||||||
|
case Backend.CPU:
|
||||||
|
operations = [
|
||||||
|
f"{prefix}_neg_parallelized",
|
||||||
|
f"{prefix}_add_parallelized",
|
||||||
|
f"{prefix}_mul_parallelized",
|
||||||
|
f"{prefix}_eq_parallelized",
|
||||||
|
f"{prefix}_gt_parallelized",
|
||||||
|
f"{prefix}_max_parallelized",
|
||||||
|
f"{prefix}_bitand_parallelized",
|
||||||
|
f"{prefix}_div_rem_parallelized",
|
||||||
|
f"{prefix}_left_shift_parallelized",
|
||||||
|
f"{prefix}_rotate_left_parallelized",
|
||||||
|
f"{prefix}_leading_zeros_parallelized",
|
||||||
|
f"{prefix}_ilog2_parallelized",
|
||||||
|
f"{prefix}_if_then_else_parallelized",
|
||||||
|
]
|
||||||
|
case Backend.GPU:
|
||||||
|
match operand_type:
|
||||||
|
case OperandType.CipherText:
|
||||||
|
prefix = "cuda"
|
||||||
|
case OperandType.PlainText:
|
||||||
|
prefix = "cuda_scalar"
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
f"{prefix}_neg",
|
||||||
|
f"{prefix}_add",
|
||||||
|
f"{prefix}_mul",
|
||||||
|
f"{prefix}_eq",
|
||||||
|
f"{prefix}_gt",
|
||||||
|
f"{prefix}_max",
|
||||||
|
f"{prefix}_bitand",
|
||||||
|
f"{prefix}_div_rem",
|
||||||
|
f"{prefix}_left_shift",
|
||||||
|
f"{prefix}_rotate_left",
|
||||||
|
f"{prefix}_leading_zeros",
|
||||||
|
f"{prefix}_ilog2",
|
||||||
|
f"{prefix}_if_then_else",
|
||||||
|
]
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"backend '{self.backend}' not supported yet for integer formatting"
|
||||||
|
)
|
||||||
|
|
||||||
|
display_names = [
|
||||||
|
OperationDisplayName.Negation,
|
||||||
|
OperationDisplayName.AddSub,
|
||||||
|
OperationDisplayName.Mul,
|
||||||
|
OperationDisplayName.EqualNotEqual,
|
||||||
|
OperationDisplayName.Comparisons,
|
||||||
|
OperationDisplayName.MaxMin,
|
||||||
|
OperationDisplayName.Bitwise,
|
||||||
|
OperationDisplayName.DivRem,
|
||||||
|
OperationDisplayName.Shifts,
|
||||||
|
OperationDisplayName.Rotates,
|
||||||
|
OperationDisplayName.LeadingTrailing,
|
||||||
|
OperationDisplayName.Log2,
|
||||||
|
OperationDisplayName.Select,
|
||||||
|
]
|
||||||
|
|
||||||
|
types = ALL_RUST_TYPES.copy()
|
||||||
|
excluded_types = excluded_types if excluded_types is not None else []
|
||||||
|
for excluded in excluded_types:
|
||||||
|
types.remove(excluded)
|
||||||
|
|
||||||
|
first_column_header = "Operation \\ Size"
|
||||||
|
|
||||||
|
# Adapt list to plaintext benchmarks results.
|
||||||
|
if operand_type == OperandType.PlainText:
|
||||||
|
if self.backend == Backend.CPU:
|
||||||
|
div_name = f"{prefix}_div_parallelized"
|
||||||
|
rem_name = f"{prefix}_rem_parallelized"
|
||||||
|
elif self.backend == Backend.GPU:
|
||||||
|
div_name = f"{prefix}_div"
|
||||||
|
rem_name = f"{prefix}_rem"
|
||||||
|
|
||||||
|
operations.insert(8, div_name)
|
||||||
|
operations.insert(9, rem_name)
|
||||||
|
operations.pop(7) # Remove div_rem_parallelized
|
||||||
|
|
||||||
|
display_names.insert(
|
||||||
|
8,
|
||||||
|
OperationDisplayName.Div,
|
||||||
|
)
|
||||||
|
display_names.insert(
|
||||||
|
9,
|
||||||
|
OperationDisplayName.Rem,
|
||||||
|
)
|
||||||
|
display_names.pop(7) # Remove Div / Rem
|
||||||
|
|
||||||
|
# Negation operation doesn't exist in plaintext
|
||||||
|
operations.pop(0)
|
||||||
|
display_names.pop(0)
|
||||||
|
|
||||||
|
data_without_excluded_types = copy.deepcopy(data)
|
||||||
|
for v in data_without_excluded_types.values():
|
||||||
|
for excluded in excluded_types:
|
||||||
|
try:
|
||||||
|
v.pop(excluded.value)
|
||||||
|
except KeyError:
|
||||||
|
# Type is not contained in the results, ignoring
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered_data = filter(lambda t: t in operations, data_without_excluded_types)
|
||||||
|
# Get operation names as key of the dict to ease fetching
|
||||||
|
filtered_data_dict = {
|
||||||
|
item: tuple(data_without_excluded_types[item].values())
|
||||||
|
for item in filtered_data
|
||||||
|
}
|
||||||
|
|
||||||
|
result_lines = []
|
||||||
|
for name, op in zip(display_names, operations):
|
||||||
|
try:
|
||||||
|
line = {first_column_header: name.value}
|
||||||
|
line.update(
|
||||||
|
{
|
||||||
|
types[i].name: value
|
||||||
|
for i, value in enumerate(filtered_data_dict[op])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result_lines.append(line)
|
||||||
|
except KeyError:
|
||||||
|
# Operation not found in the results, ignoring this line.
|
||||||
|
print(
|
||||||
|
f"backend '{self.backend}' could not find operation '{op}' to put in line '{name}'"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return [
|
||||||
|
BenchArray(result_lines, self.layer),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _build_results_dict(
|
||||||
|
self,
|
||||||
|
pfails: list[ErrorFailureProbability],
|
||||||
|
noise_distributions: list[NoiseDistribution],
|
||||||
|
operation_displays: list[CoreCryptoOperation],
|
||||||
|
default_precisions: Callable[[], dict],
|
||||||
|
):
|
||||||
|
results_dict = {}
|
||||||
|
|
||||||
|
for pfail in pfails:
|
||||||
|
for noise in noise_distributions:
|
||||||
|
results_dict[CoreCryptoResultsKey(pfail, noise)] = {
|
||||||
|
o: default_precisions() for o in operation_displays
|
||||||
|
}
|
||||||
|
|
||||||
|
return results_dict
|
||||||
|
|
||||||
|
def _generate_core_crypto_showcase_arrays(
|
||||||
|
self,
|
||||||
|
data,
|
||||||
|
):
|
||||||
|
supported_pfails = [
|
||||||
|
ErrorFailureProbability.TWO_MINUS_40,
|
||||||
|
ErrorFailureProbability.TWO_MINUS_64,
|
||||||
|
ErrorFailureProbability.TWO_MINUS_128,
|
||||||
|
]
|
||||||
|
noise_distributions = [
|
||||||
|
NoiseDistribution.Gaussian,
|
||||||
|
NoiseDistribution.TUniform,
|
||||||
|
]
|
||||||
|
|
||||||
|
operation_displays = [op.value for op in OPERATIONS_DISPLAYS]
|
||||||
|
|
||||||
|
sorted_results = self._build_results_dict(
|
||||||
|
supported_pfails,
|
||||||
|
noise_distributions,
|
||||||
|
OPERATIONS_DISPLAYS,
|
||||||
|
DEFAULT_CORE_CRYPTO_PRECISIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
for operation, timings in data.items():
|
||||||
|
try:
|
||||||
|
formatted_name = CoreCryptoOperation.from_str(operation)
|
||||||
|
except NotImplementedError:
|
||||||
|
# Operation is not supported.
|
||||||
|
continue
|
||||||
|
|
||||||
|
for param_definition, value in timings.items():
|
||||||
|
pfail = param_definition.p_fail
|
||||||
|
if pfail not in supported_pfails:
|
||||||
|
print(f"[{operation}] P-fail '{pfail}' is not supported")
|
||||||
|
continue
|
||||||
|
noise = param_definition.noise_distribution
|
||||||
|
precision = int(param_definition.message_size) * 2
|
||||||
|
key = CoreCryptoResultsKey(pfail, noise)
|
||||||
|
|
||||||
|
if (
|
||||||
|
formatted_name == CoreCryptoOperation.MultiBitPBS
|
||||||
|
or formatted_name == CoreCryptoOperation.KeySwitchMultiBitPBS
|
||||||
|
) and param_definition.pbs_kind != PBSKind.MultiBit:
|
||||||
|
# Skip this operation since a multi-bit operation cannot be done with any other parameters type.
|
||||||
|
continue
|
||||||
|
|
||||||
|
grouping_factor = param_definition.grouping_factor
|
||||||
|
if (
|
||||||
|
grouping_factor is not None
|
||||||
|
and grouping_factor != self.requested_grouping_factor
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
param_definition.details["variation"]
|
||||||
|
or param_definition.details["trailing_details"]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
sorted_results[key][formatted_name][precision] = value
|
||||||
|
except KeyError:
|
||||||
|
# Operation is not supposed to appear in the formatted array.
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_column_header = "Operation \\ Precision (bits)"
|
||||||
|
|
||||||
|
arrays = []
|
||||||
|
for key, results in sorted_results.items():
|
||||||
|
array = []
|
||||||
|
for operation, timings in results.items():
|
||||||
|
d = {first_column_header: operation.value}
|
||||||
|
d.update({str(k): v for k, v in timings.items()})
|
||||||
|
array.append(d)
|
||||||
|
|
||||||
|
arrays.append(
|
||||||
|
BenchArray(
|
||||||
|
array,
|
||||||
|
self.layer,
|
||||||
|
metadata={"pfail": key.pfail, "noise": key.noise_distribution},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return arrays
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Core_crypto layer constants
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
|
OPERATIONS_DISPLAYS = [
|
||||||
|
# CoreCryptoOperation.KeySwitch, # Uncomment this line to get keyswitch in the tables
|
||||||
|
CoreCryptoOperation.PBS,
|
||||||
|
CoreCryptoOperation.MultiBitPBS,
|
||||||
|
CoreCryptoOperation.KeyswitchPBS,
|
||||||
|
CoreCryptoOperation.KeySwitchMultiBitPBS,
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_CORE_CRYPTO_PRECISIONS = lambda: {
|
||||||
|
2: "N/A",
|
||||||
|
4: "N/A",
|
||||||
|
6: "N/A",
|
||||||
|
8: "N/A",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CoreCryptoResultsKey:
|
||||||
|
"""
|
||||||
|
Representation of a hashable result key for the core_crypto layer.
|
||||||
|
|
||||||
|
:param pfail: Probability of failure associated with the cryptographic result.
|
||||||
|
:type pfail: ErrorFailureProbability
|
||||||
|
:param noise_distribution: Noise distribution parameter linked to the
|
||||||
|
cryptographic result.
|
||||||
|
:type noise_distribution: NoiseDistribution
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, pfail: ErrorFailureProbability, noise_distribution: NoiseDistribution
|
||||||
|
):
|
||||||
|
self.pfail = pfail
|
||||||
|
self.noise_distribution = noise_distribution
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
self.pfail == other.pfail
|
||||||
|
and self.noise_distribution == other.noise_distribution
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.pfail, self.noise_distribution))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"CoreCryptoResultsKey(pfail={self.pfail}, noise_distribution={self.noise_distribution})"
|
||||||
|
|
||||||
|
|
||||||
|
class CSVFormatter(GenericFormatter):
|
||||||
|
"""
|
||||||
|
Formatter to generate CSV content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_csv(self, data: dict[str, collections.defaultdict]) -> list[list]:
|
||||||
|
"""
|
||||||
|
Generates a CSV-compatible data structure based on the provided input data and
|
||||||
|
the current layer type. The method processes the input to construct headers and
|
||||||
|
rows suitable for CSV writing.
|
||||||
|
|
||||||
|
:param data: A dictionary where keys represent row identifiers and values
|
||||||
|
are dictionaries of column-value pairs representing the data for
|
||||||
|
each row.
|
||||||
|
:type data: dict[str, collections.defaultdict]
|
||||||
|
|
||||||
|
:return: A list of lists where each sub-list represents a row in the CSV,
|
||||||
|
including the header row.
|
||||||
|
:rtype: list
|
||||||
|
|
||||||
|
:raises NotImplementedError: If the layer type specified in the object's
|
||||||
|
`layer` attribute is unsupported.
|
||||||
|
"""
|
||||||
|
headers_values = data.get(list(data)[0]).keys()
|
||||||
|
|
||||||
|
match self.layer:
|
||||||
|
case Layer.Integer:
|
||||||
|
headers = ["Operation \\ Size(bit)", *headers_values]
|
||||||
|
case Layer.CoreCrypto:
|
||||||
|
headers = ["Operation \\ Parameters set", *headers_values]
|
||||||
|
case _:
|
||||||
|
print(
|
||||||
|
f"tfhe-rs layer '{self.layer}' currently not supported for CSV writing"
|
||||||
|
)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
csv_data = [headers]
|
||||||
|
csv_data.extend(
|
||||||
|
[[key, *list(values_dict.values())] for key, values_dict in data.items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
return csv_data
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownFormatter(GenericFormatter):
|
||||||
|
"""
|
||||||
|
Formatter to generate Markdown content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_markdown_array(
|
||||||
|
self,
|
||||||
|
generic_array: BenchArray,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generates a Markdown representation of the provided generic array.
|
||||||
|
|
||||||
|
:param generic_array: The input array encapsulated in a BenchArray object.
|
||||||
|
|
||||||
|
:return: A Markdown formatted string representing the input array.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
md_array = (
|
||||||
|
markdown_table(generic_array.array)
|
||||||
|
.set_params(row_sep="markdown", quote=False, padding_weight="right")
|
||||||
|
.get_markdown()
|
||||||
|
)
|
||||||
|
|
||||||
|
return md_array
|
||||||
|
|
||||||
|
|
||||||
|
# -------------
|
||||||
|
# SVG constants
|
||||||
|
# -------------
|
||||||
|
|
||||||
|
BLACK_COLOR = "black"
|
||||||
|
WHITE_COLOR = "white"
|
||||||
|
LIGHT_GREY_COLOR = "#f3f3f3"
|
||||||
|
YELLOW_COLOR = "#fbbc04"
|
||||||
|
|
||||||
|
FONT_FAMILY = "Arial"
|
||||||
|
FONT_SIZE = 14
|
||||||
|
|
||||||
|
BORDER_WIDTH_PIXEL = 2
|
||||||
|
# Operation name is always in table first column
|
||||||
|
OPERATION_NAME_HORIZONTAL_POSITION = 6
|
||||||
|
|
||||||
|
SPECIAL_CHARS_PAIRS = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
"\\|": "|",
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------
|
||||||
|
|
||||||
|
|
||||||
|
class SVGFormatter(GenericFormatter):
|
||||||
|
"""
|
||||||
|
Formatter to generate SVG content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _transform_special_characters(strg: str):
|
||||||
|
for char, replacement in SPECIAL_CHARS_PAIRS.items():
|
||||||
|
if char in strg:
|
||||||
|
strg = strg.replace(char, replacement)
|
||||||
|
|
||||||
|
return strg
|
||||||
|
|
||||||
|
def _build_svg_headers_row(
|
||||||
|
self,
|
||||||
|
layer: Layer,
|
||||||
|
headers,
|
||||||
|
overall_width,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
):
|
||||||
|
op_header = headers.pop(0)
|
||||||
|
header_elements = [
|
||||||
|
svg.Rect(
|
||||||
|
x=0, y=0, width=overall_width, height=row_height, fill=BLACK_COLOR
|
||||||
|
),
|
||||||
|
self._build_svg_text(
|
||||||
|
OPERATION_NAME_HORIZONTAL_POSITION,
|
||||||
|
row_height / 2,
|
||||||
|
op_header,
|
||||||
|
text_anchor="start",
|
||||||
|
fill=WHITE_COLOR,
|
||||||
|
font_weight="bold",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for row_idx, type_ident in enumerate(headers):
|
||||||
|
curr_x = op_name_col_width + row_idx * per_timing_col_width
|
||||||
|
|
||||||
|
match layer:
|
||||||
|
case Layer.Integer:
|
||||||
|
type_name_width = type_ident.strip("FheUint")
|
||||||
|
header_elements.extend(
|
||||||
|
[
|
||||||
|
# Rust type class
|
||||||
|
self._build_svg_text(
|
||||||
|
curr_x + per_timing_col_width / 2,
|
||||||
|
row_height / 3,
|
||||||
|
"FheUint",
|
||||||
|
fill=WHITE_COLOR,
|
||||||
|
font_weight="bold",
|
||||||
|
),
|
||||||
|
# Actual size of the Rust type
|
||||||
|
self._build_svg_text(
|
||||||
|
curr_x + per_timing_col_width / 2,
|
||||||
|
2 * row_height / 3 + 3,
|
||||||
|
type_name_width,
|
||||||
|
fill=WHITE_COLOR,
|
||||||
|
font_weight="bold",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
case Layer.CoreCrypto:
|
||||||
|
header_elements.append(
|
||||||
|
# Core_crypto arrays contains only ciphertext modulus size as headers
|
||||||
|
self._build_svg_text(
|
||||||
|
curr_x + per_timing_col_width / 2,
|
||||||
|
row_height / 2,
|
||||||
|
type_ident,
|
||||||
|
fill=WHITE_COLOR,
|
||||||
|
font_weight="bold",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
return header_elements
|
||||||
|
|
||||||
|
def _build_svg_timing_row(
|
||||||
|
self,
|
||||||
|
timings_row,
|
||||||
|
row_y_pos,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
):
|
||||||
|
timing_elements = []
|
||||||
|
op_name = timings_row.pop(0)
|
||||||
|
timing_elements.append(
|
||||||
|
self._build_svg_text(
|
||||||
|
OPERATION_NAME_HORIZONTAL_POSITION,
|
||||||
|
row_y_pos + row_height / 2,
|
||||||
|
op_name,
|
||||||
|
text_anchor="start",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for timing_idx, timing in enumerate(timings_row):
|
||||||
|
timing_elements.append(
|
||||||
|
self._build_svg_text(
|
||||||
|
op_name_col_width
|
||||||
|
+ timing_idx * per_timing_col_width
|
||||||
|
+ per_timing_col_width / 2,
|
||||||
|
row_y_pos + row_height / 2,
|
||||||
|
timing,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return timing_elements
|
||||||
|
|
||||||
|
def _build_svg_text(
|
||||||
|
self, x, y, text, text_anchor="middle", fill=BLACK_COLOR, font_weight="normal"
|
||||||
|
):
|
||||||
|
return svg.Text(
|
||||||
|
x=x,
|
||||||
|
y=y,
|
||||||
|
dominant_baseline="middle",
|
||||||
|
text_anchor=text_anchor,
|
||||||
|
font_family=FONT_FAMILY,
|
||||||
|
font_size=FONT_SIZE,
|
||||||
|
fill=fill,
|
||||||
|
text=text,
|
||||||
|
font_weight=font_weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_svg_borders(
|
||||||
|
self,
|
||||||
|
overall_width,
|
||||||
|
overall_height,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
row_count,
|
||||||
|
col_count,
|
||||||
|
):
|
||||||
|
border_elements = []
|
||||||
|
|
||||||
|
# Horizontal borders, scrolling vertically
|
||||||
|
for row_idx in range(row_count + 2):
|
||||||
|
row_y = row_idx * row_height
|
||||||
|
border_elements.append(
|
||||||
|
svg.Line(
|
||||||
|
x1=0,
|
||||||
|
y1=row_y,
|
||||||
|
x2=overall_width,
|
||||||
|
y2=row_y,
|
||||||
|
stroke=WHITE_COLOR,
|
||||||
|
stroke_width=BORDER_WIDTH_PIXEL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vertical borders, scrolling horizontally
|
||||||
|
# Left border
|
||||||
|
border_elements.append(
|
||||||
|
svg.Line(
|
||||||
|
x1=0,
|
||||||
|
y1=0,
|
||||||
|
x2=0,
|
||||||
|
y2=overall_height,
|
||||||
|
stroke=WHITE_COLOR,
|
||||||
|
stroke_width=BORDER_WIDTH_PIXEL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Timing cols
|
||||||
|
for col_idx in range(col_count + 1):
|
||||||
|
col_x = op_name_col_width + col_idx * per_timing_col_width
|
||||||
|
border_elements.append(
|
||||||
|
svg.Line(
|
||||||
|
x1=col_x,
|
||||||
|
y1=0,
|
||||||
|
x2=col_x,
|
||||||
|
y2=overall_height,
|
||||||
|
stroke=WHITE_COLOR,
|
||||||
|
stroke_width=BORDER_WIDTH_PIXEL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return border_elements
|
||||||
|
|
||||||
|
def generate_svg_table(self, generic_array: BenchArray) -> str:
|
||||||
|
"""
|
||||||
|
Generates an SVG representation of a table from the given `BenchArray` object.
|
||||||
|
This method processes array data to create a visual representation of the
|
||||||
|
provided headers and values as an SVG image, organizing them into rows and
|
||||||
|
columns consistent with the structure of the input.
|
||||||
|
|
||||||
|
:param generic_array: The BenchArray object containing structured data for
|
||||||
|
generating the table.
|
||||||
|
:type generic_array: BenchArray
|
||||||
|
|
||||||
|
:return: An SVG representation of the table as a well-formatted string.
|
||||||
|
The SVG is generated with appropriate dimensions, colors, and styles
|
||||||
|
to ensure a visually clear and consistent layout.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
headers = list(generic_array.array[0].keys())
|
||||||
|
ops = generic_array.array[:]
|
||||||
|
|
||||||
|
# TODO Create a class to handle table dimension which will depend on tfhe-rs layer
|
||||||
|
col_count = len(headers) - 1
|
||||||
|
row_count = 1 + len(ops)
|
||||||
|
|
||||||
|
overall_width = int(round(900 / 1.25))
|
||||||
|
row_height = int(round(50 / 1.25))
|
||||||
|
op_name_col_width = int(round(375 / 1.25))
|
||||||
|
per_timing_col_width = (overall_width - op_name_col_width) / col_count
|
||||||
|
overall_height = row_count * row_height
|
||||||
|
|
||||||
|
svg_elements = []
|
||||||
|
|
||||||
|
# Generate headers row
|
||||||
|
svg_elements.extend(
|
||||||
|
self._build_svg_headers_row(
|
||||||
|
self.layer,
|
||||||
|
headers,
|
||||||
|
overall_width,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate operations rectangle
|
||||||
|
yellow_rect_for_op_names = svg.Rect(
|
||||||
|
x=0,
|
||||||
|
y=row_height,
|
||||||
|
width=op_name_col_width,
|
||||||
|
height=overall_height - row_height,
|
||||||
|
fill=YELLOW_COLOR,
|
||||||
|
)
|
||||||
|
svg_elements.append(yellow_rect_for_op_names)
|
||||||
|
|
||||||
|
# Generate timings rectangle
|
||||||
|
grey_rect_for_timings = svg.Rect(
|
||||||
|
x=op_name_col_width,
|
||||||
|
y=row_height,
|
||||||
|
width=overall_width - op_name_col_width,
|
||||||
|
height=overall_height - row_height,
|
||||||
|
fill=LIGHT_GREY_COLOR,
|
||||||
|
)
|
||||||
|
svg_elements.append(grey_rect_for_timings)
|
||||||
|
|
||||||
|
for row_count, row in enumerate(ops):
|
||||||
|
row_y = row_height + row_count * row_height
|
||||||
|
row_split = [
|
||||||
|
self._transform_special_characters(v)
|
||||||
|
for v in filter(None, row.values())
|
||||||
|
]
|
||||||
|
svg_elements.extend(
|
||||||
|
self._build_svg_timing_row(
|
||||||
|
row_split,
|
||||||
|
row_y,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate borders
|
||||||
|
svg_elements.extend(
|
||||||
|
self._build_svg_borders(
|
||||||
|
overall_width,
|
||||||
|
overall_height,
|
||||||
|
row_height,
|
||||||
|
op_name_col_width,
|
||||||
|
per_timing_col_width,
|
||||||
|
row_count,
|
||||||
|
col_count,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas = svg.SVG(
|
||||||
|
width="100%",
|
||||||
|
height=overall_height,
|
||||||
|
viewBox=f"0 0 {overall_width} {overall_height}",
|
||||||
|
elements=svg_elements,
|
||||||
|
preserveAspectRatio="meet",
|
||||||
|
)
|
||||||
|
|
||||||
|
dom = xml.dom.minidom.parseString(str(canvas))
|
||||||
|
return dom.toprettyxml()
|
||||||
|
|
||||||
|
def generate_svg_table_from_markdown_file(
|
||||||
|
self,
|
||||||
|
input_filepath: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generates an SVG table from a Markdown file.
|
||||||
|
|
||||||
|
This method reads a Markdown file, parses the table content, and transforms
|
||||||
|
it into an SVG representation. The input Markdown file must have headers
|
||||||
|
defined in the first row and rows of data separated by the "|" character.
|
||||||
|
|
||||||
|
:param input_filepath: The file path to the Markdown file.
|
||||||
|
:type input_filepath: str
|
||||||
|
|
||||||
|
:return: The generated SVG representation of the parsed Markdown table.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
with pathlib.Path(input_filepath).open("r") as f:
|
||||||
|
md_lines = f.read().splitlines()
|
||||||
|
|
||||||
|
headers = [h.lstrip().rstrip() for h in md_lines.pop(0).split("|")[1:-1]]
|
||||||
|
|
||||||
|
md_lines.pop(0) # remove separation line
|
||||||
|
parsed_data = []
|
||||||
|
for line in md_lines:
|
||||||
|
values = [v.lstrip().rstrip() for v in line.split("|")[1:-1]]
|
||||||
|
if len(values) > len(headers):
|
||||||
|
# A '|' character is contained in the operation name
|
||||||
|
values[0] += "".join(["|", values.pop(1)])
|
||||||
|
parsed_data.append({k: v for k, v in zip(headers, values)})
|
||||||
|
|
||||||
|
array = BenchArray(parsed_data, self.layer)
|
||||||
|
|
||||||
|
return self.generate_svg_table(array)
|
||||||
183
ci/data_extractor/src/regression.py
Normal file
183
ci/data_extractor/src/regression.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import config
|
||||||
|
import connector
|
||||||
|
from benchmark_specs import Backend, Layer
|
||||||
|
from exceptions import NoDataFound
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib # Python v3.11+
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
import pip._vendor.tomli as tomllib # the same tomllib that's now included in Python v3.11+
|
||||||
|
|
||||||
|
|
||||||
|
def generate_json_regression_file(
|
||||||
|
conn: connector.PostgreConnector, ops_filter: dict, user_config: config.UserConfig
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate a JSON regression file based on benchmark data from specified branches
|
||||||
|
and operations.
|
||||||
|
|
||||||
|
This function extracts benchmark data for a set of operations across HEAD
|
||||||
|
and BASE branches, processes it, and generates a structured JSON output
|
||||||
|
representing the benchmarking comparison.
|
||||||
|
|
||||||
|
:param conn: Connector to the benchmark data source.
|
||||||
|
:type conn: Any
|
||||||
|
:param ops_filter: A dictionary containing operations as keys and their
|
||||||
|
respective filters as values.
|
||||||
|
:type ops_filter: dict
|
||||||
|
:param user_config: A configuration object that contains branch information,
|
||||||
|
backend settings, and output preferences for regression.
|
||||||
|
:type user_config: UserConfig
|
||||||
|
|
||||||
|
:raises NoDataFound: If benchmark data is not found for the HEAD or BASE branch
|
||||||
|
during processing.
|
||||||
|
:raises json.JSONDecodeError: If the final dictionary cannot be converted into
|
||||||
|
JSON format.
|
||||||
|
"""
|
||||||
|
error_msg = "Cannot generate JSON regression file (error: no data found on {} branch '{}' (layer: {}, operations: {}))"
|
||||||
|
regression_data = {}
|
||||||
|
|
||||||
|
for bench_target, ops in ops_filter.items():
|
||||||
|
layer_name, _, bench_id = bench_target.partition("-")
|
||||||
|
layer = Layer.from_str(layer_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
head_branch_data = conn.fetch_benchmark_data(
|
||||||
|
user_config,
|
||||||
|
operation_filter=ops,
|
||||||
|
layer=layer,
|
||||||
|
branch=user_config.head_branch,
|
||||||
|
name_suffix="_mean_regression",
|
||||||
|
last_value_only=True,
|
||||||
|
)
|
||||||
|
except NoDataFound:
|
||||||
|
print(error_msg.format("HEAD", user_config.head_branch, layer, ops))
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
base_branch_data = conn.fetch_benchmark_data(
|
||||||
|
user_config,
|
||||||
|
operation_filter=ops,
|
||||||
|
layer=layer,
|
||||||
|
branch=user_config.base_branch,
|
||||||
|
last_value_only=False,
|
||||||
|
)
|
||||||
|
except NoDataFound:
|
||||||
|
print(error_msg.format("BASE", user_config.base_branch, layer, ops))
|
||||||
|
raise
|
||||||
|
|
||||||
|
for bench_details, values in head_branch_data.items():
|
||||||
|
regression_data[bench_details.operation_name] = {
|
||||||
|
"name": bench_details.operation_name,
|
||||||
|
"bit_size": bench_details.bit_size,
|
||||||
|
"params": bench_details.params,
|
||||||
|
"results": {
|
||||||
|
"head": {"name": user_config.head_branch, "value": values[0]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for bench_details, values in base_branch_data.items():
|
||||||
|
try:
|
||||||
|
reg = regression_data[bench_details.operation_name]
|
||||||
|
if (
|
||||||
|
reg["bit_size"] == bench_details.bit_size
|
||||||
|
and reg["params"] == bench_details.params
|
||||||
|
):
|
||||||
|
reg["results"]["base"] = {
|
||||||
|
"name": user_config.base_branch,
|
||||||
|
"data": values,
|
||||||
|
}
|
||||||
|
except KeyError:
|
||||||
|
# No value exists on the head branch for this key, ignoring the base branch result.
|
||||||
|
continue
|
||||||
|
|
||||||
|
final_dict = {
|
||||||
|
"backend": user_config.backend,
|
||||||
|
"profile": user_config.regression_selected_profile,
|
||||||
|
"operations": list(regression_data.values()),
|
||||||
|
}
|
||||||
|
|
||||||
|
output_file = "".join([user_config.output_file, ".json"])
|
||||||
|
utils.write_to_json(final_dict, output_file)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_toml_file(
|
||||||
|
path: str, backend: Backend, profile_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
"""
|
||||||
|
Parse a TOML file defining regression profiles and return its content as a dictionary.
|
||||||
|
|
||||||
|
:param path: path to TOML file
|
||||||
|
:type path: str
|
||||||
|
:param backend: type of backend used for the benchmarks
|
||||||
|
:type backend: Backend
|
||||||
|
:param profile_name: name of the regression profile
|
||||||
|
:type profile_name: str
|
||||||
|
|
||||||
|
:return: profile content formatted as `{"<benchmark_target>": ["<operation_1>", ...]}`
|
||||||
|
:rtype: dict[str, list[str]]
|
||||||
|
"""
|
||||||
|
file_path = pathlib.Path(path)
|
||||||
|
try:
|
||||||
|
return tomllib.loads(file_path.read_text())[backend][profile_name]
|
||||||
|
except tomllib.TOMLDecodeError as err:
|
||||||
|
raise RuntimeError(f"failed to parse definition file (error: {err})")
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"failed to find definition profile (error: profile '{profile_name}' cannot be found in '{file_path}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_regression_json_generation(
|
||||||
|
conn: connector.PostgreConnector, user_config: config.UserConfig
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generates a JSON file for regression testing based on the selected regression profile.
|
||||||
|
|
||||||
|
This function generates a JSON file with benchmarks for the specified regression profile.
|
||||||
|
The function requires the `regression_selected_profile` attribute of the `user_config` parameter if
|
||||||
|
custom regression profile operations are to be filtered. If no such profile is provided or no profile
|
||||||
|
definitions file is specified, the function terminates the program with an appropriate message.
|
||||||
|
|
||||||
|
:param conn: PostgreConnector object used for generating the JSON regression file.
|
||||||
|
:type conn: Any
|
||||||
|
:param user_config: Configuration object containing user-defined regression profiles and settings.
|
||||||
|
:type user_config: UserConfig
|
||||||
|
|
||||||
|
:raises SystemExit: If no regression profile is selected, or if the regression profiles
|
||||||
|
file is not provided
|
||||||
|
"""
|
||||||
|
# TODO Currently this implementation doesn't support custom regression profile.
|
||||||
|
# It will return strictly the benchmarks results for the operations defined for the selected profile.
|
||||||
|
# One way to handle that case would be to pass the custom profile as program input (could be either via a string or path containing the profile).
|
||||||
|
if not user_config.regression_selected_profile:
|
||||||
|
print(
|
||||||
|
"Regression generation requires a profile name (see: --regression-selected-profile input argument)"
|
||||||
|
)
|
||||||
|
sys.exit(5)
|
||||||
|
selected_profile = user_config.regression_selected_profile
|
||||||
|
|
||||||
|
operations_filter = {}
|
||||||
|
if user_config.regression_profiles_path:
|
||||||
|
profile_definition = parse_toml_file(
|
||||||
|
user_config.regression_profiles_path, user_config.backend, selected_profile
|
||||||
|
)
|
||||||
|
operations_filter = profile_definition["target"]
|
||||||
|
try:
|
||||||
|
user_config.pbs_kind = profile_definition["env"]["bench_param_type"].lower()
|
||||||
|
except KeyError:
|
||||||
|
# Benchmark parameters type is not declared in the regression definition file
|
||||||
|
# Ignoring
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Regression generation requires a profile definitions file (see: --regression-profiles input argument)"
|
||||||
|
)
|
||||||
|
sys.exit(5)
|
||||||
|
|
||||||
|
generate_json_regression_file(conn, operations_filter, user_config)
|
||||||
146
ci/data_extractor/src/utils.py
Normal file
146
ci/data_extractor/src/utils.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from benchmark_specs import Layer
|
||||||
|
|
||||||
|
SECONDS_IN_NANO = 1e9
|
||||||
|
MILLISECONDS_IN_NANO = 1e6
|
||||||
|
MICROSECONDS_IN_NANO = 1e3
|
||||||
|
|
||||||
|
|
||||||
|
def convert_value_to_readable_text(value: int, max_digits: int = 3) -> str:
|
||||||
|
"""
|
||||||
|
Convert timing in nanoseconds to the highest unit usable.
|
||||||
|
|
||||||
|
:param value: timing value
|
||||||
|
:type value: int
|
||||||
|
:param max_digits: number of digits to keep in the final representation of the value
|
||||||
|
:type max_digits: int, optional
|
||||||
|
|
||||||
|
:return: human-readable value with unit
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
if value > SECONDS_IN_NANO:
|
||||||
|
converted_parts = (value / SECONDS_IN_NANO), "s"
|
||||||
|
elif value > MILLISECONDS_IN_NANO:
|
||||||
|
converted_parts = (value / MILLISECONDS_IN_NANO), "ms"
|
||||||
|
elif value > MICROSECONDS_IN_NANO:
|
||||||
|
converted_parts = (value / MICROSECONDS_IN_NANO), "us"
|
||||||
|
else:
|
||||||
|
converted_parts = value, "ns"
|
||||||
|
|
||||||
|
power_of_10 = math.floor(math.log10(converted_parts[0]))
|
||||||
|
rounding_digit = max_digits - (power_of_10 + 1)
|
||||||
|
if converted_parts[0] >= 100.0:
|
||||||
|
rounding_digit = None
|
||||||
|
|
||||||
|
return f"{round(converted_parts[0], rounding_digit)} {converted_parts[1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def convert_gain_to_text(value: float) -> str:
|
||||||
|
"""
|
||||||
|
Convert gains as :class:`float` to :class:`str`
|
||||||
|
|
||||||
|
:param value: gain value
|
||||||
|
:type value: float
|
||||||
|
|
||||||
|
:return: gain as text with percentage sign
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return f"{value} %" if value < 0 else f"+{value} %"
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_csv(lines: list, output_filename: str):
|
||||||
|
"""
|
||||||
|
Write data to a CSV file.
|
||||||
|
|
||||||
|
:param lines: formatted data as iterable
|
||||||
|
:type lines: list
|
||||||
|
:param output_filename: filename where data would be written
|
||||||
|
:type output_filename: str
|
||||||
|
"""
|
||||||
|
with pathlib.Path(output_filename).open("w") as csv_file:
|
||||||
|
writer = csv.writer(csv_file, delimiter=",")
|
||||||
|
for line in lines:
|
||||||
|
writer.writerow(line)
|
||||||
|
|
||||||
|
print(f"Results written as CSV in '{output_filename}'")
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_markdown(lines: str | list[str], output_filename: str):
|
||||||
|
"""
|
||||||
|
Write data to a Markdown file.
|
||||||
|
|
||||||
|
:param lines: formatted lines
|
||||||
|
:type lines: str | list[str]
|
||||||
|
:param output_filename: filename where data would be written
|
||||||
|
:type output_filename: str
|
||||||
|
"""
|
||||||
|
if type(lines) != str:
|
||||||
|
content = "\n".join(lines) + "\n"
|
||||||
|
pathlib.Path(output_filename).write_text(content)
|
||||||
|
else:
|
||||||
|
pathlib.Path(output_filename).write_text(lines)
|
||||||
|
|
||||||
|
print(f"Results written as Markdown in '{output_filename}'")
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_svg(xml_string: str, output_filename: str):
|
||||||
|
"""
|
||||||
|
Write XML to a SVG file.
|
||||||
|
|
||||||
|
:param xml_string: XML formatted string
|
||||||
|
:type xml_string: str
|
||||||
|
:param output_filename: filename where data would be written
|
||||||
|
:type output_filename: str
|
||||||
|
"""
|
||||||
|
pathlib.Path(output_filename).write_text(xml_string)
|
||||||
|
|
||||||
|
print(f"Results written as SVG in '{output_filename}'")
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_json(data: dict, output_filename: str):
|
||||||
|
"""
|
||||||
|
Write data to a JSON file.
|
||||||
|
|
||||||
|
:param data: data that would be dumped as JSON
|
||||||
|
:type data: dict
|
||||||
|
:param output_filename: filename where data would be written
|
||||||
|
:type output_filename: str
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
dump = json.dumps(data)
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
print(f"couldn't convert results into json format (error: {err})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
pathlib.Path(output_filename).write_text(dump)
|
||||||
|
print(f"Results written as JSON in '{output_filename}'")
|
||||||
|
|
||||||
|
|
||||||
|
def append_suffix_to_filename(filename: str, suffix: str, stem: str) -> str:
|
||||||
|
"""
|
||||||
|
Appends a suffix to a given filename, considering a specific stem. If the filename
|
||||||
|
already ends with the given stem, the suffix is inserted before the stem. Otherwise,
|
||||||
|
the suffix and stem are added to the end of the filename.
|
||||||
|
|
||||||
|
:param filename: The original filename to which the suffix and stem will be appended
|
||||||
|
:type filename: str
|
||||||
|
:param suffix: The string to be appended as the suffix
|
||||||
|
:type suffix: str
|
||||||
|
:param stem: The specific stem to check or append after the suffix
|
||||||
|
:type stem: str
|
||||||
|
|
||||||
|
:return: The new filename with the appended suffix and stem
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
filename = filename[:] # Make a copy to avoid modifying the original filename
|
||||||
|
if filename.endswith(stem):
|
||||||
|
filename = filename[: -len(stem)] + suffix + stem
|
||||||
|
else:
|
||||||
|
filename += suffix + stem
|
||||||
|
|
||||||
|
return filename
|
||||||
@@ -13,13 +13,20 @@ One can also provide a fully custom profile via the issue comment string see: fu
|
|||||||
|
|
||||||
This script is also capable of checking for performance regression based on previous benchmarks results.
|
This script is also capable of checking for performance regression based on previous benchmarks results.
|
||||||
It works by providing a result file containing the baseline values and the results of the last run.
|
It works by providing a result file containing the baseline values and the results of the last run.
|
||||||
|
Alongside this mode, a performance report can be generated to help identify potential regressions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import enum
|
import enum
|
||||||
|
import json
|
||||||
|
import math
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import statistics
|
||||||
import sys
|
import sys
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from py_markdown_table.markdown_table import markdown_table
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -32,12 +39,28 @@ parser.add_argument(
|
|||||||
dest="issue_comment",
|
dest="issue_comment",
|
||||||
help="GitHub issue comment defining the regression benchmark profile to use",
|
help="GitHub issue comment defining the regression benchmark profile to use",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results-file",
|
||||||
|
dest="results_file",
|
||||||
|
help="Path to the results file containing the baseline and last run results",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--generate-report",
|
||||||
|
dest="generate_report",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Generate markdown report of the regression check",
|
||||||
|
)
|
||||||
|
|
||||||
COMMENT_IDENTIFIER = "/bench"
|
COMMENT_IDENTIFIER = "/bench"
|
||||||
|
|
||||||
|
SECONDS_IN_NANO = 1e9
|
||||||
|
MILLISECONDS_IN_NANO = 1e6
|
||||||
|
MICROSECONDS_IN_NANO = 1e3
|
||||||
|
|
||||||
CWD = pathlib.Path(__file__).parent
|
CWD = pathlib.Path(__file__).parent
|
||||||
REPO_ROOT = CWD.parent
|
REPO_ROOT = CWD.parent.parent
|
||||||
PROFILE_DEFINITION_PATH = CWD.joinpath("regression.toml")
|
PROFILE_DEFINITION_PATH = CWD.parent.joinpath("regression.toml")
|
||||||
BENCH_TARGETS_PATH = REPO_ROOT.joinpath("tfhe-benchmark/Cargo.toml")
|
BENCH_TARGETS_PATH = REPO_ROOT.joinpath("tfhe-benchmark/Cargo.toml")
|
||||||
# Files generated after parsing an issue comment
|
# Files generated after parsing an issue comment
|
||||||
GENERATED_COMMANDS_PATH = CWD.joinpath("perf_regression_generated_commands.json")
|
GENERATED_COMMANDS_PATH = CWD.joinpath("perf_regression_generated_commands.json")
|
||||||
@@ -305,6 +328,8 @@ class ProfileDefinition:
|
|||||||
case TfheBackend.Hpu:
|
case TfheBackend.Hpu:
|
||||||
features.extend(["hpu", "hpu-v80"])
|
features.extend(["hpu", "hpu-v80"])
|
||||||
|
|
||||||
|
features.append("pbs-stats")
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
def generate_cargo_commands(self):
|
def generate_cargo_commands(self):
|
||||||
@@ -415,12 +440,312 @@ def write_backend_config_to_file(backend, profile):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for filepart, content in [("backend", backend), ("profile", profile)]:
|
for filepart, content in [("backend", backend), ("profile", profile)]:
|
||||||
pathlib.Path(f"ci/perf_regression_slab_{filepart}_config.txt").write_text(
|
CWD.joinpath(f"perf_regression_slab_{filepart}_config.txt").write_text(
|
||||||
f"{content}\n"
|
f"{content}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO Perform regression computing by providing a file containing results from database that would be parsed
|
def write_regression_config_to_file(tfhe_rs_backend, regression_profile):
|
||||||
|
"""
|
||||||
|
Write tfhe-rs backend and regression configuration to different files to ease parsing.
|
||||||
|
|
||||||
|
:param backend:
|
||||||
|
:param profile:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
for filepart, content in [
|
||||||
|
("tfhe_rs_backend", tfhe_rs_backend),
|
||||||
|
("selected_profile", regression_profile),
|
||||||
|
]:
|
||||||
|
CWD.joinpath(f"perf_regression_{filepart}_config.txt").write_text(
|
||||||
|
f"{content}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Scale factor to improve the signal/noise ratio in anomaly detection.
|
||||||
|
MAJOR_CHANGE_SCALE_FACTOR = 4
|
||||||
|
MINOR_CHANGE_SCALE_FACTOR = 2
|
||||||
|
REGRESSION_REPORT_FILE = CWD.joinpath("regression_report.md")
|
||||||
|
|
||||||
|
|
||||||
|
class PerfChange(enum.StrEnum):
|
||||||
|
NoChange = "no changes"
|
||||||
|
MinorImprovement = "minor improvement"
|
||||||
|
MajorImprovement = "improvement"
|
||||||
|
MinorRegression = "minor regression"
|
||||||
|
MajorRegression = "regression"
|
||||||
|
|
||||||
|
def get_emoji(self):
|
||||||
|
match self:
|
||||||
|
case PerfChange.NoChange:
|
||||||
|
return ":heavy_minus_sign:"
|
||||||
|
case PerfChange.MinorImprovement:
|
||||||
|
return ":white_check_mark:"
|
||||||
|
case PerfChange.MajorImprovement:
|
||||||
|
return ":heavy_check_mark:"
|
||||||
|
case PerfChange.MinorRegression:
|
||||||
|
return ":warning:"
|
||||||
|
case PerfChange.MajorRegression:
|
||||||
|
return ":bangbang:"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OperationPerformance:
|
||||||
|
name: str
|
||||||
|
baseline_mean: float
|
||||||
|
baseline_stdev: float
|
||||||
|
head_branch_value: float
|
||||||
|
change_percentage: float
|
||||||
|
change_type: PerfChange = PerfChange.NoChange
|
||||||
|
|
||||||
|
def __init__(self, name: str, baseline_data: list[float], head_branch_value: float):
|
||||||
|
self.name = name
|
||||||
|
self.baseline_mean = round(statistics.mean(baseline_data), 2)
|
||||||
|
self.baseline_stdev = round(statistics.stdev(baseline_data), 2)
|
||||||
|
self.head_branch_value = head_branch_value
|
||||||
|
|
||||||
|
def compute_change(self):
|
||||||
|
self.change_percentage = round(
|
||||||
|
(self.head_branch_value - self.baseline_mean) / self.baseline_mean * 100, 2
|
||||||
|
)
|
||||||
|
|
||||||
|
major_threshold = MAJOR_CHANGE_SCALE_FACTOR * self.baseline_stdev
|
||||||
|
minor_threshold = MINOR_CHANGE_SCALE_FACTOR * self.baseline_stdev
|
||||||
|
|
||||||
|
if self.head_branch_value > self.baseline_mean + major_threshold:
|
||||||
|
self.change_type = PerfChange.MajorRegression
|
||||||
|
elif self.head_branch_value > self.baseline_mean + minor_threshold:
|
||||||
|
self.change_type = PerfChange.MinorRegression
|
||||||
|
elif (self.head_branch_value < self.baseline_mean + minor_threshold) and (
|
||||||
|
self.head_branch_value > self.baseline_mean - minor_threshold
|
||||||
|
):
|
||||||
|
# Between +/- MINOR_CHANGE_SCALE_FACTOR * Std_dev we consider there is no change
|
||||||
|
self.change_type = PerfChange.NoChange
|
||||||
|
elif self.head_branch_value < self.baseline_mean - major_threshold:
|
||||||
|
self.change_type = PerfChange.MajorImprovement
|
||||||
|
elif self.head_branch_value < self.baseline_mean - minor_threshold:
|
||||||
|
self.change_type = PerfChange.MinorImprovement
|
||||||
|
|
||||||
|
return self.change_percentage, self.change_type
|
||||||
|
|
||||||
|
def change_percentage_as_str(self):
|
||||||
|
if (
|
||||||
|
self.change_type == PerfChange.MajorImprovement
|
||||||
|
or self.change_type == PerfChange.MinorImprovement
|
||||||
|
or (
|
||||||
|
self.change_type == PerfChange.NoChange
|
||||||
|
and (self.head_branch_value < self.baseline_mean)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
sign = ""
|
||||||
|
else:
|
||||||
|
# Minus sign is already embedded in the float value.
|
||||||
|
sign = "+"
|
||||||
|
|
||||||
|
return f"{sign}{self.change_percentage}%"
|
||||||
|
|
||||||
|
|
||||||
|
def convert_value_to_readable_text(value_nanos: float, max_digits=3):
|
||||||
|
"""
|
||||||
|
Convert timing in nanoseconds to the highest unit usable.
|
||||||
|
|
||||||
|
:param value_nanos: timing value
|
||||||
|
:param max_digits: number of digits to keep in the final representation of the value
|
||||||
|
|
||||||
|
:return: human-readable value with unit as :class:`str`
|
||||||
|
"""
|
||||||
|
if value_nanos > SECONDS_IN_NANO:
|
||||||
|
converted_parts = (value_nanos / SECONDS_IN_NANO), "s"
|
||||||
|
elif value_nanos > MILLISECONDS_IN_NANO:
|
||||||
|
converted_parts = (value_nanos / MILLISECONDS_IN_NANO), "ms"
|
||||||
|
elif value_nanos > MICROSECONDS_IN_NANO:
|
||||||
|
converted_parts = (value_nanos / MICROSECONDS_IN_NANO), "us"
|
||||||
|
else:
|
||||||
|
converted_parts = value_nanos, "ns"
|
||||||
|
|
||||||
|
power_of_10 = math.floor(math.log10(converted_parts[0]))
|
||||||
|
rounding_digit = max_digits - (power_of_10 + 1)
|
||||||
|
if converted_parts[0] >= 100.0:
|
||||||
|
rounding_digit = None
|
||||||
|
|
||||||
|
return f"{round(converted_parts[0], rounding_digit)} {converted_parts[1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def check_performance_changes(results_file: pathlib.Path):
|
||||||
|
"""
|
||||||
|
Check if any operation has regressed compared to the base branch.
|
||||||
|
|
||||||
|
Results file must be in JSON format with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"backend": "<tfhe-rs_backend>",
|
||||||
|
"profile": "<regression_profile>",
|
||||||
|
"operation": [
|
||||||
|
{
|
||||||
|
"name": "<operation_name>",
|
||||||
|
"bit_size": <int>,
|
||||||
|
"params": "<parameters_alias>",
|
||||||
|
|
||||||
|
"results": {
|
||||||
|
"base": {
|
||||||
|
"name": "<base_branche_name>",
|
||||||
|
"data": [
|
||||||
|
<float>,
|
||||||
|
<float>,
|
||||||
|
...,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"head": {
|
||||||
|
"name": "<dev_branch_name>",
|
||||||
|
"value": <float>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:param results_file: path to the result file
|
||||||
|
:type results_file: pathlib.Path
|
||||||
|
|
||||||
|
:return: :class:`list` of :class:`OperationPerformance`
|
||||||
|
"""
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
results = json.loads(results_file.read_text())
|
||||||
|
for operation in results["operations"]:
|
||||||
|
op_name = operation["name"]
|
||||||
|
try:
|
||||||
|
baseline_data = operation["results"]["base"]["data"]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(
|
||||||
|
f"no base branch data found in results file for '{op_name}' operation"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
head_branch_value = operation["results"]["head"]["value"]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(
|
||||||
|
f"no head branch value found in results file for '{op_name}' operation"
|
||||||
|
)
|
||||||
|
|
||||||
|
op_perf = OperationPerformance(op_name, baseline_data, head_branch_value)
|
||||||
|
op_perf.compute_change()
|
||||||
|
changes.append(op_perf)
|
||||||
|
|
||||||
|
return changes, results["backend"], results["profile"]
|
||||||
|
|
||||||
|
|
||||||
|
OPERATION_HEADER = "Operation"
|
||||||
|
CURRENT_VALUE_HEADER = "Current (ms)"
|
||||||
|
BASELINE_VALUE_HEADER = "Baseline (ms)"
|
||||||
|
BASELINE_STDDEV_HEADER = "Baseline Stddev (ms)"
|
||||||
|
CHANGE_HEADER = "Change (%)"
|
||||||
|
STATUS_HEADER = "Status"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_regression_report(
|
||||||
|
ops_performances: list[OperationPerformance], backend: str, profile: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate a regression report in Markdown format and write it to a specified file.
|
||||||
|
|
||||||
|
This function analyzes performance data for various operations and generates a Markdown
|
||||||
|
formatted report summarizing the performance. It highlights any major regressions by
|
||||||
|
providing detailed data about the affected operations and overall benchmark results.
|
||||||
|
Additionally, it includes configuration details, such as the backend and regression profile
|
||||||
|
used in the analysis.
|
||||||
|
|
||||||
|
:param ops_performances: A list of performance data for operations to be analyzed
|
||||||
|
and summarized.
|
||||||
|
:type ops_performances: list[OperationPerformance]
|
||||||
|
:param backend: The backend being used in the performance analysis.
|
||||||
|
:type backend: str
|
||||||
|
:param profile: The profile identifying the regression analysis configuration.
|
||||||
|
:type profile: str
|
||||||
|
"""
|
||||||
|
full_data = [
|
||||||
|
{
|
||||||
|
OPERATION_HEADER: op.name,
|
||||||
|
CURRENT_VALUE_HEADER: convert_value_to_readable_text(op.head_branch_value),
|
||||||
|
BASELINE_VALUE_HEADER: convert_value_to_readable_text(op.baseline_mean),
|
||||||
|
BASELINE_STDDEV_HEADER: convert_value_to_readable_text(op.baseline_stdev),
|
||||||
|
CHANGE_HEADER: op.change_percentage_as_str(),
|
||||||
|
STATUS_HEADER: " ".join((op.change_type.get_emoji(), op.change_type.value)),
|
||||||
|
}
|
||||||
|
for op in ops_performances
|
||||||
|
]
|
||||||
|
full_array_markdown = (
|
||||||
|
markdown_table(full_data)
|
||||||
|
.set_params(row_sep="markdown", quote=False)
|
||||||
|
.get_markdown()
|
||||||
|
)
|
||||||
|
|
||||||
|
regression_data = []
|
||||||
|
for op in ops_performances:
|
||||||
|
if op.change_type != PerfChange.MajorRegression:
|
||||||
|
continue
|
||||||
|
|
||||||
|
regression_data.append(
|
||||||
|
{
|
||||||
|
OPERATION_HEADER: op.name,
|
||||||
|
CURRENT_VALUE_HEADER: convert_value_to_readable_text(
|
||||||
|
op.head_branch_value
|
||||||
|
),
|
||||||
|
BASELINE_VALUE_HEADER: convert_value_to_readable_text(op.baseline_mean),
|
||||||
|
CHANGE_HEADER: op.change_percentage_as_str(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
comment_body = []
|
||||||
|
|
||||||
|
if regression_data:
|
||||||
|
regression_array_markdown = (
|
||||||
|
markdown_table(regression_data)
|
||||||
|
.set_params(row_sep="markdown", quote=False)
|
||||||
|
.get_markdown()
|
||||||
|
)
|
||||||
|
|
||||||
|
regression_details = [
|
||||||
|
"> [!CAUTION]",
|
||||||
|
"> Performances for some operations have regressed compared to the base branch.",
|
||||||
|
"", # Add a newline since tables cannot be rendered in markdown note.
|
||||||
|
regression_array_markdown,
|
||||||
|
"", # Add a newline to avoid rendering the next line of text into the array.
|
||||||
|
]
|
||||||
|
comment_body.extend(regression_details)
|
||||||
|
else:
|
||||||
|
comment_body.append("No performance regression detected. :tada:")
|
||||||
|
|
||||||
|
comment_body.append(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"Configuration",
|
||||||
|
f"* backend: `{backend}`",
|
||||||
|
f"* regression-profile: `{profile}`",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
all_results_details = [
|
||||||
|
"<details>",
|
||||||
|
"<summary><strong>View All Benchmarks</strong></summary>",
|
||||||
|
"",
|
||||||
|
full_array_markdown,
|
||||||
|
"</details>",
|
||||||
|
]
|
||||||
|
comment_body.extend(all_results_details)
|
||||||
|
|
||||||
|
formatted_text = "\n".join(comment_body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
REGRESSION_REPORT_FILE.write_text(formatted_text)
|
||||||
|
except Exception as err:
|
||||||
|
print(f"failed to write regression report (error: {err})")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -449,8 +774,27 @@ if __name__ == "__main__":
|
|||||||
write_backend_config_to_file(
|
write_backend_config_to_file(
|
||||||
definition.slab_backend, definition.slab_profile
|
definition.slab_backend, definition.slab_profile
|
||||||
)
|
)
|
||||||
|
write_regression_config_to_file(
|
||||||
|
definition.backend, definition.regression_profile
|
||||||
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(f"failed to write commands/env to file (error:{err})")
|
print(f"failed to write commands/env to file (error:{err})")
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
elif args.command == "check_regression":
|
elif args.command == "check_regression":
|
||||||
pass
|
results_file = args.results_file
|
||||||
|
if not results_file:
|
||||||
|
print(
|
||||||
|
f"cannot run `{args.command}` command: please specify the results file path with `--results-file` argument"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results_file_path = pathlib.Path(results_file)
|
||||||
|
perf_changes, backend, profile = check_performance_changes(results_file_path)
|
||||||
|
|
||||||
|
if args.generate_report:
|
||||||
|
try:
|
||||||
|
generate_regression_report(perf_changes, backend, profile)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(4)
|
||||||
|
|
||||||
|
# TODO Add unittests primarly to check if commands and env generated are correct.
|
||||||
1
ci/perf_regression/requirements.txt
Normal file
1
ci/perf_regression/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
py-markdown-table==1.3.0
|
||||||
@@ -42,20 +42,25 @@
|
|||||||
# See ci/slab.toml file to have the list of all supported profiles.
|
# See ci/slab.toml file to have the list of all supported profiles.
|
||||||
|
|
||||||
[gpu.default]
|
[gpu.default]
|
||||||
target.integer-bench = ["mul", "div"]
|
target.integer = ["mul"]
|
||||||
target.hlapi-dex = ["dex_swap"]
|
target.hlapi-dex = ["dex::swap_claim::no_cmux"]
|
||||||
slab.backend = "hyperstack"
|
slab.backend = "hyperstack"
|
||||||
slab.profile = "single-h100"
|
slab.profile = "single-h100"
|
||||||
env.fast_bench = "TRUE"
|
env.fast_bench = "TRUE"
|
||||||
|
env.bench_param_type = "MULTI_BIT"
|
||||||
|
|
||||||
[gpu.multi-h100]
|
[gpu.multi-h100]
|
||||||
target.integer-bench = ["mul", "div"]
|
target.integer = ["mul", "div"]
|
||||||
target.hlapi-dex = ["dex_swap"]
|
target.hlapi-dex = ["dex_swap"]
|
||||||
slab.backend = "hyperstack"
|
slab.backend = "hyperstack"
|
||||||
slab.profile = "multi-h100"
|
slab.profile = "multi-h100"
|
||||||
|
env.fast_bench = "TRUE"
|
||||||
|
env.bench_param_type = "MULTI_BIT"
|
||||||
|
|
||||||
[cpu.default]
|
[cpu.default]
|
||||||
target.integer-bench = ["add_parallelized", "mul_parallelized", "div_parallelized"]
|
target.integer = ["add_parallelized"]
|
||||||
|
target.hlapi-dex = ["dex::swap_claim::no_cmux"]
|
||||||
|
target.hlapi-erc20 = ["transfer::whitepaper"]
|
||||||
slab.backend = "aws"
|
slab.backend = "aws"
|
||||||
slab.profile = "bench"
|
slab.profile = "bench"
|
||||||
env.fast_bench = "TRUE"
|
env.fast_bench = "TRUE"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ user = "ubuntu"
|
|||||||
|
|
||||||
[backend.aws.bench]
|
[backend.aws.bench]
|
||||||
region = "eu-west-1"
|
region = "eu-west-1"
|
||||||
image_id = "ami-0b8fd02b263ebdcf4"
|
image_id = "ami-06e3a9d9b57d38676"
|
||||||
instance_type = "hpc7a.96xlarge"
|
instance_type = "hpc7a.96xlarge"
|
||||||
user = "ubuntu"
|
user = "ubuntu"
|
||||||
|
|
||||||
|
|||||||
@@ -43,25 +43,25 @@ pbs-stats = ["tfhe/pbs-stats"]
|
|||||||
zk-pok = ["tfhe/zk-pok"]
|
zk-pok = ["tfhe/zk-pok"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "boolean-bench"
|
name = "boolean"
|
||||||
path = "benches/boolean/bench.rs"
|
path = "benches/boolean/bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["boolean", "internal-keycache"]
|
required-features = ["boolean", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "shortint-bench"
|
name = "shortint"
|
||||||
path = "benches/shortint/bench.rs"
|
path = "benches/shortint/bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "oprf-shortint-bench"
|
name = "shortint-oprf"
|
||||||
path = "benches/shortint/oprf.rs"
|
path = "benches/shortint/oprf.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "glwe_packing_compression-shortint-bench"
|
name = "shortint-glwe_packing_compression"
|
||||||
path = "benches/shortint/glwe_packing_compression.rs"
|
path = "benches/shortint/glwe_packing_compression.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
@@ -91,55 +91,55 @@ harness = false
|
|||||||
required-features = ["integer", "internal-keycache"]
|
required-features = ["integer", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "glwe_packing_compression-integer-bench"
|
name = "integer-glwe_packing_compression"
|
||||||
path = "benches/integer/glwe_packing_compression.rs"
|
path = "benches/integer/glwe_packing_compression.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "integer-bench"
|
name = "integer"
|
||||||
path = "benches/integer/bench.rs"
|
path = "benches/integer/bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "integer-signed-bench"
|
name = "integer-signed"
|
||||||
path = "benches/integer/signed_bench.rs"
|
path = "benches/integer/signed_bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
required-features = ["integer", "pbs-stats", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "zk-pke-bench"
|
name = "integer-zk-pke"
|
||||||
path = "benches/integer/zk_pke.rs"
|
path = "benches/integer/zk_pke.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["integer", "zk-pok", "pbs-stats", "internal-keycache"]
|
required-features = ["integer", "zk-pok", "pbs-stats", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "ks-bench"
|
name = "core_crypto-ks"
|
||||||
path = "benches/core_crypto/ks_bench.rs"
|
path = "benches/core_crypto/ks_bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "pbs-bench"
|
name = "core_crypto-pbs"
|
||||||
path = "benches/core_crypto/pbs_bench.rs"
|
path = "benches/core_crypto/pbs_bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["boolean", "shortint", "internal-keycache"]
|
required-features = ["boolean", "shortint", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "ks-pbs-bench"
|
name = "core_crypto-ks-pbs"
|
||||||
path = "benches/core_crypto/ks_pbs_bench.rs"
|
path = "benches/core_crypto/ks_pbs_bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "modulus_switch_noise_reduction"
|
name = "core_crypto-modulus_switch_noise_reduction"
|
||||||
path = "benches/core_crypto/modulus_switch_noise_reduction.rs"
|
path = "benches/core_crypto/modulus_switch_noise_reduction.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint"]
|
required-features = ["shortint"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "pbs128-bench"
|
name = "core_crypto-pbs128"
|
||||||
path = "benches/core_crypto/pbs128_bench.rs"
|
path = "benches/core_crypto/pbs128_bench.rs"
|
||||||
harness = false
|
harness = false
|
||||||
required-features = ["shortint", "internal-keycache"]
|
required-features = ["shortint", "internal-keycache"]
|
||||||
|
|||||||
Reference in New Issue
Block a user