From 9ee825900208a490cfa06eeb29d422dcd077605e Mon Sep 17 00:00:00 2001 From: Baptiste Roux Date: Fri, 16 May 2025 14:15:38 +0200 Subject: [PATCH] feat(hpu): Add Hpu backend implementation This backend abstract communication with Hpu Fpga hardware. It define it's proper entities to prevent circular dependencies with tfhe-rs. Object lifetime is handle through Arc> wrapper, and enforce that all objects currently alive in Hpu Hw are also kept valid on the host side. It contains the second version of HPU instruction set (HIS_V2.0): * DOp have following properties: + Template as first class citizen + Support of Immediate template + Direct parser and conversion between Asm/Hex + Replace deku (and it's associated endianess limitation) by + bitfield_struct and manual parsing * IOp have following properties: + Support various number of Destination + Support various number of Sources + Support various number of Immediat values + Support of multiple bitwidth (Not implemented yet in the Fpga firmware) Details could be view in `backends/tfhe-hpu-backend/Readme.md` --- .github/actionlint.yaml | 1 + .github/workflows/benchmark_hpu_integer.yml | 88 + .github/workflows/cargo_build.yml | 5 + .github/workflows/cargo_test_fft.yml | 4 +- .github/workflows/cargo_test_ntt.yml | 4 +- .github/workflows/hpu_hlapi_tests.yml | 73 + .github/workflows/make_release_hpu.yml | 105 + .lfsconfig | 2 + Cargo.toml | 2 + Makefile | 79 + _typos.toml | 4 +- backends/tfhe-hpu-backend/.gitattributes | 3 + backends/tfhe-hpu-backend/.gitignore | 3 + backends/tfhe-hpu-backend/Cargo.toml | 88 + backends/tfhe-hpu-backend/LICENSE | 28 + backends/tfhe-hpu-backend/Readme.md | 261 + backends/tfhe-hpu-backend/build.rs | 26 + .../config_store/sim/custom_iop/cust_0.asm | 15 + .../config_store/sim/custom_iop/cust_1.asm | 11 + .../config_store/sim/custom_iop/cust_10.asm | 25 + .../config_store/sim/custom_iop/cust_16.asm | 6 + .../config_store/sim/custom_iop/cust_17.asm | 15 + .../config_store/sim/custom_iop/cust_18.asm | 23 + .../config_store/sim/custom_iop/cust_19.asm | 19 + .../config_store/sim/custom_iop/cust_2.asm | 11 + .../config_store/sim/custom_iop/cust_20.asm | 22 + .../config_store/sim/custom_iop/cust_21.asm | 24 + .../config_store/sim/custom_iop/cust_3.asm | 16 + .../config_store/sim/custom_iop/cust_8.asm | 19 + .../config_store/sim/custom_iop/cust_9.asm | 21 + .../config_store/sim/hpu_config.toml | 108 + .../sim/hpu_regif_core_cfg_1in3.toml | 256 + .../sim/hpu_regif_core_cfg_3in3.toml | 51 + .../sim/hpu_regif_core_prc_1in3.toml | 336 + .../sim/hpu_regif_core_prc_3in3.toml | 100 + .../config_store/sim/tb_hpu_regif_dummy.toml | 22 + .../config_store/u55c_gf64/Readme.md | 6 + .../u55c_gf64/custom_iop/cust_0.asm | 15 + .../u55c_gf64/custom_iop/cust_1.asm | 11 + .../u55c_gf64/custom_iop/cust_10.asm | 25 + .../u55c_gf64/custom_iop/cust_16.asm | 6 + .../u55c_gf64/custom_iop/cust_17.asm | 15 + .../u55c_gf64/custom_iop/cust_18.asm | 23 + .../u55c_gf64/custom_iop/cust_19.asm | 19 + .../u55c_gf64/custom_iop/cust_2.asm | 11 + .../u55c_gf64/custom_iop/cust_20.asm | 22 + .../u55c_gf64/custom_iop/cust_21.asm | 24 + .../u55c_gf64/custom_iop/cust_3.asm | 16 + .../u55c_gf64/custom_iop/cust_8.asm | 19 + .../u55c_gf64/custom_iop/cust_9.asm | 21 + .../config_store/u55c_gf64/hpu_config.toml | 98 + .../u55c_gf64/hpu_msplit_3parts.xclbin | 3 + .../u55c_gf64/hpu_msplit_3parts.xclbin.info | 1550 ++++ .../hpu_msplit_3parts.xclbin.link_summary | 1377 +++ .../u55c_gf64/hpu_regif_core.toml | 622 ++ .../config_store/v80/Readme.md | 74 + .../config_store/v80/custom_iop/cust_0.asm | 15 + .../config_store/v80/custom_iop/cust_1.asm | 11 + .../config_store/v80/custom_iop/cust_10.asm | 25 + .../config_store/v80/custom_iop/cust_16.asm | 6 + .../config_store/v80/custom_iop/cust_17.asm | 15 + .../config_store/v80/custom_iop/cust_18.asm | 23 + .../config_store/v80/custom_iop/cust_19.asm | 19 + .../config_store/v80/custom_iop/cust_2.asm | 11 + .../config_store/v80/custom_iop/cust_20.asm | 22 + .../config_store/v80/custom_iop/cust_21.asm | 24 + .../config_store/v80/custom_iop/cust_3.asm | 16 + .../config_store/v80/custom_iop/cust_4.asm | 264 + .../config_store/v80/custom_iop/cust_8.asm | 19 + .../config_store/v80/custom_iop/cust_9.asm | 21 + .../config_store/v80/hpu_config.toml | 112 + .../v80/hpu_regif_core_cfg_1in3.toml | 256 + .../v80/hpu_regif_core_cfg_3in3.toml | 51 + .../v80/hpu_regif_core_prc_1in3.toml | 336 + .../v80/hpu_regif_core_prc_3in3.toml | 100 + .../figures/tfhe-hpu-backend.excalidraw.png | Bin 0 -> 275359 bytes backends/tfhe-hpu-backend/python/README | 12 + backends/tfhe-hpu-backend/python/bin/demo.py | 28 + .../tfhe-hpu-backend/python/data/trace.json | 7628 +++++++++++++++++ .../tfhe-hpu-backend/python/lib/example.json | 3 + .../python/lib/isctrace/__init__.py | 4 + .../python/lib/isctrace/analysis.py | 300 + .../python/lib/isctrace/fmt.py | 110 + .../python/lib/isctrace/hw.py | 83 + .../python/lib/isctrace/mockup.py | 93 + .../tfhe-hpu-backend/python/requirements.txt | 3 + backends/tfhe-hpu-backend/src/asm/dop/arg.rs | 519 ++ .../tfhe-hpu-backend/src/asm/dop/dop_macro.rs | 388 + .../tfhe-hpu-backend/src/asm/dop/field.rs | 168 + backends/tfhe-hpu-backend/src/asm/dop/fmt.rs | 245 + backends/tfhe-hpu-backend/src/asm/dop/mod.rs | 531 ++ .../tfhe-hpu-backend/src/asm/dop/opcode.rs | 166 + .../tfhe-hpu-backend/src/asm/dop/pbs_macro.rs | 152 + backends/tfhe-hpu-backend/src/asm/iop/arg.rs | 545 ++ .../tfhe-hpu-backend/src/asm/iop/field.rs | 540 ++ backends/tfhe-hpu-backend/src/asm/iop/fmt.rs | 154 + .../tfhe-hpu-backend/src/asm/iop/iop_macro.rs | 61 + backends/tfhe-hpu-backend/src/asm/iop/mod.rs | 184 + .../tfhe-hpu-backend/src/asm/iop/opcode.rs | 61 + backends/tfhe-hpu-backend/src/asm/mod.rs | 334 + .../tfhe-hpu-backend/src/asm/tests/dop.asm | 31 + .../tfhe-hpu-backend/src/asm/tests/iop.asm | 31 + .../tfhe-hpu-backend/src/asm/tests/mod.rs | 49 + .../src/entities/glwe_ciphertext.rs | 108 + .../src/entities/glwe_lookuptable.rs | 104 + .../src/entities/lwe_bootstrap_key.rs | 161 + .../src/entities/lwe_ciphertext.rs | 164 + .../src/entities/lwe_keyswitch_key.rs | 149 + backends/tfhe-hpu-backend/src/entities/mod.rs | 37 + .../src/entities/parameters.rs | 197 + .../src/entities/traits/container.rs | 104 + .../src/entities/traits/mod.rs | 1 + backends/tfhe-hpu-backend/src/ffi/mod.rs | 328 + backends/tfhe-hpu-backend/src/ffi/sim/ipc.rs | 215 + backends/tfhe-hpu-backend/src/ffi/sim/mod.rs | 266 + backends/tfhe-hpu-backend/src/ffi/v80/ami.rs | 328 + .../tfhe-hpu-backend/src/ffi/v80/mem_alloc.rs | 136 + backends/tfhe-hpu-backend/src/ffi/v80/mod.rs | 115 + backends/tfhe-hpu-backend/src/ffi/v80/qdma.rs | 106 + .../src/ffi/xrt/cxx/hpu_hw.cc | 104 + .../tfhe-hpu-backend/src/ffi/xrt/cxx/hpu_hw.h | 80 + .../src/ffi/xrt/cxx/mem_zone.cc | 59 + .../src/ffi/xrt/cxx/mem_zone.h | 50 + .../tfhe-hpu-backend/src/ffi/xrt/cxx/mod.rs | 79 + backends/tfhe-hpu-backend/src/ffi/xrt/mod.rs | 96 + .../tfhe-hpu-backend/src/fw/fw_impl/demo.rs | 135 + .../tfhe-hpu-backend/src/fw/fw_impl/ilp.rs | 874 ++ .../src/fw/fw_impl/llt/kogge.rs | 497 ++ .../src/fw/fw_impl/llt/mod.rs | 500 ++ .../src/fw/fw_impl/llt/vardeg.rs | 160 + .../tfhe-hpu-backend/src/fw/fw_impl/mod.rs | 45 + .../tfhe-hpu-backend/src/fw/isc_sim/mod.rs | 160 + .../tfhe-hpu-backend/src/fw/isc_sim/pe.rs | 572 ++ .../tfhe-hpu-backend/src/fw/isc_sim/pool.rs | 531 ++ .../tfhe-hpu-backend/src/fw/isc_sim/report.rs | 94 + .../src/fw/isc_sim/scheduler.rs | 420 + backends/tfhe-hpu-backend/src/fw/metavar.rs | 1400 +++ backends/tfhe-hpu-backend/src/fw/mod.rs | 102 + backends/tfhe-hpu-backend/src/fw/program.rs | 538 ++ .../tfhe-hpu-backend/src/fw/rtl/config.rs | 89 + .../tfhe-hpu-backend/src/fw/rtl/macros.rs | 82 + backends/tfhe-hpu-backend/src/fw/rtl/mod.rs | 1837 ++++ .../tfhe-hpu-backend/src/interface/backend.rs | 932 ++ .../tfhe-hpu-backend/src/interface/cmd.rs | 153 + .../tfhe-hpu-backend/src/interface/config.rs | 163 + .../tfhe-hpu-backend/src/interface/device.rs | 237 + .../tfhe-hpu-backend/src/interface/io_dump.rs | 188 + .../src/interface/memory/ciphertext.rs | 250 + .../src/interface/memory/huge.rs | 188 + .../src/interface/memory/mod.rs | 12 + .../tfhe-hpu-backend/src/interface/mod.rs | 40 + .../tfhe-hpu-backend/src/interface/rtl/mod.rs | 9 + .../src/interface/rtl/params.rs | 462 + .../src/interface/rtl/runtime.rs | 934 ++ .../src/interface/variable.rs | 280 + .../tfhe-hpu-backend/src/isc_trace/fmt.rs | 165 + .../src/isc_trace/fmt/test/data.rs | 205 + .../src/isc_trace/fmt/test/mod.rs | 56 + .../tfhe-hpu-backend/src/isc_trace/mod.rs | 63 + .../src/isc_trace/packed_struct.rs | 149 + backends/tfhe-hpu-backend/src/lib.rs | 18 + backends/tfhe-hpu-backend/src/prelude.rs | 27 + .../tfhe-hpu-backend/src/utils/dop_fmt.rs | 68 + backends/tfhe-hpu-backend/src/utils/fw.rs | 201 + backends/tfhe-hpu-backend/src/utils/hputil.rs | 290 + .../tfhe-hpu-backend/src/utils/iop_fmt.rs | 67 + mockups/tfhe-hpu-mockup/Cargo.toml | 36 + mockups/tfhe-hpu-mockup/Justfile | 50 + mockups/tfhe-hpu-mockup/LICENSE | 28 + mockups/tfhe-hpu-mockup/Readme.md | 170 + .../figures/tfhe-hpu-mockup.excalidraw.png | Bin 0 -> 265988 bytes .../tfhe-hpu-mockup/params/gaussian_44b.toml | 44 + .../params/gaussian_44b_fast.toml | 44 + .../tfhe-hpu-mockup/params/gaussian_64b.toml | 47 + .../params/gaussian_64b_fast.toml | 44 + .../params/gaussian_64b_pfail64.toml | 47 + .../params/gaussian_64b_pfail64_psi64.toml | 48 + .../params/tuniform_64b_fast.toml | 44 + .../params/tuniform_64b_pfail64_psi64.toml | 48 + mockups/tfhe-hpu-mockup/src/ipc.rs | 102 + mockups/tfhe-hpu-mockup/src/lib.rs | 906 ++ mockups/tfhe-hpu-mockup/src/mockup.rs | 174 + .../tfhe-hpu-mockup/src/modules/memory/ddr.rs | 65 + .../tfhe-hpu-mockup/src/modules/memory/hbm.rs | 109 + .../tfhe-hpu-mockup/src/modules/memory/mod.rs | 56 + mockups/tfhe-hpu-mockup/src/modules/mod.rs | 15 + mockups/tfhe-hpu-mockup/src/modules/params.rs | 57 + mockups/tfhe-hpu-mockup/src/modules/regmap.rs | 652 ++ mockups/tfhe-hpu-mockup/src/modules/ucore.rs | 137 + setup_hpu.sh | 146 + tasks/src/check_tfhe_docs_are_tested.rs | 3 +- tfhe-benchmark/Cargo.toml | 2 + .../benches/core_crypto/pbs_bench.rs | 6 +- .../benches/high_level_api/bench.rs | 173 +- .../benches/high_level_api/erc20.rs | 236 +- tfhe-benchmark/benches/integer/bench.rs | 338 +- tfhe-benchmark/src/params.rs | 62 +- tfhe-benchmark/src/params_aliases.rs | 11 + tfhe-benchmark/src/utilities.rs | 13 +- tfhe/Cargo.toml | 34 + tfhe/docs/README.md | 2 +- tfhe/docs/SUMMARY.md | 4 + ...rk_fheuint64_tuniform_2m64_ciphertext.svg} | 0 ...nchmark_hpux1_tuniform_2m64_ciphertext.svg | 67 + ...enchmark_hpux1_tuniform_2m64_plaintext.svg | 39 + .../hpu_acceleration/benchmark.md | 3 + .../hpu_acceleration/run_on_hpu.md | 150 + .../docs/getting_started/benchmarks/README.md | 4 +- .../benchmarks/cpu/cpu_integer_operations.md | 2 +- .../getting_started/benchmarks/hpu/README.md | 11 + .../benchmarks/hpu/hpu_integer_operations.md | 30 + tfhe/examples/dark_market/main.rs | 4 +- tfhe/examples/hpu/bench.rs | 315 + tfhe/examples/hpu/hlapi.rs | 197 + tfhe/examples/hpu/matmul.rs | 157 + tfhe/examples/sha256.rs | 5 + .../lwe_bootstrap_key_conversion.rs | 14 +- .../ntt64_bnf_pbs.rs | 36 +- .../ntt64_pbs.rs | 18 +- .../test/lwe_programmable_bootstrapping.rs | 12 +- .../test/noise_distribution/lwe_hpu_noise.rs | 637 ++ .../algorithms/test/noise_distribution/mod.rs | 2 + .../variance_formula/lwe_keyswitch.rs | 93 + .../lwe_programmable_bootstrap.rs | 87 + .../variance_formula/mod.rs | 3 + .../variance_formula/secure_noise.rs | 70 + .../commons/math/decomposition/decomposer.rs | 42 + .../src/core_crypto/commons/math/ntt/ntt64.rs | 1 + .../core_crypto/commons/traits/create_from.rs | 2 +- .../entities/ntt_lwe_bootstrap_key.rs | 11 + tfhe/src/core_crypto/hpu/algorithms/mod.rs | 2 + .../core_crypto/hpu/algorithms/modswitch.rs | 37 + tfhe/src/core_crypto/hpu/algorithms/order.rs | 118 + .../hpu/entities/glwe_ciphertext.rs | 32 + .../hpu/entities/glwe_lookuptable.rs | 134 + .../hpu/entities/lwe_bootstrap_key.rs | 384 + .../hpu/entities/lwe_ciphertext.rs | 86 + .../hpu/entities/lwe_keyswitch_key.rs | 262 + tfhe/src/core_crypto/hpu/entities/mod.rs | 10 + tfhe/src/core_crypto/hpu/mod.rs | 3 + tfhe/src/core_crypto/mod.rs | 4 + .../high_level_api/array/dynamic/booleans.rs | 12 + .../high_level_api/array/dynamic/signed.rs | 12 + .../high_level_api/array/dynamic/unsigned.rs | 12 + tfhe/src/high_level_api/array/mod.rs | 12 + tfhe/src/high_level_api/booleans/base.rs | 127 + tfhe/src/high_level_api/booleans/encrypt.rs | 4 + tfhe/src/high_level_api/booleans/inner.rs | 200 +- tfhe/src/high_level_api/booleans/oprf.rs | 6 +- .../high_level_api/booleans/squashed_noise.rs | 8 +- tfhe/src/high_level_api/compact_list.rs | 6 +- .../compressed_ciphertext_list.rs | 67 +- tfhe/src/high_level_api/config.rs | 7 + tfhe/src/high_level_api/global_state.rs | 61 +- tfhe/src/high_level_api/integers/oprf.rs | 17 + .../high_level_api/integers/signed/base.rs | 64 + .../high_level_api/integers/signed/encrypt.rs | 2 + .../high_level_api/integers/signed/inner.rs | 11 +- .../src/high_level_api/integers/signed/ops.rs | 144 + .../integers/signed/overflowing_ops.rs | 20 + .../integers/signed/scalar_ops.rs | 140 +- .../integers/signed/squashed_noise.rs | 8 +- .../high_level_api/integers/unsigned/base.rs | 133 +- .../integers/unsigned/encrypt.rs | 4 + .../high_level_api/integers/unsigned/inner.rs | 226 +- .../high_level_api/integers/unsigned/ops.rs | 301 + .../integers/unsigned/overflowing_ops.rs | 20 + .../integers/unsigned/scalar_ops.rs | 162 +- .../integers/unsigned/squashed_noise.rs | 8 +- .../integers/unsigned/tests/cpu.rs | 48 + .../integers/unsigned/tests/gpu.rs | 12 + .../integers/unsigned/tests/hpu.rs | 84 + .../integers/unsigned/tests/mod.rs | 378 +- tfhe/src/high_level_api/keys/mod.rs | 2 + tfhe/src/high_level_api/keys/server.rs | 45 +- tfhe/src/high_level_api/mod.rs | 2 + tfhe/src/high_level_api/prelude.rs | 8 +- tfhe/src/high_level_api/strings/ascii/comp.rs | 56 + .../high_level_api/strings/ascii/contains.rs | 24 + tfhe/src/high_level_api/strings/ascii/find.rs | 16 + tfhe/src/high_level_api/strings/ascii/mod.rs | 2 + .../strings/ascii/no_pattern.rs | 30 +- .../high_level_api/strings/ascii/replace.rs | 25 + .../src/high_level_api/strings/ascii/strip.rs | 16 + tfhe/src/high_level_api/strings/ascii/trim.rs | 12 + .../high_level_api/tests/tags_on_entities.rs | 4 + tfhe/src/high_level_api/traits.rs | 24 + tfhe/src/integer/bigint/static_unsigned.rs | 12 + tfhe/src/integer/hpu/ciphertext/mod.rs | 240 + tfhe/src/integer/hpu/mod.rs | 78 + tfhe/src/integer/keycache.rs | 58 +- tfhe/src/integer/mod.rs | 3 + tfhe/src/lib.rs | 4 + tfhe/src/shortint/engine/mod.rs | 5 +- tfhe/src/shortint/keycache.rs | 6 + tfhe/src/shortint/parameters/hpu.rs | 60 + tfhe/src/shortint/parameters/mod.rs | 2 + tfhe/src/shortint/parameters/v1_2/hpu.rs | 52 + tfhe/src/shortint/parameters/v1_2/mod.rs | 6 + tfhe/src/test_user_docs.rs | 14 + tfhe/tests/hpu.rs | 632 ++ 301 files changed, 46112 insertions(+), 461 deletions(-) create mode 100644 .github/workflows/benchmark_hpu_integer.yml create mode 100644 .github/workflows/hpu_hlapi_tests.yml create mode 100644 .github/workflows/make_release_hpu.yml create mode 100644 .lfsconfig create mode 100644 backends/tfhe-hpu-backend/.gitattributes create mode 100644 backends/tfhe-hpu-backend/.gitignore create mode 100644 backends/tfhe-hpu-backend/Cargo.toml create mode 100644 backends/tfhe-hpu-backend/LICENSE create mode 100644 backends/tfhe-hpu-backend/Readme.md create mode 100644 backends/tfhe-hpu-backend/build.rs create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_0.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_1.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_10.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_16.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_17.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_18.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_19.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_2.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_20.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_21.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_3.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_8.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_9.asm create mode 100644 backends/tfhe-hpu-backend/config_store/sim/hpu_config.toml create mode 100644 backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_1in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_3in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_1in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_3in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/sim/tb_hpu_regif_dummy.toml create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/Readme.md create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_0.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_1.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_10.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_16.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_17.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_18.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_19.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_2.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_20.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_21.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_3.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_8.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_9.asm create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_config.toml create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.info create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.link_summary create mode 100644 backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_regif_core.toml create mode 100644 backends/tfhe-hpu-backend/config_store/v80/Readme.md create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_0.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_1.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_10.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_16.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_17.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_18.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_19.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_2.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_20.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_21.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_3.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_4.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_8.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_9.asm create mode 100644 backends/tfhe-hpu-backend/config_store/v80/hpu_config.toml create mode 100644 backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_1in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_3in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_1in3.toml create mode 100644 backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_3in3.toml create mode 100644 backends/tfhe-hpu-backend/figures/tfhe-hpu-backend.excalidraw.png create mode 100644 backends/tfhe-hpu-backend/python/README create mode 100755 backends/tfhe-hpu-backend/python/bin/demo.py create mode 100644 backends/tfhe-hpu-backend/python/data/trace.json create mode 100644 backends/tfhe-hpu-backend/python/lib/example.json create mode 100644 backends/tfhe-hpu-backend/python/lib/isctrace/__init__.py create mode 100644 backends/tfhe-hpu-backend/python/lib/isctrace/analysis.py create mode 100644 backends/tfhe-hpu-backend/python/lib/isctrace/fmt.py create mode 100644 backends/tfhe-hpu-backend/python/lib/isctrace/hw.py create mode 100644 backends/tfhe-hpu-backend/python/lib/isctrace/mockup.py create mode 100644 backends/tfhe-hpu-backend/python/requirements.txt create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/arg.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/dop_macro.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/field.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/fmt.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/opcode.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/dop/pbs_macro.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/arg.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/field.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/fmt.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/iop_macro.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/iop/opcode.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/asm/tests/dop.asm create mode 100644 backends/tfhe-hpu-backend/src/asm/tests/iop.asm create mode 100644 backends/tfhe-hpu-backend/src/asm/tests/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/glwe_ciphertext.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/glwe_lookuptable.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/lwe_bootstrap_key.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/lwe_ciphertext.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/lwe_keyswitch_key.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/parameters.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/traits/container.rs create mode 100644 backends/tfhe-hpu-backend/src/entities/traits/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/sim/ipc.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/sim/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/v80/ami.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/v80/mem_alloc.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/v80/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/v80/qdma.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/cxx/hpu_hw.cc create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/cxx/hpu_hw.h create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/cxx/mem_zone.cc create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/cxx/mem_zone.h create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/cxx/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/ffi/xrt/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/demo.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/ilp.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/llt/kogge.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/llt/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/llt/vardeg.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/fw_impl/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/isc_sim/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/isc_sim/pe.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/isc_sim/pool.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/isc_sim/report.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/isc_sim/scheduler.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/metavar.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/program.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/rtl/config.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/rtl/macros.rs create mode 100644 backends/tfhe-hpu-backend/src/fw/rtl/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/backend.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/cmd.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/config.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/device.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/io_dump.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/memory/ciphertext.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/memory/huge.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/memory/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/rtl/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/rtl/params.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/rtl/runtime.rs create mode 100644 backends/tfhe-hpu-backend/src/interface/variable.rs create mode 100644 backends/tfhe-hpu-backend/src/isc_trace/fmt.rs create mode 100644 backends/tfhe-hpu-backend/src/isc_trace/fmt/test/data.rs create mode 100644 backends/tfhe-hpu-backend/src/isc_trace/fmt/test/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/isc_trace/mod.rs create mode 100644 backends/tfhe-hpu-backend/src/isc_trace/packed_struct.rs create mode 100644 backends/tfhe-hpu-backend/src/lib.rs create mode 100644 backends/tfhe-hpu-backend/src/prelude.rs create mode 100644 backends/tfhe-hpu-backend/src/utils/dop_fmt.rs create mode 100644 backends/tfhe-hpu-backend/src/utils/fw.rs create mode 100644 backends/tfhe-hpu-backend/src/utils/hputil.rs create mode 100644 backends/tfhe-hpu-backend/src/utils/iop_fmt.rs create mode 100644 mockups/tfhe-hpu-mockup/Cargo.toml create mode 100644 mockups/tfhe-hpu-mockup/Justfile create mode 100644 mockups/tfhe-hpu-mockup/LICENSE create mode 100644 mockups/tfhe-hpu-mockup/Readme.md create mode 100644 mockups/tfhe-hpu-mockup/figures/tfhe-hpu-mockup.excalidraw.png create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_44b.toml create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_44b_fast.toml create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_64b.toml create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_64b_fast.toml create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_64b_pfail64.toml create mode 100644 mockups/tfhe-hpu-mockup/params/gaussian_64b_pfail64_psi64.toml create mode 100644 mockups/tfhe-hpu-mockup/params/tuniform_64b_fast.toml create mode 100644 mockups/tfhe-hpu-mockup/params/tuniform_64b_pfail64_psi64.toml create mode 100644 mockups/tfhe-hpu-mockup/src/ipc.rs create mode 100644 mockups/tfhe-hpu-mockup/src/lib.rs create mode 100644 mockups/tfhe-hpu-mockup/src/mockup.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/memory/ddr.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/memory/hbm.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/memory/mod.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/mod.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/params.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/regmap.rs create mode 100644 mockups/tfhe-hpu-mockup/src/modules/ucore.rs create mode 100644 setup_hpu.sh rename tfhe/docs/_static/{cpu_gpu_integer_benchmark_fheuint64_tuniform_2m64_ciphertext.svg => cpu_gpu_hpu_integer_benchmark_fheuint64_tuniform_2m64_ciphertext.svg} (100%) create mode 100644 tfhe/docs/_static/hpu_integer_benchmark_hpux1_tuniform_2m64_ciphertext.svg create mode 100644 tfhe/docs/_static/hpu_integer_benchmark_hpux1_tuniform_2m64_plaintext.svg create mode 100644 tfhe/docs/configuration/hpu_acceleration/benchmark.md create mode 100644 tfhe/docs/configuration/hpu_acceleration/run_on_hpu.md create mode 100644 tfhe/docs/getting_started/benchmarks/hpu/README.md create mode 100644 tfhe/docs/getting_started/benchmarks/hpu/hpu_integer_operations.md create mode 100644 tfhe/examples/hpu/bench.rs create mode 100644 tfhe/examples/hpu/hlapi.rs create mode 100644 tfhe/examples/hpu/matmul.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/lwe_hpu_noise.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/variance_formula/lwe_keyswitch.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/variance_formula/lwe_programmable_bootstrap.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/variance_formula/mod.rs create mode 100644 tfhe/src/core_crypto/algorithms/test/noise_distribution/variance_formula/secure_noise.rs create mode 100644 tfhe/src/core_crypto/hpu/algorithms/mod.rs create mode 100644 tfhe/src/core_crypto/hpu/algorithms/modswitch.rs create mode 100644 tfhe/src/core_crypto/hpu/algorithms/order.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/glwe_ciphertext.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/glwe_lookuptable.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/lwe_bootstrap_key.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/lwe_ciphertext.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/lwe_keyswitch_key.rs create mode 100644 tfhe/src/core_crypto/hpu/entities/mod.rs create mode 100644 tfhe/src/core_crypto/hpu/mod.rs create mode 100644 tfhe/src/high_level_api/integers/unsigned/tests/hpu.rs create mode 100644 tfhe/src/integer/hpu/ciphertext/mod.rs create mode 100644 tfhe/src/integer/hpu/mod.rs create mode 100644 tfhe/src/shortint/parameters/hpu.rs create mode 100644 tfhe/src/shortint/parameters/v1_2/hpu.rs create mode 100644 tfhe/tests/hpu.rs diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 7a9086cb0..584f41649 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -6,6 +6,7 @@ self-hosted-runner: - large_windows_16_latest - large_ubuntu_16 - large_ubuntu_16-22.04 + - v80-desktop # Configuration variables in array of strings defined in your repository or # organization. `null` means disabling configuration variables check. # Empty array means no configuration variable is allowed. diff --git a/.github/workflows/benchmark_hpu_integer.yml b/.github/workflows/benchmark_hpu_integer.yml new file mode 100644 index 000000000..fb12c3ff4 --- /dev/null +++ b/.github/workflows/benchmark_hpu_integer.yml @@ -0,0 +1,88 @@ +# Run all integer benchmarks on a permanent HPU instance and return parsed results to Slab CI bot. +name: Hpu Integer Benchmarks + +on: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RESULTS_FILENAME: parsed_benchmark_results_${{ github.sha }}.json + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + RUST_BACKTRACE: "full" + RUST_MIN_STACK: "8388608" + +permissions: {} + +jobs: + integer-benchmarks-hpu: + name: Execute integer & erc20 benchmarks for HPU backend + runs-on: v80-desktop + concurrency: + group: ${{ github.workflow }}_${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + timeout-minutes: 1440 # 24 hours + steps: + # Needed as long as hw_regmap repository is private + - name: Configure SSH + uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Checkout tfhe-rs repo with tags + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 + persist-credentials: 'false' + token: ${{ secrets.REPO_CHECKOUT_TOKEN }} + + - name: Get benchmark details + run: | + { + echo "BENCH_DATE=$(date --iso-8601=seconds)"; + echo "COMMIT_DATE=$(git --no-pager show -s --format=%cd --date=iso8601-strict ${{ github.sha }})"; + echo "COMMIT_HASH=$(git describe --tags --dirty)"; + } >> "${GITHUB_ENV}" + + - name: Install rust + uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 + with: + toolchain: nightly + + - name: Checkout Slab repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: zama-ai/slab + path: slab + persist-credentials: 'false' + token: ${{ secrets.REPO_CHECKOUT_TOKEN }} + + - name: Run benchmarks + run: | + make bench_integer_hpu + make bench_hlapi_erc20_hpu + + - name: Parse results + run: | + python3 ./ci/benchmark_parser.py target/criterion "${RESULTS_FILENAME}" \ + --database tfhe_rs \ + --hardware "hpu_x1" \ + --backend hpu \ + --project-version "${COMMIT_HASH}" \ + --branch "${REF_NAME}" \ + --commit-date "${COMMIT_DATE}" \ + --bench-date "${BENCH_DATE}" \ + --walk-subdirs + env: + REF_NAME: ${{ github.ref_name }} + + - name: Upload parsed results artifact + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 + with: + name: ${{ github.sha }}_integer_benchmarks + path: ${{ env.RESULTS_FILENAME }} + + - name: Send data to Slab + shell: bash + run: | + python3 slab/scripts/data_sender.py "${RESULTS_FILENAME}" "${{ secrets.JOB_SECRET }}" \ + --slab-url "${{ secrets.SLAB_URL }}" diff --git a/.github/workflows/cargo_build.yml b/.github/workflows/cargo_build.yml index 32edfe80b..d75c22bbf 100644 --- a/.github/workflows/cargo_build.yml +++ b/.github/workflows/cargo_build.yml @@ -94,5 +94,10 @@ jobs: run: | make build_tfhe_coverage + - name: Run Hpu pcc checks + if: ${{ contains(matrix.os, 'ubuntu') }} + run: | + make pcc_hpu + # The wasm build check is a bit annoying to set-up here and is done during the tests in # aws_tfhe_tests.yml diff --git a/.github/workflows/cargo_test_fft.yml b/.github/workflows/cargo_test_fft.yml index 8c25092dc..12edc0614 100644 --- a/.github/workflows/cargo_test_fft.yml +++ b/.github/workflows/cargo_test_fft.yml @@ -51,7 +51,7 @@ jobs: runs-on: ${{ matrix.runner_type }} strategy: matrix: - runner_type: [ubuntu-latest, macos-latest, windows-latest] + runner_type: [ ubuntu-latest, macos-latest, windows-latest ] fail-fast: false steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -82,7 +82,7 @@ jobs: runs-on: ${{ matrix.runner_type }} strategy: matrix: - runner_type: [ubuntu-latest, macos-latest, windows-latest] + runner_type: [ ubuntu-latest, macos-latest, windows-latest ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: diff --git a/.github/workflows/cargo_test_ntt.yml b/.github/workflows/cargo_test_ntt.yml index f21f35f26..aceee9509 100644 --- a/.github/workflows/cargo_test_ntt.yml +++ b/.github/workflows/cargo_test_ntt.yml @@ -51,7 +51,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] fail-fast: false steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -77,7 +77,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: diff --git a/.github/workflows/hpu_hlapi_tests.yml b/.github/workflows/hpu_hlapi_tests.yml new file mode 100644 index 000000000..a4773f31f --- /dev/null +++ b/.github/workflows/hpu_hlapi_tests.yml @@ -0,0 +1,73 @@ +# Test tfhe-fft +name: Cargo Test HLAPI HPU + +on: + pull_request: + push: + branches: + - main + +env: + CARGO_TERM_COLOR: always + IS_PULL_REQUEST: ${{ github.event_name == 'pull_request' }} + CHECKOUT_TOKEN: ${{ secrets.REPO_CHECKOUT_TOKEN || secrets.GITHUB_TOKEN }} + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + + +permissions: { } + +jobs: + should-run: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + hpu_test: ${{ env.IS_PULL_REQUEST == 'false' || steps.changed-files.outputs.hpu_any_changed }} + steps: + - name: Checkout tfhe-rs + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 + persist-credentials: 'false' + token: ${{ env.CHECKOUT_TOKEN }} + + - name: Check for file changes + id: changed-files + uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 + with: + files_yaml: | + hpu: + - tfhe/Cargo.toml + - Makefile + - backends/tfhe-hpu-backend/** + - mockups/tfhe-hpu-mockup/** + + cargo-tests-hpu: + needs: should-run + if: needs.should-run.outputs.hpu_test == 'true' + runs-on: large_ubuntu_16 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: 'false' + token: ${{ env.CHECKOUT_TOKEN }} + + - name: Install Rust + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af + with: + toolchain: stable + override: true + + - name: Install Just + run: | + cargo install just + + - name: Test HLAPI HPU + run: | + source setup_hpu.sh + just -f mockups/tfhe-hpu-mockup/Justfile BUILD_PROFILE=release mockup & + make HPU_CONFIG=sim test_high_level_api_hpu + diff --git a/.github/workflows/make_release_hpu.yml b/.github/workflows/make_release_hpu.yml new file mode 100644 index 000000000..a51be72a1 --- /dev/null +++ b/.github/workflows/make_release_hpu.yml @@ -0,0 +1,105 @@ +name: Publish HPU release + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry-run" + type: boolean + default: true + +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +permissions: {} + +jobs: + verify_tag: + uses: ./.github/workflows/verify_tagged_commit.yml + secrets: + RELEASE_TEAM: ${{ secrets.RELEASE_TEAM }} + READ_ORG_TOKEN: ${{ secrets.READ_ORG_TOKEN }} + + package: + runs-on: ubuntu-latest + needs: verify_tag + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + persist-credentials: 'false' + token: ${{ secrets.REPO_CHECKOUT_TOKEN }} + - name: Prepare package + run: | + cargo package -p tfhe-hpu-backend + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: crate + path: target/package/*.crate + - name: generate hash + id: hash + run: cd target/package && echo "hash=$(sha256sum ./*.crate | base64 -w0)" >> "${GITHUB_OUTPUT}" + + provenance: + if: ${{ !inputs.dry_run }} + needs: [package] + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + permissions: + # Needed to detect the GitHub Actions environment + actions: read + # Needed to create the provenance via GitHub OIDC + id-token: write + # Needed to upload assets/artifacts + contents: write + with: + # SHA-256 hashes of the Crate package. + base64-subjects: ${{ needs.package.outputs.hash }} + + publish_release: + name: Publish tfhe-hpu-backend Release + runs-on: ubuntu-latest + needs: [verify_tag, package] # for comparing hashes + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + persist-credentials: 'false' + token: ${{ secrets.REPO_CHECKOUT_TOKEN }} + + - name: Publish crate.io package + env: + CRATES_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + DRY_RUN: ${{ inputs.dry_run && '--dry-run' || '' }} + run: | + # DRY_RUN expansion cannot be double quoted when variable contains empty string otherwise cargo publish + # would fail. This is safe since DRY_RUN is handled in the env section above. + # shellcheck disable=SC2086 + cargo publish -p tfhe-hpu-backend --token "${CRATES_TOKEN}" ${DRY_RUN} + + - name: Generate hash + id: published_hash + run: cd target/package && echo "pub_hash=$(sha256sum ./*.crate | base64 -w0)" >> "${GITHUB_OUTPUT}" + + - name: Slack notification (hashes comparison) + if: ${{ needs.package.outputs.hash != steps.published_hash.outputs.pub_hash }} + continue-on-error: true + uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3 + env: + SLACK_COLOR: failure + SLACK_MESSAGE: "SLSA tfhe-hpu-backend crate - hash comparison failure: (${{ env.ACTION_RUN_URL }})" + + - name: Slack Notification + if: ${{ failure() || (cancelled() && github.event_name != 'pull_request') }} + continue-on-error: true + uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "tfhe-hpu-backend release failed: (${{ env.ACTION_RUN_URL }})" diff --git a/.lfsconfig b/.lfsconfig new file mode 100644 index 000000000..e494bcf0b --- /dev/null +++ b/.lfsconfig @@ -0,0 +1,2 @@ +[lfs] + fetchexclude = * diff --git a/Cargo.toml b/Cargo.toml index 421ee22c2..dcf978b49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,12 @@ members = [ "tasks", "tfhe-csprng", "backends/tfhe-cuda-backend", + "backends/tfhe-hpu-backend", "utils/tfhe-versionable", "utils/tfhe-versionable-derive", "utils/param_dedup", "tests", + "mockups/tfhe-hpu-mockup", ] exclude = [ diff --git a/Makefile b/Makefile index 34b29a86e..29d482d94 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ SHELL:=$(shell /usr/bin/env which bash) OS:=$(shell uname) RS_CHECK_TOOLCHAIN:=$(shell cat toolchain.txt | tr -d '\n') CARGO_RS_CHECK_TOOLCHAIN:=+$(RS_CHECK_TOOLCHAIN) +CARGO_BUILD_JOBS=default CPU_COUNT=$(shell ./scripts/cpu_count.sh) RS_BUILD_TOOLCHAIN:=stable CARGO_RS_BUILD_TOOLCHAIN:=+$(RS_BUILD_TOOLCHAIN) @@ -55,6 +56,9 @@ REGEX_PATTERN?='' TFHECUDA_SRC=backends/tfhe-cuda-backend/cuda TFHECUDA_BUILD=$(TFHECUDA_SRC)/build +# tfhe-hpu-backend +HPU_CONFIG=v80 + # Exclude these files from coverage reports define COVERAGE_EXCLUDED_FILES --exclude-files apps/trivium/src/trivium/* \ @@ -301,6 +305,13 @@ check_gpu: install_rs_check_toolchain --all-targets \ -p $(TFHE_SPEC) +.PHONY: clippy_hpu # Run clippy lints on tfhe with "hpu" enabled +clippy_hpu: install_rs_check_toolchain + RUSTFLAGS="$(RUSTFLAGS)" cargo "$(CARGO_RS_CHECK_TOOLCHAIN)" clippy \ + --features=boolean,shortint,integer,internal-keycache,hpu,pbs-stats,extended-types \ + --all-targets \ + -p $(TFHE_SPEC) -- --no-deps -D warnings + .PHONY: fix_newline # Fix newline at end of file issues to be UNIX compliant fix_newline: check_linelint_installed linelint -a . @@ -473,6 +484,11 @@ clippy_cuda_backend: install_rs_check_toolchain RUSTFLAGS="$(RUSTFLAGS)" cargo "$(CARGO_RS_CHECK_TOOLCHAIN)" clippy --all-targets \ -p tfhe-cuda-backend -- --no-deps -D warnings +.PHONY: clippy_hpu_backend # Run clippy lints on the tfhe-hpu-backend +clippy_hpu_backend: install_rs_check_toolchain + RUSTFLAGS="$(RUSTFLAGS)" cargo "$(CARGO_RS_CHECK_TOOLCHAIN)" clippy --all-targets \ + -p tfhe-hpu-backend -- --no-deps -D warnings + .PHONY: check_rust_bindings_did_not_change # Check rust bindings are up to date for tfhe-cuda-backend check_rust_bindings_did_not_change: cargo build -p tfhe-cuda-backend && "$(MAKE)" fmt_gpu && \ @@ -702,6 +718,28 @@ test_signed_integer_multi_bit_gpu_ci: install_rs_check_toolchain install_cargo_n --cargo-profile "$(CARGO_PROFILE)" --multi-bit --backend "gpu" \ --signed-only --tfhe-package "$(TFHE_SPEC)" +.PHONY: test_integer_hpu_ci # Run the tests for integer ci on hpu backend +test_integer_hpu_ci: install_rs_check_toolchain install_cargo_nextest + cargo test --release -p $(TFHE_SPEC) --features hpu-v80 --test hpu + +.PHONY: test_integer_hpu_mockup_ci # Run the tests for integer ci on hpu backend and mockup +test_integer_hpu_mockup_ci: install_rs_check_toolchain install_cargo_nextest + source ./setup_hpu.sh --config sim ; \ + cargo build --release --bin hpu_mockup; \ + coproc target/release/hpu_mockup --params mockups/tfhe-hpu-mockup/params/tuniform_64b_pfail64_psi64.toml > mockup.log; \ + HPU_TEST_ITER=1 \ + cargo test --profile devo -p $(TFHE_SPEC) --features hpu --test hpu -- u32 && \ + kill %1 + +.PHONY: test_integer_hpu_mockup_ci_fast # Run the quick tests for integer ci on hpu backend and mockup. +test_integer_hpu_mockup_ci_fast: install_rs_check_toolchain install_cargo_nextest + source ./setup_hpu.sh --config sim ; \ + cargo build --profile devo --bin hpu_mockup; \ + coproc target/devo/hpu_mockup --params mockups/tfhe-hpu-mockup/params/tuniform_64b_fast.toml > mockup.log; \ + HPU_TEST_ITER=1 \ + cargo test --profile devo -p $(TFHE_SPEC) --features hpu --test hpu -- u32 && \ + kill %1 + .PHONY: test_boolean # Run the tests of the boolean module test_boolean: install_rs_build_toolchain RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_BUILD_TOOLCHAIN) test --profile $(CARGO_PROFILE) \ @@ -857,6 +895,22 @@ test_high_level_api_gpu: install_rs_build_toolchain install_cargo_nextest --features=integer,internal-keycache,gpu -p $(TFHE_SPEC) \ -E "test(/high_level_api::.*gpu.*/)" +test_high_level_api_hpu: install_rs_build_toolchain install_cargo_nextest +ifeq ($(HPU_CONFIG), v80) + RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_BUILD_TOOLCHAIN) nextest run --cargo-profile $(CARGO_PROFILE) \ + --build-jobs=$(CARGO_BUILD_JOBS) \ + --test-threads=1 \ + --features=integer,internal-keycache,hpu,hpu-v80 -p $(TFHE_SPEC) \ + -E "test(/high_level_api::.*hpu.*/)" +else + RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_BUILD_TOOLCHAIN) nextest run --cargo-profile $(CARGO_PROFILE) \ + --build-jobs=$(CARGO_BUILD_JOBS) \ + --test-threads=1 \ + --features=integer,internal-keycache,hpu -p $(TFHE_SPEC) \ + -E "test(/high_level_api::.*hpu.*/)" +endif + + .PHONY: test_strings # Run the tests for strings ci test_strings: install_rs_build_toolchain RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_BUILD_TOOLCHAIN) test --profile $(CARGO_PROFILE) \ @@ -1100,6 +1154,12 @@ clippy_bench_gpu: install_rs_check_toolchain --features=gpu,shortint,integer,internal-keycache,nightly-avx512,pbs-stats,zk-pok \ -p tfhe-benchmark -- --no-deps -D warnings +.PHONY: clippy_bench_hpu # Run clippy lints on tfhe-benchmark +clippy_bench_hpu: install_rs_check_toolchain + RUSTFLAGS="$(RUSTFLAGS)" cargo "$(CARGO_RS_CHECK_TOOLCHAIN)" clippy --all-targets \ + --features=hpu,shortint,integer,internal-keycache,pbs-stats\ + -p tfhe-benchmark -- --no-deps -D warnings + .PHONY: print_doc_bench_parameters # Print parameters used in doc benchmarks print_doc_bench_parameters: RUSTFLAGS="" cargo run --example print_doc_bench_parameters \ @@ -1133,6 +1193,14 @@ bench_signed_integer_gpu: install_rs_check_toolchain --bench integer-signed-bench \ --features=integer,gpu,internal-keycache,nightly-avx512,pbs-stats -p tfhe-benchmark -- +.PHONY: bench_integer_hpu # Run benchmarks for integer on HPU backend +bench_integer_hpu: install_rs_check_toolchain + source ./setup_hpu.sh --config $(HPU_CONFIG) ; \ + 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 \ + --bench integer-bench \ + --features=integer,internal-keycache,pbs-stats,hpu,hpu-v80 -p tfhe-benchmark -- --quick + .PHONY: bench_integer_compression # Run benchmarks for unsigned integer compression bench_integer_compression: install_rs_check_toolchain RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_TYPE=$(BENCH_TYPE) \ @@ -1324,6 +1392,14 @@ bench_hlapi_dex_gpu: install_rs_check_toolchain --bench hlapi-dex \ --features=integer,gpu,internal-keycache,pbs-stats,nightly-avx512 -p tfhe-benchmark -- +.PHONY: bench_hlapi_erc20_hpu # Run benchmarks for ECR20 operations on HPU +bench_hlapi_erc20_hpu: install_rs_check_toolchain + source ./setup_hpu.sh --config $(HPU_CONFIG) ; \ + RUSTFLAGS="$(RUSTFLAGS)" \ + cargo $(CARGO_RS_CHECK_TOOLCHAIN) bench \ + --bench hlapi-erc20 \ + --features=integer,internal-keycache,hpu,hpu-v80 -p tfhe-benchmark -- --quick + .PHONY: bench_tfhe_zk_pok # Run benchmarks for the tfhe_zk_pok crate bench_tfhe_zk_pok: install_rs_check_toolchain RUSTFLAGS="$(RUSTFLAGS)" \ @@ -1423,6 +1499,9 @@ tfhe_lints pcc_gpu: check_rust_bindings_did_not_change clippy_rustdoc_gpu \ clippy_gpu clippy_cuda_backend clippy_bench_gpu check_compile_tests_benches_gpu +.PHONY: pcc_hpu # pcc stands for pre commit checks for HPU compilation +pcc_hpu: clippy_hpu clippy_hpu_backend test_integer_hpu_mockup_ci_fast + .PHONY: fpcc # pcc stands for pre commit checks, the f stands for fast fpcc: no_tfhe_typo no_dbg_log check_parameter_export_ok check_fmt check_typos lint_doc \ check_md_docs_are_tested clippy_fast check_compile_tests diff --git a/_typos.toml b/_typos.toml index 69e10146c..4946598a7 100644 --- a/_typos.toml +++ b/_typos.toml @@ -11,11 +11,13 @@ extend-ignore-identifiers-re = [ # Example with string replacing "hello" with "herlo" "herlo", # Example in trivium - "C9217BA0D762ACA1" + "C9217BA0D762ACA1", + "0x[0-9a-fA-F]+" ] [files] extend-exclude = [ "backends/tfhe-cuda-backend/cuda/src/fft128/twiddles.cu", "backends/tfhe-cuda-backend/cuda/src/fft/twiddles.cu", + "backends/tfhe-hpu-backend/config_store/**/*.link_summary", ] diff --git a/backends/tfhe-hpu-backend/.gitattributes b/backends/tfhe-hpu-backend/.gitattributes new file mode 100644 index 000000000..afe123f81 --- /dev/null +++ b/backends/tfhe-hpu-backend/.gitattributes @@ -0,0 +1,3 @@ +*.xclbin filter=lfs diff=lfs merge=lfs -text +*.pdi filter=lfs diff=lfs merge=lfs -text +python/lib/example.json filter=lfs diff=lfs merge=lfs -text diff --git a/backends/tfhe-hpu-backend/.gitignore b/backends/tfhe-hpu-backend/.gitignore new file mode 100644 index 000000000..b175ca32e --- /dev/null +++ b/backends/tfhe-hpu-backend/.gitignore @@ -0,0 +1,3 @@ +ngt_* +config +kogge_cfg.toml diff --git a/backends/tfhe-hpu-backend/Cargo.toml b/backends/tfhe-hpu-backend/Cargo.toml new file mode 100644 index 000000000..9022cd5c6 --- /dev/null +++ b/backends/tfhe-hpu-backend/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "tfhe-hpu-backend" +version = "0.1.0" +edition = "2021" +license = "BSD-3-Clause-Clear" +description = "HPU implementation on FPGA of TFHE-rs primitives." +homepage = "https://www.zama.ai/" +documentation = "https://docs.zama.ai/tfhe-rs" +repository = "https://github.com/zama-ai/tfhe-rs" +readme = "README.md" +keywords = ["fully", "homomorphic", "encryption", "fhe", "cryptography", "hardware", "fpga"] + +[features] +hw-xrt = [] +hw-v80 = [] +io-dump = ["num-traits"] +rtl_graph = ["dot2"] +utils = ["clap", "clap-num", "bitvec", "serde_json"] + +[build-dependencies] +cxx-build = "1.0" + +[dependencies] +cxx = "1.0" +hw_regmap = "0.1.0" + +strum = { version = "0.26.2", features = ["derive"] } +strum_macros = "0.26.2" +enum_dispatch = "0.3.13" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +toml = { version = "0.8.*", features = [] } +paste = "1.0.15" +thiserror = "1.0.61" +bytemuck = "1.16.0" +anyhow = "1.0.82" +lazy_static = "1.4.0" +rand = "0.8.5" +regex = "1.10.4" +bitflags = { version = "2.5.0", features = ["serde"] } +itertools = "0.11.0" +lru = "0.12.3" +bitfield-struct = "0.10.0" +crossbeam = { version = "0.8.4", features = ["crossbeam-queue"] } +rayon = { workspace = true } + +# Dependencies used for Sim feature +ipc-channel = "0.18.3" + +# Dependencies used for debug feature +num-traits = { version = "*", optional = true } +clap = { version = "4.4.4", features = ["derive"], optional = true } +clap-num = { version = "1.1.1", optional = true } +nix = { version = "0.29.0", features = ["ioctl", "uio"] } + +# Dependencies used for rtl_graph features +dot2 = { version = "*", optional = true } + +bitvec = { version = "*", optional = true } +serde_json = { version = "*", optional = true } + +# Binary for manual debugging +# Enable to access Hpu register and drive some custom sequence by hand +[[bin]] +name = "hputil" +path = "src/utils/hputil.rs" +required-features = ["utils"] + +# Binary for asm manipulation +# Enable to convert back and forth between asm/hex format +[[bin]] +name = "dop_fmt" +path = "src/utils/dop_fmt.rs" +required-features = ["utils"] + +# Enable to convert back and forth between asm/hex format +[[bin]] +name = "iop_fmt" +path = "src/utils/iop_fmt.rs" +required-features = ["utils"] + +# Firmware generation +# Enable to expand IOp in list of Dop for inspection +[[bin]] +name = "fw" +path = "src/utils/fw.rs" +required-features = ["utils"] diff --git a/backends/tfhe-hpu-backend/LICENSE b/backends/tfhe-hpu-backend/LICENSE new file mode 100644 index 000000000..48312e88a --- /dev/null +++ b/backends/tfhe-hpu-backend/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause Clear License + +Copyright © 2025 ZAMA. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +3. Neither the name of ZAMA nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. +THIS SOFTWARE IS PROVIDED BY THE ZAMA AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +ZAMA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backends/tfhe-hpu-backend/Readme.md b/backends/tfhe-hpu-backend/Readme.md new file mode 100644 index 000000000..f80e2c633 --- /dev/null +++ b/backends/tfhe-hpu-backend/Readme.md @@ -0,0 +1,261 @@ +# TFHE-hpu-backend + +## Brief +The `tfhe-hpu-backend` holds the code to interface with the HPU accelerator of TFHE. +It contains a `HpuDevice` abstraction that enables easy configuration and dispatching of TFHE operations on the HPU accelerator. + +The user API exposes the following functions for hardware setup: +- `HpuDevice::new`, `HpuDevice::from_config`: Instantiates abstraction device from configuration file. +- `HpuDevice::init`: Configures and uploads the required public material. +- `new_var_from`: Creates a HPU ciphertext from `tfhe-rs` ciphertext. + +HPU device could also be used from `integer` with the help of the following function: +- `tfhe::integer::hpu::init_device`: Init given HPU device with server key. +- `tfhe::integer::hpu::ciphertext::HpuRadixCiphertext::from_radix_ciphertext`: Convert a CpuRadixCiphertext in it's HPU counterpart. + +HPU device could also be used seamlessly from `hl-api` by setting up a thread-local HPU server key: +- `tfhe::Config::from_hpu_device`: Extract hl-api configuration from HpuDevice. +- `tfhe::set_server_key`: Register the Hpu server key in the current thread. + +HPU variables could also be created from a `high-level-api` object, with the help of the `hw-xfer` feature. +This implements a trait that enables `clone_on`, `mv_on` `FheUint` object on the HPU accelerator, and cast back `from` them. + +These objects implement the `std::ops` trait and could be used to dispatch operations on HPU hardware. + +### Backend structure +`tfhe-hpu-backend` is split in various modules: +- `entities`: Defines structure handled by HPU accelerator. Conversion traits from/into those objects are implemented in `tfhe-rs`. +- `asm`: Describes assembly-like language for the HPU. It enables abstract HPU behavior and easily updates it through micro-code. +- `fw`: Abstraction to help the micro-code designer. Uses a simple rust program for describing new HPU operations. Helps with register/heap management. +- `interface`: + + `device`: High-level structure that exposes the User API. + + `backend`: Inner private structure that contains HPU modules + + `variable`: Wraps HPU ciphertexts. It enables to hook an hardware object lifetime within the `rust` borrow-checker. + + `memory`: Handles on-board memory allocation and synchronization + + `config`: Helps to configure HPU accelerator through a TOML configuration file + + `cmd`: Translates operation over `variable` in concrete HPU commands + + `regmap`: Communicates with the HPU internal register with ease. + + `rtl`: Defines concrete `rust` structure populated from HPU's status/configuration registers + + +Below is an overview of the internal structure of the Backend. +![HPU backend structure](./figures/tfhe-hpu-backend.excalidraw.png) + +This picture depicts the internal modules of `tfhe-hpu-backend`, Device is the main entry point for the user. Its lifecycle is as follows: + +1. Create HpuDevice, open link with the associated FPGA. Configure associated drivers and upload the bitstream. Read FPGA registers to extract supported configuration and features. Build Firmware conversion table (IOp -> DOps stream). + +2. Allocate required memory chunks in the on-board memory. Upload public material required by TFHE computation. + +3. Create HPU variables that handle TFHE Ciphertexts. It wraps TFHE Ciphertext with required internal resources and enforces the correct lifetime management. This abstraction enforces that during the variable lifecycle all required resources are valid. + +4. Users could trigger HPU operation from the HPU variable. + Variable abstraction enforces that required objects are correctly synced on the hardware and converts each operation in a concrete HPU command. + When HPU operation is acknowledged by the hardware, the internal state of the associated variable is updated. + This mechanism enables asynchronous operation and minimal amount of Host to/from HW memory transfer. + This mechanism also enables offloading a computation graph to the HPU and requires a synchronization only on the final results. + +## Example +### Configuration file +HPU configuration knobs are gathered in a TOML configuration file. This file describes the targeted FPGA with its associated configuration: +```toml +[fpga] # FPGA target + # Register layout in the FPGA + regmap=["${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_3in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_3in3.toml"] + polling_us=10 +[fpga.ffi.V80] # Hardware properties + ami_dev="/dev/ami1" # Name of ami device + qdma_h2c="/dev/qdma${V80_PCIE_DEV}001-MM-0" # QDma host to card device + qdma_c2h="/dev/qdma${V80_PCIE_DEV}001-MM-1" # QDma card to host device + +[rtl] # RTL option + bpip_used = true # BPIP/IPIP mode + bpip_use_opportunism = false # Use strict flush paradigm + bpip_timeout = 100_000 # BPIP timeout in clock `cycles` + +[board] # Board configuration + ct_mem = 32768 # Number of allocated ciphertext + ct_pc = [ # Memory used for ciphertext + {Hbm= {pc=32}}, + {Hbm= {pc=33}}, + ] + heap_size = 16384 # Number of slots reserved for heap + + lut_mem = 256 # Number of allocated LUT table + lut_pc = {Hbm={pc=34}} # Memory used for LUT + + fw_size= 16777216 # Size in byte of the Firmware translation table + fw_pc = {Ddr= {offset= 0x3900_0000}} # Memory used for firmware translation table + + bsk_pc = [ # Memory used for Bootstrapping key + {Hbm={pc=8}}, + {Hbm={pc=12}}, + {Hbm={pc=24}}, + {Hbm={pc=28}}, + {Hbm={pc=40}}, + {Hbm={pc=44}}, + {Hbm={pc=56}}, + {Hbm={pc=60}} + ] + + ksk_pc = [ # Memory used for Keyswitching key + {Hbm={pc=0}}, + {Hbm={pc=1}}, + {Hbm={pc=2}}, + {Hbm={pc=3}}, + {Hbm={pc=4}}, + {Hbm={pc=5}}, + {Hbm={pc=6}}, + {Hbm={pc=7}}, + {Hbm={pc=16}}, + {Hbm={pc=17}}, + {Hbm={pc=18}}, + {Hbm={pc=19}}, + {Hbm={pc=20}}, + {Hbm={pc=21}}, + {Hbm={pc=22}}, + {Hbm={pc=23}} + ] + + trace_pc = {Hbm={pc=35}} # Memory used for trace log + trace_depth = 32 # Size of Memory in MiB allocated for trace log + +[firmware] # Firmware properties + implementation = "Llt" # Firmware flavor to use + integer_w=[4,6,8,10,12,14,16,32,64,128] # List of supported IOp width + min_batch_size = 11 # Minimum batch size for maximum throughput + kogge_cfg = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/kogge_cfg.toml" + custom_iop.'IOP[0]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_0.asm" + +# Default firmware configuration. Could be edited on per-IOp basis +[firmware.op_cfg.default] + fill_batch_fifo = true + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + ``` + +### Device setup +Following code snippet shows how to instantiate and configure a `HpuDevice`: +```rust + // Following code snippets used the HighLevelApi abstraction + // Instantiate HpuDevice -------------------------------------------------- + let hpu_device = HpuDevice::from_config(&args.config.expand()); + + // Generate keys ---------------------------------------------------------- + let config = Config::from_hpu_device(&hpu_device); + + let cks = ClientKey::generate(config); + let csks = CompressedServerKey::new(&cks); + + // Register HpuDevice and key as thread-local engine + set_server_key((hpu_device, csks)); +``` + +### Clone CPU ciphertext on HPU +Following code snippet shows how to convert CPU ciphertext in HPU one: +``` rust + // Draw random value as input + let a = rand::thread_rng().gen_range(0..u8::MAX); + + // Encrypt them on Cpu side + let a_fhe = FheUint8::encrypt(a, &cks); + + // Clone a ciphertext and move them in HpuWorld + // NB: Data doesn't move over Pcie at this stage + // Data are only arranged in Hpu ordered an copy in the host internal buffer + let a_hpu = a_fhe.clone_on(&hpu_device); +``` + +### Dispatch operation on HPU +Once registered as thread-local engine, HighLevel FheUint are converted in Hpu format. +Following code snippets show how to start operation on HPU: + +``` rust + // Sum ------------------------------------------------------------- + // Generate random inputs value and compute expected result + let in_a = rng.gen_range(0..u64::max_value()); + let in_b = rng.gen_range(0..u64::max_value()); + let clear_sum_ab = in_a.wrapping_add(in_b); + + // Encrypt input value + let fhe_a = FheUint64::encrypt(in_a, cks); + let fhe_b = FheUint64::encrypt(in_b, cks); + + // Triggered operation on HPU through hl_api + let fhe_sum_ab = fhe_a+fhe_b; + + // Decrypt values + let dec_sum_ab: u64 = fhe_sum_ab.decrypt(cks); +``` + +## Pre-made Examples +There are some example applications already available in `tfhe/examples/hpu`: + * hpu_hlapi: Depict the used of HPU device through HighLevelApi. + * hpu_bench: Depict the used of HPU device through Integer abstraction level. + +In order to run those applications on hardware, user must build from the project root (i.e `tfhe-rs-internal`) with `hpu-v80` features: + +> NB: Running examples required to have correctly pulled the `.pdi` files. Those files, due to their size, are backed by git-lfs and disabled by default. +> In order to retrieve them, use the following command: +> ```bash +> git lfs pull --include="*" --exclude="" +> ``` + +``` bash +cargo build --release --features="hpu-v80" --example hpu_hlapi --example hpu_bench +# Correctly setup environment with setup_hpu.sh script +source setup_hpu.sh --config v80 --init-qdma +./target/release/examples/hpu_bench --integer-w 64 --integer-w 32 --iop MUL --iter 10 +./target/release/examples/hpu_hlapi +``` + +## Test framework +There is also a set of tests backed in tfhe-rs. Tests are gather in testbundle over various integer width. +Those tests have 5 sub-kind: +* `alu`: Run and check all ct x ct IOp +* `alus`: Run and check all ct x scalar IOp +* `bitwise`: Run and check all bitwise IOp +* `cmp`: Run and check all comparison IOp +* `ternary`: Run and check ternary operation +* `algo`: Run and check IOp dedicated to offload small algorithms + + +Snippets below give some example of command that could be used for testing: +``` bash +# Correctly setup environment with setup_hpu.sh script +source setup_hpu.sh --config v80 --init-qdma + +# Run all sub-kind for 64b integer width +cargo test --release --features="hpu-v80" --test hpu -- u64 + +# Run only `bitwise` sub-kind for all integer width IOp +cargo test --release --features="hpu-v80" --test hpu -- bitwise +``` + +## Benches framework +HPU is completely integrated in tfhe benchmark system. Performances results could be extracted from HighLevelApi or Integer Api. +Three benchmarks could be started, through the following Makefile target for simplicity: +``` bash +# Do not forget to correctly set environment before hand +source setup_hpu.sh --config v80 --init-qdma + +# Run hlapi benches +make test_high_level_api_hpu + +# Run hlapi erc20 benches +make bench_hlapi_erc20_hpu + +# Run integer level benches +make bench_integer_hpu +``` + +## Eager to start without real Hardware ? +You are still waiting your FPGA board and are frustrated by lead time ? +Don't worry, you have backed-up. A dedicated simulation infrastructure with accurate performance estimation is available in tfhe-rs. +You can use it on any linux/MacOs to test HPU integration within tfhe-rs and optimized your application for HPU target. +Simply through an eye to [Hpu mockup](../../mockups/tfhe-hpu-mockup/Reaadme.md), and follow the instruction. diff --git a/backends/tfhe-hpu-backend/build.rs b/backends/tfhe-hpu-backend/build.rs new file mode 100644 index 000000000..291897082 --- /dev/null +++ b/backends/tfhe-hpu-backend/build.rs @@ -0,0 +1,26 @@ +fn main() { + if cfg!(feature = "hw-xrt") { + println!("cargo:rustc-link-search=/opt/xilinx/xrt/lib"); + println!("cargo:rustc-link-lib=dylib=stdc++"); + println!("cargo:rustc-link-lib=dl"); + println!("cargo:rustc-link-lib=rt"); + println!("cargo:rustc-link-lib=uuid"); + println!("cargo:rustc-link-lib=dylib=xrt_coreutil"); + + cxx_build::bridge("src/ffi/xrt/mod.rs") + .file("src/ffi/xrt/cxx/hpu_hw.cc") + .file("src/ffi/xrt/cxx/mem_zone.cc") + .flag_if_supported("-std=c++23") + .include("/opt/xilinx/xrt/include") // Enhance: support parsing bash env instead of hard path + .flag("-fmessage-length=0") + .compile("hpu-hw-ffi"); + + println!("cargo:rerun-if-changed=src/ffi/xrt/mod.rs"); + println!("cargo:rerun-if-changed=src/ffi/xrt/cxx/hpu_hw.cc"); + println!("cargo:rerun-if-changed=src/ffi/xrt/cxx/hpu_hw.h"); + println!("cargo:rerun-if-changed=src/ffi/xrt/cxx/mem_zone.cc"); + println!("cargo:rerun-if-changed=src/ffi/xrt/cxx/mem_zone.h"); + } else { + // Simulation ffi -> nothing to do + } +} diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_0.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_0.asm new file mode 100644 index 000000000..838beed9e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_0.asm @@ -0,0 +1,15 @@ +# CUST_0 +# Simple IOp to check the xfer between Hpu/Cpu +# Construct constant in dest slot -> 249 (0xf9) +SUB R0 R0 R0 +ADDS R0 R0 1 +ST TD[0].0 R0 +SUB R1 R1 R1 +ADDS R1 R1 2 +ST TD[0].1 R1 +SUB R2 R2 R2 +ADDS R2 R2 3 +ST TD[0].2 R2 +SUB R3 R3 R3 +ADDS R3 R3 3 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_1.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_1.asm new file mode 100644 index 000000000..3679e2c5f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_1.asm @@ -0,0 +1,11 @@ +# CUST_1 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_a +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_10.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_10.asm new file mode 100644 index 000000000..f591d66b3 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_10.asm @@ -0,0 +1,25 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 + Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +ADD R4 R5 R6 +ST TD[0].2 R4 + +; Dst[0].2 <- Src[0].2 + Src[1].2 +LD R9 TS[0].2 +LD R10 TS[1].2 +ADD R8 R9 R10 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 + Src[1].3 +LD R13 TS[0].3 +LD R14 TS[1].3 +ADD R12 R13 R14 +ST TD[0].3 R0 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_16.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_16.asm new file mode 100644 index 000000000..0b4cfe80f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_16.asm @@ -0,0 +1,6 @@ +# CUST_16 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a.0) +LD R0 TS[0].0 +PBS_F R0 R0 PbsNone +ST TD[0].0 R0 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_17.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_17.asm new file mode 100644 index 000000000..bdb6711a7 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_17.asm @@ -0,0 +1,15 @@ +# CUST_17 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a) +LD R0 TS[0].0 +PBS R0 R0 PbsNone +ST TD[0].0 R0 +LD R1 TS[0].1 +PBS R1 R1 PbsNone +ST TD[0].1 R1 +LD R2 TS[0].2 +PBS R2 R2 PbsNone +ST TD[0].2 R2 +LD R3 TS[0].3 +PBS_F R3 R3 PbsNone +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_18.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_18.asm new file mode 100644 index 000000000..c4b9a46a0 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_18.asm @@ -0,0 +1,23 @@ +; CUST_18 +; Simple IOp to check extraction pattern +; Correct result: +; * Dst[0,1] <- Src[0][0,1] +; * Dst[2,3] <- Src[1][0,1] + +; Pack Src[0][0,1] with a Mac and extract Carry/Msg in Dst[0][0,1] +LD R0 TS[0].0 +LD R1 TS[0].1 +MAC R3 R1 R0 4 +PBS R4 R3 PbsMsgOnly +PBS R5 R3 PbsCarryInMsg +ST TD[0].0 R4 +ST TD[0].1 R5 + +; Pack Src[1][0,1] with a Mac and extract Carry/Msg in Dst[0][2,3] +LD R10 TS[1].0 +LD R11 TS[1].1 +MAC R13 R11 R10 4 +PBS R14 R13 PbsMsgOnly +PBS R15 R13 PbsCarryInMsg +ST TD[0].2 R14 +ST TD[0].3 R15 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_19.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_19.asm new file mode 100644 index 000000000..0974347fa --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_19.asm @@ -0,0 +1,19 @@ +; CUST_19 +; Simple IOp to check PbsMl2 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- 0 +; * Dst[0][2] <- Src[0][0] +1 +; * Dst[0][3] <- 0 +; i.e Cust_19(0x2) => 0x32 + +; Construct a 0 for destination padding +SUB R16 R16 R16 + +; Apply PbsMl2 on Src[0] result goes in dest[0][0-3] (0-padded) +LD R0 TS[0].0 +PBS_ML2_F R0 R0 PbsTestMany2 +ST TD[0].0 R0 +ST TD[0].1 R16 +ST TD[0].2 R1 +ST TD[0].3 R16 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_2.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_2.asm new file mode 100644 index 000000000..bc8e0175e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_2.asm @@ -0,0 +1,11 @@ +# CUST_2 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_b +LD R0 TS[1].0 +LD R1 TS[1].1 +LD R2 TS[1].2 +LD R3 TS[1].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_20.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_20.asm new file mode 100644 index 000000000..5f29f8ee5 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_20.asm @@ -0,0 +1,22 @@ +; CUST_20 +; Simple IOp to check PbsMl4 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; i.e Cust_20(0x0) => 0xe4 + +SUB R16 R16 R16 +ST TD[0].0 R0 +ST TD[0].1 R0 +ST TD[0].2 R0 +ST TD[0].3 R0 + +; Apply PbsMl4 on Src[0] result goes in dest[0][0-3] +LD R0 TS[0].0 +PBS_ML4_F R0 R0 PbsTestMany4 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_21.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_21.asm new file mode 100644 index 000000000..5a601bbe6 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_21.asm @@ -0,0 +1,24 @@ +; CUST_21 +; Simple IOp to check PbsMl8 +; WARN: This operation required 16b ct width +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; * Dst[0][4] <- Src[0][0] +4 +; * Dst[0][5] <- Src[0][0] +5 +; * Dst[0][6] <- Src[0][0] +6 +; * Dst[0][7] <- Src[0][0] +7 + +; Apply PbsMl8 on Src[0] result goes in dest[0][0-7] +LD R0 TS[0].0 +PBS_ML8_F R0 R0 PbsTestMany8 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 +ST TD[0].4 R4 +ST TD[0].5 R5 +ST TD[0].6 R6 +ST TD[0].7 R7 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_3.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_3.asm new file mode 100644 index 000000000..d13ca243c --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_3.asm @@ -0,0 +1,16 @@ +# CUST_3 +# Simple IOp to check isc behavior +# Generate obvious deps and check that isc correctly issued the dop +# Correct result must bu Dest <- Src[0] +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +PBS R4 R0 PbsNone +ST TD[0].0 R4 +PBS R4 R1 PbsNone +ST TD[0].1 R4 +PBS R4 R2 PbsNone +ST TD[0].2 R4 +PBS_F R4 R3 PbsNone +ST TD[0].3 R4 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_8.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_8.asm new file mode 100644 index 000000000..c02eee9cd --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_8.asm @@ -0,0 +1,19 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +SUB R4 R5 R6 +ST TD[0].1 R4 + +; Dst[0].2 <- Src[0].2 + (Src[1].2 *4) +LD R9 TS[0].2 +LD R10 TS[1].2 +MAC R8 R9 R10 4 +ST TD[0].2 R8 diff --git a/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_9.asm b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_9.asm new file mode 100644 index 000000000..5e5cc4129 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/custom_iop/cust_9.asm @@ -0,0 +1,21 @@ +; CUST_9 +; Simple IOp to check the ALU Scalar operation +; Dst[0].0 <- Src[0].0 + Imm[0].0 +LD R1 TS[0].0 +ADDS R0 R1 TI[0].0 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Imm[0].1 +LD R5 TS[0].1 +SUBS R4 R5 TI[0].1 +ST TD[0].1 R4 + +; Dst[0].2 <- Imm[0].2 - Src[0].2 +LD R9 TS[0].2 +SSUB R8 R9 TI[0].2 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 * Imm[0].3 +LD R13 TS[0].3 +MULS R12 R13 TI[0].3 +ST TD[0].3 R12 diff --git a/backends/tfhe-hpu-backend/config_store/sim/hpu_config.toml b/backends/tfhe-hpu-backend/config_store/sim/hpu_config.toml new file mode 100644 index 000000000..80e7a4827 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/hpu_config.toml @@ -0,0 +1,108 @@ + +[fpga] + regmap=["${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_3in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_3in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/tb_hpu_regif_dummy.toml"] + polling_us=100000 +[fpga.ffi.Sim] + ipc_name="/tmp/${USER}/hpu_mockup_ipc" + +[rtl] + bpip_use = true + bpip_use_opportunism = true + bpip_timeout = 100_000 + +[board] + ct_mem = 32768 + ct_pc = [ + {Hbm= {pc=32}}, + {Hbm= {pc=33}}, + ] + heap_size = 16384 + + lut_mem = 256 + lut_pc = {Hbm={pc=34}} + + fw_size= 16777215 # i.e. 16 MiB + fw_pc = {Ddr= {offset= 0x3900_0000}} # NB: Allocation must take place in the Discret DDR + + bsk_pc = [ + {Hbm={pc=8}}, + {Hbm={pc=12}}, + {Hbm={pc=24}}, + {Hbm={pc=28}}, + {Hbm={pc=40}}, + {Hbm={pc=44}}, + {Hbm={pc=56}}, + {Hbm={pc=60}} + ] + + ksk_pc = [ + {Hbm={pc=0}}, + {Hbm={pc=1}}, + {Hbm={pc=2}}, + {Hbm={pc=3}}, + {Hbm={pc=4}}, + {Hbm={pc=5}}, + {Hbm={pc=6}}, + {Hbm={pc=7}}, + {Hbm={pc=16}}, + {Hbm={pc=17}}, + {Hbm={pc=18}}, + {Hbm={pc=19}}, + {Hbm={pc=20}}, + {Hbm={pc=21}}, + {Hbm={pc=22}}, + {Hbm={pc=23}} + ] + + trace_pc = {Hbm={pc=35}} + trace_depth = 32 # In MB + +[firmware] + implementation = "Llt" + integer_w=[2,4,6,8,10,12,14,16,32,64,128] + min_batch_size = 11 + kogge_cfg = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/kogge_cfg.toml" + custom_iop.'IOP[0]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_0.asm" + custom_iop.'IOP[1]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_1.asm" + custom_iop.'IOP[2]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_2.asm" + custom_iop.'IOP[3]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_3.asm" + custom_iop.'IOP[8]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_8.asm" + custom_iop.'IOP[9]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_9.asm" + custom_iop.'IOP[16]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_16.asm" + custom_iop.'IOP[17]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_17.asm" + custom_iop.'IOP[18]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_18.asm" + custom_iop.'IOP[19]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_19.asm" + custom_iop.'IOP[20]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_20.asm" + custom_iop.'IOP[21]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_21.asm" + +[firmware.op_cfg.default] + fill_batch_fifo = true + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MUL] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MULS] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.ERC_20] + fill_batch_fifo = true + min_batch_size = false + use_tiers = true + flush_behaviour = "Patient" + flush = true diff --git a/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_1in3.toml b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_1in3.toml new file mode 100644 index 000000000..bfdb80263 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_1in3.toml @@ -0,0 +1,256 @@ +module_name="hpu_regif_core_cfg_1in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x00 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_cfg_1in3] +description="entry_cfg_1in3 section with known value used for debug." +offset= 0x0 + +[section.entry_cfg_1in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x01010101} + +[section.entry_cfg_1in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x11111111} + +[section.entry_cfg_1in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x21212121} + + +[section.entry_cfg_1in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x31313131} + +# ===================================================================================================================== +[section.info] +description="RTL architecture parameters" +offset= 0x10 + +[section.info.register.version] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="VERSION"} + +[section.info.register.ntt_architecture] + description="NTT architecture" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="NTT_CORE_ARCH"} + +[section.info.register.ntt_structure] + description="NTT structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix = { size_b=8, offset_b=0 , default={Param="R"}, description="NTT radix"} + field.psi = { size_b=8, offset_b=8 , default={Param="PSI"}, description="NTT psi"} + field.div = { size_b=8, offset_b=16, default={Param="BWD_PSI_DIV"}, description="NTT backward div"} + field.delta = { size_b=8, offset_b=24, default={Param="DELTA"}, description="NTT network delta (for wmm arch)"} + +[section.info.register.ntt_rdx_cut] + description="NTT radix cuts, in log2 unit (for gf64 arch)" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix_cut0 = { size_b=4, offset_b=0 , default={Param="NTT_RDX_CUT_S_0"}, description="NTT radix cut #0"} + field.radix_cut1 = { size_b=4, offset_b=4 , default={Param="NTT_RDX_CUT_S_1"}, description="NTT radix cut #1"} + field.radix_cut2 = { size_b=4, offset_b=8 , default={Param="NTT_RDX_CUT_S_2"}, description="NTT radix cut #2"} + field.radix_cut3 = { size_b=4, offset_b=12, default={Param="NTT_RDX_CUT_S_3"}, description="NTT radix cut #3"} + field.radix_cut4 = { size_b=4, offset_b=16, default={Param="NTT_RDX_CUT_S_4"}, description="NTT radix cut #4"} + field.radix_cut5 = { size_b=4, offset_b=20, default={Param="NTT_RDX_CUT_S_5"}, description="NTT radix cut #5"} + field.radix_cut6 = { size_b=4, offset_b=24, default={Param="NTT_RDX_CUT_S_6"}, description="NTT radix cut #6"} + field.radix_cut7 = { size_b=4, offset_b=28, default={Param="NTT_RDX_CUT_S_7"}, description="NTT radix cut #7"} + +[section.info.register.ntt_pbs] + description="Maximum number of PBS in the NTT pipeline" + owner="Parameter" + read_access="Read" + write_access="None" + field.batch_pbs_nb = { size_b=8, offset_b=0 , default={Param="BATCH_PBS_NB"}, description="Maximum number of PBS in the NTT pipe"} + field.total_pbs_nb = { size_b=8, offset_b=8 , default={Param="TOTAL_PBS_NB"}, description="Maximum number of PBS stored in PEP buffer"} + +[section.info.register.ntt_modulo] + description="Code associated to the NTT prime" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="MOD_NTT_NAME"} + +[section.info.register.application] + description="Code associated with the application" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="APPLICATION_NAME"} + +[section.info.register.ks_structure] + description="Key-switch structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.x = { size_b=8, offset_b=0 , default={Param="LBX"}, description="Number of coefficients on X dimension"} + field.y = { size_b=8, offset_b=8 , default={Param="LBY"}, description="Number of coefficients on Y dimension"} + field.z = { size_b=8, offset_b=16, default={Param="LBZ"}, description="Number of coefficients on Z dimension"} + +[section.info.register.ks_crypto_param] + description="Key-switch crypto parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.mod_ksk_w = { size_b=8, offset_b=0 , default={Param="MOD_KSK_W"}, description="Width of KSK modulo"} + field.ks_l = { size_b=8, offset_b=8 , default={Param="KS_L"}, description="Number of KS decomposition level"} + field.ks_b = { size_b=8, offset_b=16, default={Param="KS_B_W"}, description="Width of KS decomposition base"} + +[section.info.register.regf_structure] + description="Register file structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.reg_nb = { size_b=8, offset_b=0 , default={Param="REGF_REG_NB"}, description="Number of registers in regfile"} + field.coef_nb = { size_b=8, offset_b=8 , default={Param="REGF_COEF_NB"}, description="Number of coefficients at regfile interface"} + +[section.info.register.isc_structure] + description="Instruction scheduler structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.depth = { size_b=8, offset_b=0 , default={Param="ISC_DEPTH"}, description="Number of slots in ISC lookahead buffer."} + field.min_iop_size = { size_b=8, offset_b=8 , default={Param="MIN_IOP_SIZE"}, description="Minimum number of DOp per IOp to prevent sync_id overflow."} + +[section.info.register.pe_properties] + description="Processing elements parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.alu_nb = { size_b=8, offset_b=24 , default={Param="PEA_ALU_NB"}, description="Number of coefficients processed in parallel in pe_alu"} + field.pep_regf_period = { size_b=8, offset_b=16 , default={Param="PEP_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEP and regfile"} + field.pem_regf_period = { size_b=8, offset_b=8 , default={Param="PEM_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEM and regfile"} + field.pea_regf_period = { size_b=8, offset_b=0 , default={Param="PEA_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEA and regfile"} + +[section.info.register.bsk_structure] + description="BSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_cut_nb = { size_b=8, offset_b=8 , default={Param="BSK_CUT_NB"}, description="BSK cut nb"} + +[section.info.register.ksk_structure] + description="KSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.ksk_cut_nb = { size_b=8, offset_b=8 , default={Param="KSK_CUT_NB"}, description="KSK cut nb"} + +[section.info.register.hbm_axi4_nb] + description="Number of AXI4 connections to HBM" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_pc = { size_b=8, offset_b=0 , default={Param="BSK_PC"}, description="Number of HBM connections for BSK"} + field.ksk_pc = { size_b=8, offset_b=8, default={Param="KSK_PC"}, description="Number of HBM connections for KSK"} + field.pem_pc = { size_b=8, offset_b=16, default={Param="PEM_PC"}, description="Number of HBM connections for ciphertexts (PEM)"} + field.glwe_pc = { size_b=8, offset_b=24, default={Param="GLWE_PC"}, description="Number of HBM connections for GLWE"} + +[section.info.register.hbm_axi4_dataw_pem] + description="Ciphertext HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_PEM_DATA_W"} + +[section.info.register.hbm_axi4_dataw_glwe] + description="GLWE HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_GLWE_DATA_W"} + +[section.info.register.hbm_axi4_dataw_bsk] + description="BSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_BSK_DATA_W"} + +[section.info.register.hbm_axi4_dataw_ksk] + description="KSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_KSK_DATA_W"} + + +# ===================================================================================================================== +[section.hbm_axi4_addr_1in3] +offset= 0x1000 +description="HBM AXI4 connection address offset" + +[section.hbm_axi4_addr_1in3.register.ct] + description="Address offset for each ciphertext HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb","_pc1_lsb", "_pc1_msb"] + +[section.hbm_axi4_addr_1in3.register.glwe] + description="Address offset for each GLWE HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + + +[section.hbm_axi4_addr_1in3.register.ksk] + description="Address offset for each KSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb", "_pc8_lsb", "_pc8_msb", "_pc9_lsb", "_pc9_msb", "_pc10_lsb", "_pc10_msb", "_pc11_lsb", "_pc11_msb", "_pc12_lsb", "_pc12_msb", "_pc13_lsb", "_pc13_msb", "_pc14_lsb", "_pc14_msb", "_pc15_lsb", "_pc15_msb"] + + [section.hbm_axi4_addr_1in3.register.trc] + description="Address offset for each trace HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + +# ===================================================================================================================== +[section.bpip] +offset= 0x2000 +description="BPIP configuration" + +[section.bpip.register.use] + description="(1) Use BPIP mode, (0) use IPIP mode (default)" + owner="User" + read_access="Read" + write_access="Write" + field.use_bpip = { size_b=1, offset_b=0 , default={Cst=1}, description="use"} + field.use_opportunism = { size_b=1, offset_b=1 , default={Cst=0}, description="use opportunistic PBS flush"} + +[section.bpip.register.timeout] + description="Timeout for BPIP mode" + owner="User" + read_access="Read" + write_access="Write" + default={Cst=0xffffffff} diff --git a/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_3in3.toml b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_3in3.toml new file mode 100644 index 000000000..4afc095ab --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_cfg_3in3.toml @@ -0,0 +1,51 @@ +module_name="hpu_regif_core_cfg_3in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x20000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_cfg_3in3] +description="entry_cfg_3in3 section with known value used for debug." +offset= 0x0 + +[section.entry_cfg_3in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x03030303} + +[section.entry_cfg_3in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x13131313} + +[section.entry_cfg_3in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x23232323} + +[section.entry_cfg_3in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x33333333} + +# ===================================================================================================================== +[section.hbm_axi4_addr_3in3] +description="HBM AXI4 connection address offset" +offset= 0x10 + +[section.hbm_axi4_addr_3in3.register.bsk] + description="Address offset for each BSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb", "_pc8_lsb", "_pc8_msb", "_pc9_lsb", "_pc9_msb", "_pc10_lsb", "_pc10_msb", "_pc11_lsb", "_pc11_msb", "_pc12_lsb", "_pc12_msb", "_pc13_lsb", "_pc13_msb", "_pc14_lsb", "_pc14_msb", "_pc15_lsb", "_pc15_msb"] diff --git a/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_1in3.toml b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_1in3.toml new file mode 100644 index 000000000..ef20175f8 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_1in3.toml @@ -0,0 +1,336 @@ +module_name="hpu_regif_core_prc_1in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x10000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_prc_1in3] +description="entry_prc_1in3 section with known value used for debug." +offset= 0x0 + +[section.entry_prc_1in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x02020202} + +[section.entry_prc_1in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x12121212} + +[section.entry_prc_1in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x22222222} + +[section.entry_prc_1in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x32323232} + +# ===================================================================================================================== +[section.status_1in3] +description="HPU status of part 1in3" +offset= 0x10 + +[section.status_1in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 1in3"} + +# ===================================================================================================================== +[section.ksk_avail] +description="KSK availability configuration" +offset= 0x1000 + +[section.ksk_avail.register.avail] + description="KSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.ksk_avail.register.reset] + description="KSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.runtime_1in3] +description="Runtime information" +offset= 0x2000 + +[section.runtime_1in3.register.pep_cmux_loop] + description="PEP: CMUX iteration loop number" + owner="Kernel" + read_access="Read" + write_access="None" + field.br_loop = { size_b=15, offset_b=0 , default={Cst=0}, description="PBS current BR-loop"} + field.br_loop_c = { size_b=1, offset_b=15 , default={Cst=0}, description="PBS current BR-loop parity"} + field.ks_loop = { size_b=15, offset_b=16 , default={Cst=0}, description="KS current KS-loop"} + field.ks_loop_c = { size_b=1, offset_b=31 , default={Cst=0}, description="KS current KS-loop parity"} + +[section.runtime_1in3.register.pep_pointer_0] + description="PEP: pointers (part 1)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pool_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pool_rp"} + field.pool_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pool_wp"} + field.ldg_pt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ldg_pt"} + field.ldb_pt = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ldb_pt"} + +[section.runtime_1in3.register.pep_pointer_1] + description="PEP: pointers (part 2)" + owner="Kernel" + read_access="Read" + write_access="None" + field.ks_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP ks_in_rp"} + field.ks_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP ks_in_wp"} + field.ks_out_rp = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ks_out_rp"} + field.ks_out_wp = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ks_out_wp"} + +[section.runtime_1in3.register.pep_pointer_2] + description="PEP: pointers (part 3)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pbs_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pbs_in_rp"} + field.pbs_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pbs_in_wp"} + field.ipip_flush_last_pbs_in_loop = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP IPIP flush last pbs_in_loop"} + +[section.runtime_1in3.register.isc_latest_instruction] + description="ISC: 4 latest instructions received ([0] is the most recent)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_0","_1","_2","_3"] + +[section.runtime_1in3.register.pep_seq_bpip_batch_cnt] + description="PEP: BPIP batch counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_flush_cnt] + description="PEP: BPIP batch triggered by a flush counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_timeout_cnt] + description="PEP: BPIP batch triggered by a timeout counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_waiting_batch_cnt] + description="PEP: BPIP batch that waits the trigger counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_filling_cnt] + description="PEP: Count batch with filled with a given number of CT (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_1","_2","_3","_4","_5","_6","_7","_8","_9","_10","_11","_12","_13","_14","_15","_16"] + +[section.runtime_1in3.register.pep_seq_ld_ack_cnt] + description="PEP: load BLWE ack counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_cmux_not_full_batch_cnt] + description="PEP: not full batch CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_ipip_flush_cnt] + description="PEP: IPIP flush CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldb_rcp_dur] + description="PEP: load BLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_req_dur] + description="PEP: load GLWE request max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_rcp_dur] + description="PEP: load GLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_load_ksk_rcp_dur] + description="PEP: load KSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + + +[section.runtime_1in3.register.pep_mmacc_sxt_rcp_dur] + description="PEP: MMACC SXT reception duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_req_dur] + description="PEP: MMACC SXT request duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_cmd_wait_b_dur] + description="PEP: MMACC SXT command wait for b duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_inst_cnt] + description="PEP: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ack_cnt] + description="PEP: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_inst_cnt] + description="PEM: load input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_ack_cnt] + description="PEM: load instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_inst_cnt] + description="PEM: store input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_ack_cnt] + description="PEM: store instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_inst_cnt] + description="PEA: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_ack_cnt] + description="PEA: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_inst_cnt] + description="ISC: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_ack_cnt] + description="ISC: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_info_0] + description="PEM: load first data)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_0","_pc0_1","_pc0_2","_pc0_3","_pc1_0","_pc1_1","_pc1_2","_pc1_3"] + +[section.runtime_1in3.register.pem_load_info_1] + description="PEM: load first address" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_lsb","_pc0_msb","_pc1_lsb","_pc1_msb"] + +[section.runtime_1in3.register.pem_store_info_0] + description="PEM: store info 0)" + owner="Kernel" + read_access="Read" + write_access="None" + field.cmd_vld = { size_b=1, offset_b=0 , default={Cst=0}, description="PEM_ST cmd vld"} + field.cmd_rdy = { size_b=1, offset_b=1 , default={Cst=0}, description="PEM_ST cmd rdy"} + field.pem_regf_rd_req_vld = { size_b=1, offset_b=2 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_vld"} + field.pem_regf_rd_req_rdy = { size_b=1, offset_b=3 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_rdy"} + field.brsp_fifo_in_vld = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST brsp_fifo_in_vld"} + field.brsp_fifo_in_rdy = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST brsp_fifo_in_rdy"} + field.rcp_fifo_in_vld = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST rcp_fifo_in_vld"} + field.rcp_fifo_in_rdy = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST rcp_fifo_in_rdy"} + field.r2_axi_vld = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST r2_axi_vld"} + field.r2_axi_rdy = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST r2_axi_rdy"} + field.c0_enough_location = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST c0_enough_location"} + +[section.runtime_1in3.register.pem_store_info_1] + description="PEM: store info 1" + owner="Kernel" + read_access="Read" + write_access="None" + field.s0_cmd_vld = { size_b=4, offset_b=0 , default={Cst=0}, description="PEM_ST s0_cmd_vld"} + field.s0_cmd_rdy = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST s0_cmd_rdy"} + field.m_axi_bvalid = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST m_axi_bvalid"} + field.m_axi_bready = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST m_axi_bready"} + field.m_axi_wvalid = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST m_axi_wvalid"} + field.m_axi_wready = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST m_axi_wready"} + field.m_axi_awvalid = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST m_axi_awvalid"} + field.m_axi_awready = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST m_axi_awready"} + +[section.runtime_1in3.register.pem_store_info_2] + description="PEM: store info 2" + owner="Kernel" + read_access="Read" + write_access="None" + field.c0_free_loc_cnt = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST c0_free_loc_cnt"} + field.brsp_bresp_cnt = { size_b=16, offset_b=16 , default={Cst=0}, description="PEM_ST brsp_bresp_cnt"} + +[section.runtime_1in3.register.pem_store_info_3] + description="PEM: store info 3" + owner="Kernel" + read_access="Read" + write_access="None" + field.brsp_ack_seen = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST brsp_ack_seen"} + field.c0_cmd_cnt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEM_ST c0_cmd_cnt"} diff --git a/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_3in3.toml b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_3in3.toml new file mode 100644 index 000000000..627f140c1 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/hpu_regif_core_prc_3in3.toml @@ -0,0 +1,100 @@ +module_name="hpu_regif_core_prc_3in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x30000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_prc_3in3] +description="entry_prc_3in3 section with known value used for debug." +offset= 0x0 + +[section.entry_prc_3in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x04040404} + +[section.entry_prc_3in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x14141414} + +[section.entry_prc_3in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x24242424} + +[section.entry_prc_3in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x34343434} + +# ===================================================================================================================== +[section.status_3in3] +description="HPU status of parts 2in3 and 3in3" +offset= 0x10 + +[section.status_3in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 3in3"} + +# ===================================================================================================================== +[section.bsk_avail] +description="BSK availability configuration" +offset= 0x1000 + +[section.bsk_avail.register.avail] + description="BSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.bsk_avail.register.reset] + description="BSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.runtime_3in3] +description="Runtime information" +offset= 0x2000 + +[section.runtime_3in3.register.pep_load_bsk_rcp_dur] + description="PEP: load BSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + +[section.runtime_3in3.register.pep_bskif_req_info_0] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_br_loop_rp = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK read pointer"} + field.req_br_loop_wp = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK write pointer"} + +[section.runtime_3in3.register.pep_bskif_req_info_1] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_prf_br_loop = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK prefetch pointer"} + field.req_parity = { size_b=1, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK pointer parity"} + field.req_assigned = { size_b=1, offset_b=31 , default={Cst=0}, description="PEP BSK_IF requester assignment"} diff --git a/backends/tfhe-hpu-backend/config_store/sim/tb_hpu_regif_dummy.toml b/backends/tfhe-hpu-backend/config_store/sim/tb_hpu_regif_dummy.toml new file mode 100644 index 000000000..2777aaf78 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/sim/tb_hpu_regif_dummy.toml @@ -0,0 +1,22 @@ +module_name="tb_hpu_regif_dummy" +description="Fake registers needed by the mockup" +word_size_b = 32 +offset = 0x40000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ============================================================================== +[section.WorkAck] +description="Purpose of this section" + +[section.WorkAck.register.workq] + description="Insert work in workq and read status" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.WorkAck.register.ackq] + description="Pop ack from in ackq" + owner="Kernel" + read_access="ReadNotify" + write_access="None" diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/Readme.md b/backends/tfhe-hpu-backend/config_store/u55c_gf64/Readme.md new file mode 100644 index 000000000..2fbe5df9f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/Readme.md @@ -0,0 +1,6 @@ +# Fpga version + +Built with the following command: (i.e. xrt/run_syn_hpu_msplit_3parts_64b.sh) +``` +just zaxl-build hpu_msplit_3parts 3 "0:300" "-F TOP_MSPLIT TOP_MSPLIT_1 -F TOP_BATCH TOP_BATCH_TOPhpu_BPBS8_TPBS32 -F TOP_PCMAX TOP_PCMAX_pem2_glwe1_bsk8_ksk8 -F TOP_PC TOP_PC_pem2_glwe1_bsk4_ksk4 -F APPLICATION APPLI_msg2_carry2 -F NTT_MOD NTT_MOD_goldilocks -F NTT_CORE_ARCH NTT_CORE_ARCH_gf64 -F NTT_CORE_R_PSI NTT_CORE_R2_PSI16 -F NTT_CORE_RDX_CUT NTT_CORE_RDX_CUT_n5c5c1 -F NTT_CORE_DIV NTT_CORE_DIV_1 -F BSK_SLOT_CUT BSK_SLOT8_CUT4 -F KSK_SLOT_CUT KSK_SLOT8_CUT4 -F KSLB KSLB_x2y32z3 -F HPU_PART HPU_PART_gf64 -F AXI_DATA_W AXI_DATA_W_512" "1:${PROJECT_DIR}/hw/output/micro_code/ucore_fw.elf" 'D:MEMORY_FILE_PATH=\\\"${PROJECT_DIR}/hw/\\\"' | tee build_out.log +``` diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_0.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_0.asm new file mode 100644 index 000000000..838beed9e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_0.asm @@ -0,0 +1,15 @@ +# CUST_0 +# Simple IOp to check the xfer between Hpu/Cpu +# Construct constant in dest slot -> 249 (0xf9) +SUB R0 R0 R0 +ADDS R0 R0 1 +ST TD[0].0 R0 +SUB R1 R1 R1 +ADDS R1 R1 2 +ST TD[0].1 R1 +SUB R2 R2 R2 +ADDS R2 R2 3 +ST TD[0].2 R2 +SUB R3 R3 R3 +ADDS R3 R3 3 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_1.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_1.asm new file mode 100644 index 000000000..3679e2c5f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_1.asm @@ -0,0 +1,11 @@ +# CUST_1 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_a +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_10.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_10.asm new file mode 100644 index 000000000..f591d66b3 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_10.asm @@ -0,0 +1,25 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 + Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +ADD R4 R5 R6 +ST TD[0].2 R4 + +; Dst[0].2 <- Src[0].2 + Src[1].2 +LD R9 TS[0].2 +LD R10 TS[1].2 +ADD R8 R9 R10 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 + Src[1].3 +LD R13 TS[0].3 +LD R14 TS[1].3 +ADD R12 R13 R14 +ST TD[0].3 R0 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_16.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_16.asm new file mode 100644 index 000000000..0b4cfe80f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_16.asm @@ -0,0 +1,6 @@ +# CUST_16 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a.0) +LD R0 TS[0].0 +PBS_F R0 R0 PbsNone +ST TD[0].0 R0 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_17.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_17.asm new file mode 100644 index 000000000..bdb6711a7 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_17.asm @@ -0,0 +1,15 @@ +# CUST_17 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a) +LD R0 TS[0].0 +PBS R0 R0 PbsNone +ST TD[0].0 R0 +LD R1 TS[0].1 +PBS R1 R1 PbsNone +ST TD[0].1 R1 +LD R2 TS[0].2 +PBS R2 R2 PbsNone +ST TD[0].2 R2 +LD R3 TS[0].3 +PBS_F R3 R3 PbsNone +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_18.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_18.asm new file mode 100644 index 000000000..c4b9a46a0 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_18.asm @@ -0,0 +1,23 @@ +; CUST_18 +; Simple IOp to check extraction pattern +; Correct result: +; * Dst[0,1] <- Src[0][0,1] +; * Dst[2,3] <- Src[1][0,1] + +; Pack Src[0][0,1] with a Mac and extract Carry/Msg in Dst[0][0,1] +LD R0 TS[0].0 +LD R1 TS[0].1 +MAC R3 R1 R0 4 +PBS R4 R3 PbsMsgOnly +PBS R5 R3 PbsCarryInMsg +ST TD[0].0 R4 +ST TD[0].1 R5 + +; Pack Src[1][0,1] with a Mac and extract Carry/Msg in Dst[0][2,3] +LD R10 TS[1].0 +LD R11 TS[1].1 +MAC R13 R11 R10 4 +PBS R14 R13 PbsMsgOnly +PBS R15 R13 PbsCarryInMsg +ST TD[0].2 R14 +ST TD[0].3 R15 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_19.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_19.asm new file mode 100644 index 000000000..0974347fa --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_19.asm @@ -0,0 +1,19 @@ +; CUST_19 +; Simple IOp to check PbsMl2 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- 0 +; * Dst[0][2] <- Src[0][0] +1 +; * Dst[0][3] <- 0 +; i.e Cust_19(0x2) => 0x32 + +; Construct a 0 for destination padding +SUB R16 R16 R16 + +; Apply PbsMl2 on Src[0] result goes in dest[0][0-3] (0-padded) +LD R0 TS[0].0 +PBS_ML2_F R0 R0 PbsTestMany2 +ST TD[0].0 R0 +ST TD[0].1 R16 +ST TD[0].2 R1 +ST TD[0].3 R16 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_2.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_2.asm new file mode 100644 index 000000000..bc8e0175e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_2.asm @@ -0,0 +1,11 @@ +# CUST_2 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_b +LD R0 TS[1].0 +LD R1 TS[1].1 +LD R2 TS[1].2 +LD R3 TS[1].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_20.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_20.asm new file mode 100644 index 000000000..5f29f8ee5 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_20.asm @@ -0,0 +1,22 @@ +; CUST_20 +; Simple IOp to check PbsMl4 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; i.e Cust_20(0x0) => 0xe4 + +SUB R16 R16 R16 +ST TD[0].0 R0 +ST TD[0].1 R0 +ST TD[0].2 R0 +ST TD[0].3 R0 + +; Apply PbsMl4 on Src[0] result goes in dest[0][0-3] +LD R0 TS[0].0 +PBS_ML4_F R0 R0 PbsTestMany4 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_21.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_21.asm new file mode 100644 index 000000000..5a601bbe6 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_21.asm @@ -0,0 +1,24 @@ +; CUST_21 +; Simple IOp to check PbsMl8 +; WARN: This operation required 16b ct width +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; * Dst[0][4] <- Src[0][0] +4 +; * Dst[0][5] <- Src[0][0] +5 +; * Dst[0][6] <- Src[0][0] +6 +; * Dst[0][7] <- Src[0][0] +7 + +; Apply PbsMl8 on Src[0] result goes in dest[0][0-7] +LD R0 TS[0].0 +PBS_ML8_F R0 R0 PbsTestMany8 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 +ST TD[0].4 R4 +ST TD[0].5 R5 +ST TD[0].6 R6 +ST TD[0].7 R7 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_3.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_3.asm new file mode 100644 index 000000000..d13ca243c --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_3.asm @@ -0,0 +1,16 @@ +# CUST_3 +# Simple IOp to check isc behavior +# Generate obvious deps and check that isc correctly issued the dop +# Correct result must bu Dest <- Src[0] +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +PBS R4 R0 PbsNone +ST TD[0].0 R4 +PBS R4 R1 PbsNone +ST TD[0].1 R4 +PBS R4 R2 PbsNone +ST TD[0].2 R4 +PBS_F R4 R3 PbsNone +ST TD[0].3 R4 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_8.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_8.asm new file mode 100644 index 000000000..c02eee9cd --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_8.asm @@ -0,0 +1,19 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +SUB R4 R5 R6 +ST TD[0].1 R4 + +; Dst[0].2 <- Src[0].2 + (Src[1].2 *4) +LD R9 TS[0].2 +LD R10 TS[1].2 +MAC R8 R9 R10 4 +ST TD[0].2 R8 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_9.asm b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_9.asm new file mode 100644 index 000000000..5e5cc4129 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/custom_iop/cust_9.asm @@ -0,0 +1,21 @@ +; CUST_9 +; Simple IOp to check the ALU Scalar operation +; Dst[0].0 <- Src[0].0 + Imm[0].0 +LD R1 TS[0].0 +ADDS R0 R1 TI[0].0 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Imm[0].1 +LD R5 TS[0].1 +SUBS R4 R5 TI[0].1 +ST TD[0].1 R4 + +; Dst[0].2 <- Imm[0].2 - Src[0].2 +LD R9 TS[0].2 +SSUB R8 R9 TI[0].2 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 * Imm[0].3 +LD R13 TS[0].3 +MULS R12 R13 TI[0].3 +ST TD[0].3 R12 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_config.toml b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_config.toml new file mode 100644 index 000000000..b5d460a1a --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_config.toml @@ -0,0 +1,98 @@ + +[fpga] + regmap=["${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core.toml"] + polling_us=10 +[fpga.ffi.Xrt] + id= 0 + kernel= "hpu_msplit_3parts_1in3" + xclbin="${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_msplit_3parts.xclbin" + +[rtl] + bpip_use = true + bpip_use_opportunism = true + bpip_timeout = 100_000 + +[board] + ct_mem = 4096 + ct_pc = [ + {Hbm= {pc=10}}, + {Hbm= {pc=11}}, + ] + heap_size = 3584 + + lut_mem = 256 + lut_pc = {Hbm={pc=12}} + + fw_size= 65536 + fw_pc = {Hbm={pc=1}} + + bsk_pc = [ + {Hbm={pc=2}}, + {Hbm={pc=3}}, + {Hbm={pc=4}}, + {Hbm={pc=5}}, + {Hbm={pc=6}}, + {Hbm={pc=7}}, + {Hbm={pc=8}}, + {Hbm={pc=9}} + ] + + ksk_pc = [ + {Hbm={pc=24}}, + {Hbm={pc=25}}, + {Hbm={pc=26}}, + {Hbm={pc=27}}, + {Hbm={pc=28}}, + {Hbm={pc=29}}, + {Hbm={pc=30}}, + {Hbm={pc=31}} + ] + + trace_pc = {Hbm={pc=0}} + trace_depth = 4 # In MB + +[firmware] + implementation = "Llt" + integer_w=[4,6,8,10,12,14,16,32,64,128] + min_batch_size = 6 + kogge_cfg = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/kogge_cfg.toml" + custom_iop.'IOP[0]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_0.asm" + custom_iop.'IOP[1]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_1.asm" + custom_iop.'IOP[2]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_2.asm" + custom_iop.'IOP[3]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_3.asm" + custom_iop.'IOP[8]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_8.asm" + custom_iop.'IOP[9]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_9.asm" + custom_iop.'IOP[16]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_16.asm" + custom_iop.'IOP[17]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_17.asm" + custom_iop.'IOP[18]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_18.asm" + custom_iop.'IOP[19]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_19.asm" + custom_iop.'IOP[20]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_20.asm" + custom_iop.'IOP[21]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_21.asm" + +[firmware.op_cfg.default] + fill_batch_fifo = true + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MUL] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MULS] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.ERC_20] + fill_batch_fifo = false + min_batch_size = true + use_tiers = true + flush_behaviour = "Patient" + flush = true diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin new file mode 100644 index 000000000..604d76461 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35ad67cf9760e37256a6c92cf29ea67334690b724fd3b7b859919ee9b0bde6d3 +size 78194785 diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.info b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.info new file mode 100644 index 000000000..cbaa8cba5 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.info @@ -0,0 +1,1550 @@ + +============================================================================== +XRT Build Version: 2.16.0 (Vitis) + Build Date: 2023-07-13 16:00:55 + Hash ID: 157faa07876c55bb8aa8ec51b28608a6a0f6638e +============================================================================== +xclbin Information +------------------ + Generated by: v++ (2024.1) on 2024-05-20-23:21:20 + Version: 2.16.0 + Kernels: hpu_msplit_3parts_3in3, hpu_msplit_3parts_2in3, hpu_msplit_3parts_1in3 + Signature: + Content: Bitstream + UUID (xclbin): d0bcc1ed-d380-c9cf-175f-c059794490d4 + UUID (IINTF): b7ac1abe1e3e1cb686d5a81232452676 + Sections: BITSTREAM, MEM_TOPOLOGY, IP_LAYOUT, CONNECTIVITY, + CLOCK_FREQ_TOPOLOGY, BUILD_METADATA, + EMBEDDED_METADATA, SYSTEM_METADATA, + PARTITION_METADATA, GROUP_CONNECTIVITY, GROUP_TOPOLOGY +============================================================================== +Hardware Platform (Shell) Information +------------------------------------- + Vendor: xilinx + Board: u55c + Name: gen3x16_xdma_3 + Version: 202210.1 + Generated Version: Vivado 2022.1 (SW Build: 3513633) + Created: + Fri Apr 1 11:16:28 2022 FPGA Device: xcu55c + Board Vendor: xilinx.com + Board Name: xilinx.com:au55c:1.0 + Board Part: xilinx.com:au55c:part0:1.0 + Platform VBNV: xilinx_u55c_gen3x16_xdma_3_202210_1 + Static UUID: b7ac1abe-1e3e-1cb6-86d5-a81232452676 + Feature ROM TimeStamp: 0 + +Scalable Clocks +--------------- + Name: hbm_aclk + Index: 0 + Type: SYSTEM + Frequency: 450 MHz + + Name: KERNEL_CLK + Index: 1 + Type: KERNEL + Frequency: 500 MHz + + Name: DATA_CLK + Index: 2 + Type: DATA + Frequency: 300 MHz + +System Clocks +------ + Name: ulp_ucs_aclk_kernel_00 + Type: SCALABLE + Default Freq: 300 MHz + Requested Freq: 300 MHz + Achieved Freq: 300 MHz + + Name: ulp_ucs_aclk_kernel_01 + Type: SCALABLE + Default Freq: 500 MHz + Requested Freq: 500 MHz + Achieved Freq: 500 MHz + + Name: _bd_top_blp_s_aclk_freerun_ref_00 + Type: FIXED + Default Freq: 100 MHz + +Memory Configuration +-------------------- + Name: HBM[0] + Index: 0 + Type: MEM_HBM + Base Address: 0x0 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[1] + Index: 1 + Type: MEM_DRAM + Base Address: 0x20000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[2] + Index: 2 + Type: MEM_DRAM + Base Address: 0x40000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[3] + Index: 3 + Type: MEM_DRAM + Base Address: 0x60000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[4] + Index: 4 + Type: MEM_DRAM + Base Address: 0x80000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[5] + Index: 5 + Type: MEM_DRAM + Base Address: 0xa0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[6] + Index: 6 + Type: MEM_DRAM + Base Address: 0xc0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[7] + Index: 7 + Type: MEM_DRAM + Base Address: 0xe0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[8] + Index: 8 + Type: MEM_DRAM + Base Address: 0x100000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[9] + Index: 9 + Type: MEM_DRAM + Base Address: 0x120000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[10] + Index: 10 + Type: MEM_DRAM + Base Address: 0x140000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[11] + Index: 11 + Type: MEM_DRAM + Base Address: 0x160000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[12] + Index: 12 + Type: MEM_DRAM + Base Address: 0x180000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[13] + Index: 13 + Type: MEM_DRAM + Base Address: 0x1a0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[14] + Index: 14 + Type: MEM_DRAM + Base Address: 0x1c0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[15] + Index: 15 + Type: MEM_DRAM + Base Address: 0x1e0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[16] + Index: 16 + Type: MEM_DRAM + Base Address: 0x200000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[17] + Index: 17 + Type: MEM_DRAM + Base Address: 0x220000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[18] + Index: 18 + Type: MEM_DRAM + Base Address: 0x240000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[19] + Index: 19 + Type: MEM_DRAM + Base Address: 0x260000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[20] + Index: 20 + Type: MEM_DRAM + Base Address: 0x280000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[21] + Index: 21 + Type: MEM_DRAM + Base Address: 0x2a0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[22] + Index: 22 + Type: MEM_DRAM + Base Address: 0x2c0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[23] + Index: 23 + Type: MEM_DRAM + Base Address: 0x2e0000000 + Address Size: 0x20000000 + Bank Used: No + + Name: HBM[24] + Index: 24 + Type: MEM_DRAM + Base Address: 0x300000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[25] + Index: 25 + Type: MEM_DRAM + Base Address: 0x320000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[26] + Index: 26 + Type: MEM_DRAM + Base Address: 0x340000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[27] + Index: 27 + Type: MEM_DRAM + Base Address: 0x360000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[28] + Index: 28 + Type: MEM_DRAM + Base Address: 0x380000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[29] + Index: 29 + Type: MEM_DRAM + Base Address: 0x3a0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[30] + Index: 30 + Type: MEM_DRAM + Base Address: 0x3c0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: HBM[31] + Index: 31 + Type: MEM_DRAM + Base Address: 0x3e0000000 + Address Size: 0x20000000 + Bank Used: Yes + + Name: PLRAM[0] + Index: 32 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: PLRAM[1] + Index: 33 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: PLRAM[2] + Index: 34 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: PLRAM[3] + Index: 35 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: PLRAM[4] + Index: 36 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: PLRAM[5] + Index: 37 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: HOST[0] + Index: 38 + Type: MEM_DRAM + Base Address: 0x0 + Address Size: 0x0 + Bank Used: No + + Name: dc_0 + Index: 39 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_1 + Index: 40 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_2 + Index: 41 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_3 + Index: 42 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_4 + Index: 43 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_5 + Index: 44 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_6 + Index: 45 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_7 + Index: 46 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_8 + Index: 47 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_9 + Index: 48 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_10 + Index: 49 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_11 + Index: 50 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_12 + Index: 51 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_13 + Index: 52 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_14 + Index: 53 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_15 + Index: 54 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_16 + Index: 55 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_17 + Index: 56 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_18 + Index: 57 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_19 + Index: 58 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_20 + Index: 59 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_21 + Index: 60 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_22 + Index: 61 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_23 + Index: 62 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_24 + Index: 63 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_25 + Index: 64 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_26 + Index: 65 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_27 + Index: 66 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes + + Name: dc_28 + Index: 67 + Type: MEM_STREAMING_CONNECTION + Base Address: 0x0 + Address Size: 0x0 + Bank Used: Yes +============================================================================== +Kernel: hpu_msplit_3parts_3in3 + +Definition +---------- + Signature: hpu_msplit_3parts_3in3 (void* M_AXI_BSK_0_PTR, void* M_AXI_BSK_1_PTR, void* M_AXI_BSK_2_PTR, void* M_AXI_BSK_3_PTR, void* M_AXI_BSK_4_PTR, void* M_AXI_BSK_5_PTR, void* M_AXI_BSK_6_PTR, void* M_AXI_BSK_7_PTR, void* axis_p2_p3_batch, void* axis_p2_p3_bsk_c, void* axis_p2_p3_proc_c, void* axis_p2_p3_proc_d0, void* axis_p2_p3_proc_d1, void* axis_p2_p3_proc_d2, void* axis_p2_p3_proc_d3, void* axis_p3_p2_bsk_c, void* axis_p3_p2_proc_c, void* axis_p3_p2_proc_d0, void* axis_p3_p2_proc_d1, void* axis_p3_p2_proc_d2, void* axis_p3_p2_proc_d3, void* axis_p3_p2_side) + +Ports +----- + Port: axis_p2_p3_batch + Mode: read_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p2_p3_bsk_c + Mode: read_only + Range (bytes): + Data Width: 520 bits + Port Type: stream + + Port: axis_p2_p3_proc_c + Mode: read_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p3_proc_d0 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d1 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d2 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d3 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_bsk_c + Mode: write_only + Range (bytes): + Data Width: 8 bits + Port Type: stream + + Port: axis_p3_p2_proc_c + Mode: write_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p3_p2_proc_d0 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d1 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d2 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d3 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_side + Mode: write_only + Range (bytes): + Data Width: 256 bits + Port Type: stream + + Port: m_axi_bsk_0 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_1 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_2 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_3 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_4 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_5 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_6 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_bsk_7 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: s_axil + Mode: slave + Range (bytes): 0x10000 + Data Width: 32 bits + Port Type: addressable + +-------------------------- +Instance: hpu_msplit_3parts_3in3_1 + Base Address: 0x1000000 + + Argument: M_AXI_BSK_0_PTR + Register Offset: 0x10 + Port: m_axi_bsk_0 + Memory: HBM[2] (MEM_DRAM) + + Argument: M_AXI_BSK_1_PTR + Register Offset: 0x14 + Port: m_axi_bsk_1 + Memory: HBM[3] (MEM_DRAM) + + Argument: M_AXI_BSK_2_PTR + Register Offset: 0x18 + Port: m_axi_bsk_2 + Memory: HBM[4] (MEM_DRAM) + + Argument: M_AXI_BSK_3_PTR + Register Offset: 0x1c + Port: m_axi_bsk_3 + Memory: HBM[5] (MEM_DRAM) + + Argument: M_AXI_BSK_4_PTR + Register Offset: 0x20 + Port: m_axi_bsk_4 + Memory: HBM[6] (MEM_DRAM) + + Argument: M_AXI_BSK_5_PTR + Register Offset: 0x24 + Port: m_axi_bsk_5 + Memory: HBM[7] (MEM_DRAM) + + Argument: M_AXI_BSK_6_PTR + Register Offset: 0x28 + Port: m_axi_bsk_6 + Memory: HBM[8] (MEM_DRAM) + + Argument: M_AXI_BSK_7_PTR + Register Offset: 0x2c + Port: m_axi_bsk_7 + Memory: HBM[9] (MEM_DRAM) + + Argument: axis_p2_p3_batch + Register Offset: 0x0 + Port: axis_p2_p3_batch + Memory: dc_15 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_bsk_c + Register Offset: 0x0 + Port: axis_p2_p3_bsk_c + Memory: dc_16 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_c + Register Offset: 0x0 + Port: axis_p2_p3_proc_c + Memory: dc_17 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d0 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d0 + Memory: dc_18 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d1 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d1 + Memory: dc_19 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d2 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d2 + Memory: dc_20 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d3 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d3 + Memory: dc_21 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_bsk_c + Register Offset: 0x0 + Port: axis_p3_p2_bsk_c + Memory: dc_22 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_c + Register Offset: 0x0 + Port: axis_p3_p2_proc_c + Memory: dc_23 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d0 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d0 + Memory: dc_24 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d1 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d1 + Memory: dc_25 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d2 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d2 + Memory: dc_26 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d3 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d3 + Memory: dc_27 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_side + Register Offset: 0x0 + Port: axis_p3_p2_side + Memory: dc_28 (MEM_STREAMING_CONNECTION) +Kernel: hpu_msplit_3parts_2in3 + +Definition +---------- + Signature: hpu_msplit_3parts_2in3 (void* axis_p1_p2_batch, void* axis_p1_p2_bsk_c, void* axis_p1_p2_ldg_c, void* axis_p1_p2_ldg_d, void* axis_p1_p2_mmacc_side, void* axis_p1_p2_mmfeed_c, void* axis_p1_p2_mmfeed_d0, void* axis_p1_p2_mmfeed_d1, void* axis_p1_p2_mmsxt_c, void* axis_p2_p1_bsk_c, void* axis_p2_p1_mmacc_c, void* axis_p2_p1_mmacc_d0, void* axis_p2_p1_mmacc_side, void* axis_p2_p1_mmsxt_d0, void* axis_p2_p1_side, void* axis_p2_p3_batch, void* axis_p2_p3_bsk_c, void* axis_p2_p3_proc_c, void* axis_p2_p3_proc_d0, void* axis_p2_p3_proc_d1, void* axis_p2_p3_proc_d2, void* axis_p2_p3_proc_d3, void* axis_p3_p2_bsk_c, void* axis_p3_p2_proc_c, void* axis_p3_p2_proc_d0, void* axis_p3_p2_proc_d1, void* axis_p3_p2_proc_d2, void* axis_p3_p2_proc_d3, void* axis_p3_p2_side) + +Ports +----- + Port: axis_p1_p2_batch + Mode: read_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p1_p2_bsk_c + Mode: read_only + Range (bytes): + Data Width: 520 bits + Port Type: stream + + Port: axis_p1_p2_ldg_c + Mode: read_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p1_p2_ldg_d + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmacc_side + Mode: read_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_c + Mode: read_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_d0 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_d1 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmsxt_c + Mode: read_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p2_p1_bsk_c + Mode: write_only + Range (bytes): + Data Width: 8 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_c + Mode: write_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_d0 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_side + Mode: write_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p1_mmsxt_d0 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p1_side + Mode: write_only + Range (bytes): + Data Width: 256 bits + Port Type: stream + + Port: axis_p2_p3_batch + Mode: write_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p2_p3_bsk_c + Mode: write_only + Range (bytes): + Data Width: 520 bits + Port Type: stream + + Port: axis_p2_p3_proc_c + Mode: write_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p3_proc_d0 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d1 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d2 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p3_proc_d3 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_bsk_c + Mode: read_only + Range (bytes): + Data Width: 8 bits + Port Type: stream + + Port: axis_p3_p2_proc_c + Mode: read_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p3_p2_proc_d0 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d1 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d2 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_proc_d3 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p3_p2_side + Mode: read_only + Range (bytes): + Data Width: 256 bits + Port Type: stream + +-------------------------- +Instance: hpu_msplit_3parts_2in3_1 + Base Address: not_used + + Argument: axis_p1_p2_batch + Register Offset: 0x0 + Port: axis_p1_p2_batch + Memory: dc_0 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_bsk_c + Register Offset: 0x0 + Port: axis_p1_p2_bsk_c + Memory: dc_1 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_ldg_c + Register Offset: 0x0 + Port: axis_p1_p2_ldg_c + Memory: dc_2 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_ldg_d + Register Offset: 0x0 + Port: axis_p1_p2_ldg_d + Memory: dc_3 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmacc_side + Register Offset: 0x0 + Port: axis_p1_p2_mmacc_side + Memory: dc_4 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_c + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_c + Memory: dc_5 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_d0 + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_d0 + Memory: dc_6 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_d1 + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_d1 + Memory: dc_7 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmsxt_c + Register Offset: 0x0 + Port: axis_p1_p2_mmsxt_c + Memory: dc_8 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_bsk_c + Register Offset: 0x0 + Port: axis_p2_p1_bsk_c + Memory: dc_9 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_c + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_c + Memory: dc_10 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_d0 + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_d0 + Memory: dc_11 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_side + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_side + Memory: dc_12 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmsxt_d0 + Register Offset: 0x0 + Port: axis_p2_p1_mmsxt_d0 + Memory: dc_13 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_side + Register Offset: 0x0 + Port: axis_p2_p1_side + Memory: dc_14 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_batch + Register Offset: 0x0 + Port: axis_p2_p3_batch + Memory: dc_15 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_bsk_c + Register Offset: 0x0 + Port: axis_p2_p3_bsk_c + Memory: dc_16 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_c + Register Offset: 0x0 + Port: axis_p2_p3_proc_c + Memory: dc_17 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d0 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d0 + Memory: dc_18 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d1 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d1 + Memory: dc_19 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d2 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d2 + Memory: dc_20 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p3_proc_d3 + Register Offset: 0x0 + Port: axis_p2_p3_proc_d3 + Memory: dc_21 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_bsk_c + Register Offset: 0x0 + Port: axis_p3_p2_bsk_c + Memory: dc_22 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_c + Register Offset: 0x0 + Port: axis_p3_p2_proc_c + Memory: dc_23 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d0 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d0 + Memory: dc_24 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d1 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d1 + Memory: dc_25 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d2 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d2 + Memory: dc_26 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_proc_d3 + Register Offset: 0x0 + Port: axis_p3_p2_proc_d3 + Memory: dc_27 (MEM_STREAMING_CONNECTION) + + Argument: axis_p3_p2_side + Register Offset: 0x0 + Port: axis_p3_p2_side + Memory: dc_28 (MEM_STREAMING_CONNECTION) +Kernel: hpu_msplit_3parts_1in3 + +Definition +---------- + Signature: hpu_msplit_3parts_1in3 (void* M_AXI_UCORE_PTR, void* M_AXI_TRC_PTR, void* M_AXI_PEM_0_PTR, void* M_AXI_PEM_1_PTR, void* M_AXI_GLWE_0_PTR, void* M_AXI_KSK_0_PTR, void* M_AXI_KSK_1_PTR, void* M_AXI_KSK_2_PTR, void* M_AXI_KSK_3_PTR, void* M_AXI_KSK_4_PTR, void* M_AXI_KSK_5_PTR, void* M_AXI_KSK_6_PTR, void* M_AXI_KSK_7_PTR, void* axis_p1_p2_batch, void* axis_p1_p2_bsk_c, void* axis_p1_p2_ldg_c, void* axis_p1_p2_ldg_d, void* axis_p1_p2_mmacc_side, void* axis_p1_p2_mmfeed_c, void* axis_p1_p2_mmfeed_d0, void* axis_p1_p2_mmfeed_d1, void* axis_p1_p2_mmsxt_c, void* axis_p2_p1_bsk_c, void* axis_p2_p1_mmacc_c, void* axis_p2_p1_mmacc_d0, void* axis_p2_p1_mmacc_side, void* axis_p2_p1_mmsxt_d0, void* axis_p2_p1_side) + +Ports +----- + Port: axis_p1_p2_batch + Mode: write_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p1_p2_bsk_c + Mode: write_only + Range (bytes): + Data Width: 520 bits + Port Type: stream + + Port: axis_p1_p2_ldg_c + Mode: write_only + Range (bytes): + Data Width: 32 bits + Port Type: stream + + Port: axis_p1_p2_ldg_d + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmacc_side + Mode: write_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_c + Mode: write_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_d0 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmfeed_d1 + Mode: write_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p1_p2_mmsxt_c + Mode: write_only + Range (bytes): + Data Width: 64 bits + Port Type: stream + + Port: axis_p2_p1_bsk_c + Mode: read_only + Range (bytes): + Data Width: 8 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_c + Mode: read_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_d0 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p1_mmacc_side + Mode: read_only + Range (bytes): + Data Width: 16 bits + Port Type: stream + + Port: axis_p2_p1_mmsxt_d0 + Mode: read_only + Range (bytes): + Data Width: 2048 bits + Port Type: stream + + Port: axis_p2_p1_side + Mode: read_only + Range (bytes): + Data Width: 256 bits + Port Type: stream + + Port: m_axi_glwe_0 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_0 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_1 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_2 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_3 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_4 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_5 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_6 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_ksk_7 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_pem_0 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_pem_1 + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 512 bits + Port Type: addressable + + Port: m_axi_trc + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 32 bits + Port Type: addressable + + Port: m_axi_ucore + Mode: master + Range (bytes): 0xFFFFFFFF + Data Width: 32 bits + Port Type: addressable + + Port: s_axil + Mode: slave + Range (bytes): 0x10000 + Data Width: 32 bits + Port Type: addressable + +-------------------------- +Instance: hpu_msplit_3parts_1in3_1 + Base Address: 0x800000 + + Argument: M_AXI_UCORE_PTR + Register Offset: 0x10 + Port: m_axi_ucore + Memory: HBM[1] (MEM_DRAM) + + Argument: M_AXI_TRC_PTR + Register Offset: 0x14 + Port: m_axi_trc + Memory: HBM[0] (MEM_HBM) + + Argument: M_AXI_PEM_0_PTR + Register Offset: 0x18 + Port: m_axi_pem_0 + Memory: HBM[10] (MEM_DRAM) + + Argument: M_AXI_PEM_1_PTR + Register Offset: 0x1c + Port: m_axi_pem_1 + Memory: HBM[11] (MEM_DRAM) + + Argument: M_AXI_GLWE_0_PTR + Register Offset: 0x20 + Port: m_axi_glwe_0 + Memory: HBM[12] (MEM_DRAM) + + Argument: M_AXI_KSK_0_PTR + Register Offset: 0x24 + Port: m_axi_ksk_0 + Memory: HBM[24] (MEM_DRAM) + + Argument: M_AXI_KSK_1_PTR + Register Offset: 0x28 + Port: m_axi_ksk_1 + Memory: HBM[25] (MEM_DRAM) + + Argument: M_AXI_KSK_2_PTR + Register Offset: 0x2c + Port: m_axi_ksk_2 + Memory: HBM[26] (MEM_DRAM) + + Argument: M_AXI_KSK_3_PTR + Register Offset: 0x30 + Port: m_axi_ksk_3 + Memory: HBM[27] (MEM_DRAM) + + Argument: M_AXI_KSK_4_PTR + Register Offset: 0x34 + Port: m_axi_ksk_4 + Memory: HBM[28] (MEM_DRAM) + + Argument: M_AXI_KSK_5_PTR + Register Offset: 0x38 + Port: m_axi_ksk_5 + Memory: HBM[29] (MEM_DRAM) + + Argument: M_AXI_KSK_6_PTR + Register Offset: 0x3c + Port: m_axi_ksk_6 + Memory: HBM[30] (MEM_DRAM) + + Argument: M_AXI_KSK_7_PTR + Register Offset: 0x40 + Port: m_axi_ksk_7 + Memory: HBM[31] (MEM_DRAM) + + Argument: axis_p1_p2_batch + Register Offset: 0x0 + Port: axis_p1_p2_batch + Memory: dc_0 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_bsk_c + Register Offset: 0x0 + Port: axis_p1_p2_bsk_c + Memory: dc_1 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_ldg_c + Register Offset: 0x0 + Port: axis_p1_p2_ldg_c + Memory: dc_2 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_ldg_d + Register Offset: 0x0 + Port: axis_p1_p2_ldg_d + Memory: dc_3 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmacc_side + Register Offset: 0x0 + Port: axis_p1_p2_mmacc_side + Memory: dc_4 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_c + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_c + Memory: dc_5 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_d0 + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_d0 + Memory: dc_6 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmfeed_d1 + Register Offset: 0x0 + Port: axis_p1_p2_mmfeed_d1 + Memory: dc_7 (MEM_STREAMING_CONNECTION) + + Argument: axis_p1_p2_mmsxt_c + Register Offset: 0x0 + Port: axis_p1_p2_mmsxt_c + Memory: dc_8 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_bsk_c + Register Offset: 0x0 + Port: axis_p2_p1_bsk_c + Memory: dc_9 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_c + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_c + Memory: dc_10 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_d0 + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_d0 + Memory: dc_11 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmacc_side + Register Offset: 0x0 + Port: axis_p2_p1_mmacc_side + Memory: dc_12 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_mmsxt_d0 + Register Offset: 0x0 + Port: axis_p2_p1_mmsxt_d0 + Memory: dc_13 (MEM_STREAMING_CONNECTION) + + Argument: axis_p2_p1_side + Register Offset: 0x0 + Port: axis_p2_p1_side + Memory: dc_14 (MEM_STREAMING_CONNECTION) +============================================================================== +Generated By +------------ + Command: v++ + Version: 2024.1 - 2024-05-20-23:21:20 (SW BUILD: 5074859) + Command Line: v++ --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/cfg/hpu_msplit_3parts.cfg --connectivity.nk hpu_msplit_3parts_1in3:1 --connectivity.nk hpu_msplit_3parts_2in3:1 --connectivity.nk hpu_msplit_3parts_3in3:1 --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_bsk_c --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_c --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmsxt_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmsxt_c --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_c --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_d:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_d --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d0:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d0 --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d1:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d1 --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmacc_side:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmacc_side --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_batch:hpu_msplit_3parts_2in3_1.axis_p1_p2_batch --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_d0 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_c --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_bsk_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_bsk_c --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_side --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmsxt_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmsxt_d0 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_side --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d0:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d0 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d1:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d1 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d2:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d2 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d3:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d3 --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_c --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_bsk_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_bsk_c --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_batch:hpu_msplit_3parts_3in3_1.axis_p2_p3_batch --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d0:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d0 --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d1:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d1 --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d2:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d2 --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d3:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d3 --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_c --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_bsk_c --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_side:hpu_msplit_3parts_2in3_1.axis_p3_p2_side --connectivity.slr hpu_msplit_3parts_1in3_1:SLR0 --connectivity.slr hpu_msplit_3parts_2in3_1:SLR1 --connectivity.slr hpu_msplit_3parts_3in3_1:SLR2 --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_trc:HBM[0] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ucore:HBM[1] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_pem_0:HBM[10] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_pem_1:HBM[11] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_glwe_0:HBM[12] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_0:HBM[2] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_1:HBM[3] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_2:HBM[4] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_3:HBM[5] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_4:HBM[6] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_5:HBM[7] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_6:HBM[8] --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_7:HBM[9] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_0:HBM[24] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_1:HBM[25] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_2:HBM[26] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_3:HBM[27] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_4:HBM[28] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_5:HBM[29] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_6:HBM[30] --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_7:HBM[31] --debug --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo --kernel_frequency 0:300 --link --optimize 0 --output /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin --platform xilinx_u55c_gen3x16_xdma_3_202210_1 --report_level 0 --save-temps --target hw --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin --vivado.param project.writeIntermediateCheckpoints=1 --vivado.prop run.impl_1.STEPS.OPT_DESIGN.TCL.PRE=/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/constraints/hpu_msplit_3parts_impl_opt_design_pre.xdc --vivado.prop run.synth_1.STEPS.SYNTH_DESIGN.ARGS.NO_SRLEXTRACT=true --vivado.prop run.impl_1.{STEPS.OPT_DESIGN.ARGS.MORE OPTIONS}={-hier_fanout_limit 1024} --vivado.prop run.impl_1.STEPS.PLACE_DESIGN.ARGS.DIRECTIVE=AltSpreadLogic_high --vivado.prop run.impl_1.STEPS.ROUTE_DESIGN.ARGS.DIRECTIVE=AlternateCLBRouting + Options: --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/cfg/hpu_msplit_3parts.cfg + --connectivity.nk hpu_msplit_3parts_1in3:1 + --connectivity.nk hpu_msplit_3parts_2in3:1 + --connectivity.nk hpu_msplit_3parts_3in3:1 + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_bsk_c + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_c + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmsxt_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmsxt_c + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_c + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_d:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_d + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d0:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d0 + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d1:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d1 + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_mmacc_side:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmacc_side + --connectivity.sc hpu_msplit_3parts_1in3_1.axis_p1_p2_batch:hpu_msplit_3parts_2in3_1.axis_p1_p2_batch + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_d0 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_c + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_bsk_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_bsk_c + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_side + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmsxt_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmsxt_d0 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_side + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d0:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d0 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d1:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d1 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d2:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d2 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d3:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d3 + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_c + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_bsk_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_bsk_c + --connectivity.sc hpu_msplit_3parts_2in3_1.axis_p2_p3_batch:hpu_msplit_3parts_3in3_1.axis_p2_p3_batch + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d0:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d0 + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d1:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d1 + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d2:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d2 + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d3:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d3 + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_c + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_bsk_c + --connectivity.sc hpu_msplit_3parts_3in3_1.axis_p3_p2_side:hpu_msplit_3parts_2in3_1.axis_p3_p2_side + --connectivity.slr hpu_msplit_3parts_1in3_1:SLR0 + --connectivity.slr hpu_msplit_3parts_2in3_1:SLR1 + --connectivity.slr hpu_msplit_3parts_3in3_1:SLR2 + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_trc:HBM[0] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ucore:HBM[1] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_pem_0:HBM[10] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_pem_1:HBM[11] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_glwe_0:HBM[12] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_0:HBM[2] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_1:HBM[3] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_2:HBM[4] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_3:HBM[5] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_4:HBM[6] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_5:HBM[7] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_6:HBM[8] + --connectivity.sp hpu_msplit_3parts_3in3_1.m_axi_bsk_7:HBM[9] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_0:HBM[24] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_1:HBM[25] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_2:HBM[26] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_3:HBM[27] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_4:HBM[28] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_5:HBM[29] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_6:HBM[30] + --connectivity.sp hpu_msplit_3parts_1in3_1.m_axi_ksk_7:HBM[31] + --debug + --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo + --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo + --input_files /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo + --kernel_frequency 0:300 + --link + --optimize 0 + --output /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin + --platform xilinx_u55c_gen3x16_xdma_3_202210_1 + --report_level 0 + --save-temps + --target hw + --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin + --vivado.param project.writeIntermediateCheckpoints=1 + --vivado.prop run.impl_1.STEPS.OPT_DESIGN.TCL.PRE=/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/constraints/hpu_msplit_3parts_impl_opt_design_pre.xdc + --vivado.prop run.synth_1.STEPS.SYNTH_DESIGN.ARGS.NO_SRLEXTRACT=true + --vivado.prop run.impl_1.{STEPS.OPT_DESIGN.ARGS.MORE OPTIONS}={-hier_fanout_limit 1024} + --vivado.prop run.impl_1.STEPS.PLACE_DESIGN.ARGS.DIRECTIVE=AltSpreadLogic_high + --vivado.prop run.impl_1.STEPS.ROUTE_DESIGN.ARGS.DIRECTIVE=AlternateCLBRouting +============================================================================== +User Added Key Value Pairs +-------------------------- + +============================================================================== diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.link_summary b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.link_summary new file mode 100644 index 000000000..3d71a76a6 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_msplit_3parts.xclbin.link_summary @@ -0,0 +1,1377 @@ + +{ + "thisFile": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin.link_summary", + "connectId": "", + "serverToken": "", + "timestamp": "0" +} + + +{ + "type": "ET_CmdStep", + "dateTimestamp": "Tue Mar 25 21:43:59 2025", + "timestampMillis": "1742935439546", + "buildStep": { + "cmdId": "e88f97ad-c344-46ae-84b0-dceb8646f1dd", + "name": "v++", + "logFile": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/link.steps.log", + "commandLine": "/opt/xilinx/Vitis/2024.1/bin/unwrapped/lnx64.o/v++ --vivado.prop \"run.__KERNEL__.{STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS}={-directive sdx_optimization_effort_high}\" --advanced.misc \"report=type report_timing_summary name impl_report_timing_summary_route_design_summary steps {route_design} runs {impl_1} options {-max_paths 10}\" --advanced.misc \"report=type report_timing_summary name impl_report_timing_summary_post_route_phys_opt_design_summary steps {post_route_phys_opt_design} runs {impl_1} options {-max_paths 10}\" -l -g -t hw --platform xilinx_u55c_gen3x16_xdma_3_202210_1 --save-temps --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin -o /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/cfg/hpu_msplit_3parts.cfg --vivado.prop run.impl_1.STEPS.OPT_DESIGN.TCL.PRE=/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/constraints/hpu_msplit_3parts_impl_opt_design_pre.xdc --kernel_frequency 0:300 /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo ", + "args": [ + "-l", + "-g", + "-t", + "hw", + "--platform", + "xilinx_u55c_gen3x16_xdma_3_202210_1", + "--save-temps", + "--temp_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin", + "-o", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin", + "--config", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/cfg/hpu_msplit_3parts.cfg", + "--vivado.prop", + "run.impl_1.STEPS.OPT_DESIGN.TCL.PRE=/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/constraints/hpu_msplit_3parts_impl_opt_design_pre.xdc", + "--kernel_frequency", + "0:300", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo" + ], + "iniFiles": [ + { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/cfg/hpu_msplit_3parts.cfg", + "content": "# General configuration knobs\n# equivalent of --platform knob\n# platform=\n# equivalent of --log_dir knob\n# logdir=\n# equivalent of --report_dir knob\n# report_dir=\n# equivalent of --target knob\n#target=hw\n\n# Enable debug mode\ndebug=1\nsave-temps=1\n\n# Enable link mode\n# link=1\n\n# Vivado properties\n[vivado]\nparam=project.writeIntermediateCheckpoints=1\nprop=run.synth_1.STEPS.SYNTH_DESIGN.ARGS.NO_SRLEXTRACT=true\nprop=run.impl_1.{STEPS.OPT_DESIGN.ARGS.MORE OPTIONS}={-hier_fanout_limit 1024}\nprop=run.impl_1.STEPS.PLACE_DESIGN.ARGS.DIRECTIVE=AltSpreadLogic_high\nprop=run.impl_1.STEPS.ROUTE_DESIGN.ARGS.DIRECTIVE=AlternateCLBRouting\n\n[connectivity]\n# Number of CU per xo\nnk=hpu_msplit_3parts_1in3:1\nnk=hpu_msplit_3parts_2in3:1\nnk=hpu_msplit_3parts_3in3:1\n\n# SLR assignement\nslr=hpu_msplit_3parts_1in3_1:SLR0\nslr=hpu_msplit_3parts_2in3_1:SLR1\nslr=hpu_msplit_3parts_3in3_1:SLR2\n\n# Axi4 memory connection\nsp=hpu_msplit_3parts_1in3_1.m_axi_trc:HBM[0]\n# Note that if the following is modified, do not forget to change the ucore config\nsp=hpu_msplit_3parts_1in3_1.m_axi_ucore:HBM[1]\n\nsp=hpu_msplit_3parts_1in3_1.m_axi_pem_0:HBM[10]\nsp=hpu_msplit_3parts_1in3_1.m_axi_pem_1:HBM[11]\nsp=hpu_msplit_3parts_1in3_1.m_axi_glwe_0:HBM[12]\n\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_0:HBM[2]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_1:HBM[3]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_2:HBM[4]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_3:HBM[5]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_4:HBM[6]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_5:HBM[7]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_6:HBM[8]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_7:HBM[9]\n\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_0:HBM[24]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_1:HBM[25]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_2:HBM[26]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_3:HBM[27]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_4:HBM[28]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_5:HBM[29]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_6:HBM[30]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_7:HBM[31]\n\n\n# AXI stream\n# part1 -\u003e part2\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_bsk_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmsxt_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmsxt_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_d:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_d\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d0:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d0\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d1:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d1\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmacc_side:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmacc_side\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_batch:hpu_msplit_3parts_2in3_1.axis_p1_p2_batch\n\n# part2 -\u003e part1\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_bsk_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_bsk_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_side\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmsxt_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmsxt_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_side\n\n# part2 -\u003e part3\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d0:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d1:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d1\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d2:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d2\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d3:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d3\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_bsk_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_bsk_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_batch:hpu_msplit_3parts_3in3_1.axis_p2_p3_batch\n# part3 -\u003e part2\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d0:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d0\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d1:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d1\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d2:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d2\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d3:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d3\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_c\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_bsk_c\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_side:hpu_msplit_3parts_2in3_1.axis_p3_p2_side\n" + } + ], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:43:59 2025", + "timestampMillis": "1742935439546", + "status": { + "cmdId": "e88f97ad-c344-46ae-84b0-dceb8646f1dd", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_FlowMetaData", + "dateTimestamp": "Tue Mar 25 21:44:03 2025", + "timestampMillis": "1742935443837", + "buildSummary": { + "hardwarePlatform": "xilinx_u55c_gen3x16_xdma_3_202210_1.xpfm", + "hardwareDsa": "", + "platformDirectory": "/opt/xilinx/platforms/xilinx_u55c_gen3x16_xdma_3_202210_1", + "runtime": "OpenCL", + "systemConfig": "Linux", + "flow": "BF_LINK", + "target": "TT_HW", + "binaryContainer": { + "base": { + "type": "BT_UKNOWN", + "name": "hpu_msplit_3parts", + "file": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin", + "reports": [], + "uuid": "" + }, + "kernels": [] + }, + "kernels": [ + { + "base": { + "type": "KERNEL", + "name": "hpu_msplit_3parts_1in3", + "file": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo", + "reports": [], + "uuid": "" + }, + "sources": [], + "psSources": [], + "cuNames": [ + "hpu_msplit_3parts_1in3_1" + ], + "type": "RTL", + "frequency": 0, + "freqUnits": "" + }, + { + "base": { + "type": "KERNEL", + "name": "hpu_msplit_3parts_2in3", + "file": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo", + "reports": [], + "uuid": "" + }, + "sources": [], + "psSources": [], + "cuNames": [ + "hpu_msplit_3parts_2in3_1" + ], + "type": "RTL", + "frequency": 0, + "freqUnits": "" + }, + { + "base": { + "type": "KERNEL", + "name": "hpu_msplit_3parts_3in3", + "file": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo", + "reports": [], + "uuid": "" + }, + "sources": [], + "psSources": [], + "cuNames": [ + "hpu_msplit_3parts_3in3_1" + ], + "type": "RTL", + "frequency": 0, + "freqUnits": "" + } + ], + "toolVersion": "Vitis V++ Compiler Release 2024.1. SW Build 5074859 on 2024-05-20-23:21:20" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Tue Mar 25 21:44:03 2025", + "timestampMillis": "1742935443951", + "buildStep": { + "cmdId": "33b1fc21-fde3-4a78-8553-b2ccdebcbbf2", + "name": "system_link", + "logFile": "", + "commandLine": "system_link --xo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo --xo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo --xo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo -keep --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/syslinkConfig.ini --xpfm /opt/xilinx/platforms/xilinx_u55c_gen3x16_xdma_3_202210_1/xilinx_u55c_gen3x16_xdma_3_202210_1.xpfm --target hw --output_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/sys_link", + "args": [ + "--xo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_1in3.xo", + "--xo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_2in3.xo", + "--xo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts_3in3.xo", + "-keep", + "--config", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/syslinkConfig.ini", + "--xpfm", + "/opt/xilinx/platforms/xilinx_u55c_gen3x16_xdma_3_202210_1/xilinx_u55c_gen3x16_xdma_3_202210_1.xpfm", + "--target", + "hw", + "--output_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int", + "--temp_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/sys_link" + ], + "iniFiles": [ + { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/syslinkConfig.ini", + "content": "nk=hpu_msplit_3parts_1in3:1\nnk=hpu_msplit_3parts_2in3:1\nnk=hpu_msplit_3parts_3in3:1\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_bsk_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmsxt_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmsxt_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_c:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_c\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_ldg_d:hpu_msplit_3parts_2in3_1.axis_p1_p2_ldg_d\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d0:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d0\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmfeed_d1:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmfeed_d1\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_mmacc_side:hpu_msplit_3parts_2in3_1.axis_p1_p2_mmacc_side\nsc=hpu_msplit_3parts_1in3_1.axis_p1_p2_batch:hpu_msplit_3parts_2in3_1.axis_p1_p2_batch\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_bsk_c:hpu_msplit_3parts_1in3_1.axis_p2_p1_bsk_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_side\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmsxt_d0:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmsxt_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p1_mmacc_side:hpu_msplit_3parts_1in3_1.axis_p2_p1_mmacc_side\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d0:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d0\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d1:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d1\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d2:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d2\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_d3:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_d3\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_proc_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_proc_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_bsk_c:hpu_msplit_3parts_3in3_1.axis_p2_p3_bsk_c\nsc=hpu_msplit_3parts_2in3_1.axis_p2_p3_batch:hpu_msplit_3parts_3in3_1.axis_p2_p3_batch\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d0:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d0\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d1:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d1\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d2:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d2\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_d3:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_d3\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_proc_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_proc_c\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_bsk_c:hpu_msplit_3parts_2in3_1.axis_p3_p2_bsk_c\nsc=hpu_msplit_3parts_3in3_1.axis_p3_p2_side:hpu_msplit_3parts_2in3_1.axis_p3_p2_side\nsp=hpu_msplit_3parts_1in3_1.m_axi_trc:HBM[0]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ucore:HBM[1]\nsp=hpu_msplit_3parts_1in3_1.m_axi_pem_0:HBM[10]\nsp=hpu_msplit_3parts_1in3_1.m_axi_pem_1:HBM[11]\nsp=hpu_msplit_3parts_1in3_1.m_axi_glwe_0:HBM[12]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_0:HBM[2]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_1:HBM[3]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_2:HBM[4]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_3:HBM[5]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_4:HBM[6]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_5:HBM[7]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_6:HBM[8]\nsp=hpu_msplit_3parts_3in3_1.m_axi_bsk_7:HBM[9]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_0:HBM[24]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_1:HBM[25]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_2:HBM[26]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_3:HBM[27]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_4:HBM[28]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_5:HBM[29]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_6:HBM[30]\nsp=hpu_msplit_3parts_1in3_1.m_axi_ksk_7:HBM[31]\nslr=hpu_msplit_3parts_1in3_1:SLR0\nslr=hpu_msplit_3parts_2in3_1:SLR1\nslr=hpu_msplit_3parts_3in3_1:SLR2\n\n" + } + ], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:03 2025", + "timestampMillis": "1742935443952", + "status": { + "cmdId": "33b1fc21-fde3-4a78-8553-b2ccdebcbbf2", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:26 2025", + "timestampMillis": "1742935466920", + "status": { + "cmdId": "33b1fc21-fde3-4a78-8553-b2ccdebcbbf2", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Tue Mar 25 21:44:26 2025", + "timestampMillis": "1742935466923", + "buildStep": { + "cmdId": "b848552b-dd20-45da-819c-c68595e8a991", + "name": "cf2sw", + "logFile": "", + "commandLine": "cf2sw -sdsl /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/sdsl.dat -rtd /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/cf2sw.rtd -nofilter /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/cf2sw_full.rtd -xclbin /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.xml -o /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.1.xml", + "args": [ + "-sdsl", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/sdsl.dat", + "-rtd", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/cf2sw.rtd", + "-nofilter", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/cf2sw_full.rtd", + "-xclbin", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.xml", + "-o", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.1.xml" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:26 2025", + "timestampMillis": "1742935466923", + "status": { + "cmdId": "b848552b-dd20-45da-819c-c68595e8a991", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478613", + "status": { + "cmdId": "b848552b-dd20-45da-819c-c68595e8a991", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478615", + "buildStep": { + "cmdId": "72bbf168-fd61-4e55-a3f8-502351704ed5", + "name": "rtd2_system_diagram", + "logFile": "", + "commandLine": "rtd2SystemDiagram", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478615", + "status": { + "cmdId": "72bbf168-fd61-4e55-a3f8-502351704ed5", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478799", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/systemDiagramModel.json", + "name": "", + "fileType": "JSON", + "reportType": "SYSTEM_DIAGRAM", + "cmdId": "" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478799", + "status": { + "cmdId": "72bbf168-fd61-4e55-a3f8-502351704ed5", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478801", + "buildStep": { + "cmdId": "96427ed0-9564-4a4a-817d-5767b19576ce", + "name": "vpl", + "logFile": "", + "commandLine": "vpl -t hw -f xilinx_u55c_gen3x16_xdma_3_202210_1 -s -g --kernel_frequency 0:300 --remote_ip_cache /projects/baroux/Fpga/fpga_u55c_syn/xrt/.ipcache --output_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int --log_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/logs/link --report_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/vplConfig.ini -k /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/kernel_info.dat --webtalk_flag Vitis --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link --no-info --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_3in3_1_0 --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_2in3_1_0 --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_1in3_1_0 --messageDb /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/run_link/vpl.pb /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/dr.bd.tcl", + "args": [ + "-t", + "hw", + "-f", + "xilinx_u55c_gen3x16_xdma_3_202210_1", + "-s", + "-g", + "--kernel_frequency", + "0:300", + "--remote_ip_cache", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/.ipcache", + "--output_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int", + "--log_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/logs/link", + "--report_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link", + "--config", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/vplConfig.ini", + "-k", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/kernel_info.dat", + "--webtalk_flag", + "Vitis", + "--temp_dir", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link", + "--no-info", + "--iprepo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_3in3_1_0", + "--iprepo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_2in3_1_0", + "--iprepo", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_1in3_1_0", + "--messageDb", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/run_link/vpl.pb", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/dr.bd.tcl" + ], + "iniFiles": [ + { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/vplConfig.ini", + "content": "[advanced]\nmisc=report=type report_timing_summary name impl_report_timing_summary_route_design_summary steps {route_design} runs {impl_1} options {-max_paths 10}\nmisc=report=type report_timing_summary name impl_report_timing_summary_post_route_phys_opt_design_summary steps {post_route_phys_opt_design} runs {impl_1} options {-max_paths 10}\nparam=compiler.enablePerformanceTrace=1\nparam=hw_emu.enableDebugWaveform=1\nparam=hw_emu.enableProfiling=1\nparam=compiler.vppCurrentWorkingDir=/projects/baroux/Fpga/fpga_u55c_syn/xrt\nmisc=BinaryName=hpu_msplit_3parts\n\n[connectivity]\nnk=hpu_msplit_3parts_1in3:1:hpu_msplit_3parts_1in3_1\nnk=hpu_msplit_3parts_2in3:1:hpu_msplit_3parts_2in3_1\nnk=hpu_msplit_3parts_3in3:1:hpu_msplit_3parts_3in3_1\n\n[vivado]\nprop=run.__KERNEL__.{STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS}={-directive sdx_optimization_effort_high}\nparam=project.writeIntermediateCheckpoints=1\nprop=run.impl_1.STEPS.OPT_DESIGN.TCL.PRE=/projects/baroux/Fpga/fpga_u55c_syn/xrt/kernel/xilinx_u55c_gen3x16_xdma_3_202210_1/constraints/hpu_msplit_3parts_impl_opt_design_pre.xdc\nprop=run.synth_1.STEPS.SYNTH_DESIGN.ARGS.NO_SRLEXTRACT=true\nprop=run.impl_1.{STEPS.OPT_DESIGN.ARGS.MORE OPTIONS}={-hier_fanout_limit 1024}\nprop=run.impl_1.STEPS.PLACE_DESIGN.ARGS.DIRECTIVE=AltSpreadLogic_high\nprop=run.impl_1.STEPS.ROUTE_DESIGN.ARGS.DIRECTIVE=AlternateCLBRouting\n\n" + } + ], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:38 2025", + "timestampMillis": "1742935478801", + "status": { + "cmdId": "96427ed0-9564-4a4a-817d-5767b19576ce", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_CmdStep", + "dateTimestamp": "Tue Mar 25 21:44:39 2025", + "timestampMillis": "1742935479829", + "buildStep": { + "cmdId": "d20442de-939c-4a6f-9b9a-ddfda3d86cb7", + "name": "vpl", + "logFile": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/link.steps.log", + "commandLine": "/opt/xilinx/Vitis/2024.1/bin/unwrapped/lnx64.o/vpl -t hw -f xilinx_u55c_gen3x16_xdma_3_202210_1 -s -g --kernel_frequency 0:300 --remote_ip_cache /projects/baroux/Fpga/fpga_u55c_syn/xrt/.ipcache --output_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int --log_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/logs/link --report_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link --config /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/vplConfig.ini -k /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/kernel_info.dat --webtalk_flag Vitis --temp_dir /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link --no-info --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_3in3_1_0 --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_2in3_1_0 --iprepo /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xo/ip_repo/zama_ai_RTLKernel_hpu_msplit_3parts_1in3_1_0 --messageDb /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/run_link/vpl.pb /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/dr.bd.tcl ", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/run_link" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:39 2025", + "timestampMillis": "1742935479829", + "status": { + "cmdId": "d20442de-939c-4a6f-9b9a-ddfda3d86cb7", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_VivadoProject", + "dateTimestamp": "Tue Mar 25 21:44:43 2025", + "timestampMillis": "1742935483403", + "vivadoProject": { + "openDir": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl", + "openScript": "openprj.tcl", + "relativeProject": "prj/prj.xpr" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Tue Mar 25 21:44:43 2025", + "timestampMillis": "1742935483404", + "buildStep": { + "cmdId": "ce88c619-cd2a-42ba-8a22-4f799c78e22f", + "name": "vivado", + "logFile": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/vivado.log", + "commandLine": "vivado -log vivado.log -applog -m64 -messageDb vivado.pb -mode batch -source vpl.tcl -notrace", + "args": [ + "-log", + "vivado.log", + "-applog", + " -m64", + "-messageDb", + "vivado.pb", + "-mode", + "batch", + "-source", + "vpl.tcl", + "-notrace" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/run_link" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 21:44:43 2025", + "timestampMillis": "1742935483404", + "status": { + "cmdId": "ce88c619-cd2a-42ba-8a22-4f799c78e22f", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Tue Mar 25 21:47:29 2025", + "timestampMillis": "1742935649601", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/automation_summary_pre_synthesis.txt", + "name": "", + "fileType": "TEXT", + "reportType": "VITIS_DESIGN_FLOW", + "cmdId": "" + } +} + + +{ + "type": "ET_CmdStep", + "dateTimestamp": "Tue Mar 25 22:02:26 2025", + "timestampMillis": "1742936546572", + "buildStep": { + "cmdId": "9382dc96-73d2-4ae0-bb24-681279d2b32e", + "name": "vivado.impl", + "logFile": "", + "commandLine": "", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 22:02:26 2025", + "timestampMillis": "1742936546572", + "status": { + "cmdId": "9382dc96-73d2-4ae0-bb24-681279d2b32e", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_CmdStep", + "dateTimestamp": "Tue Mar 25 22:02:26 2025", + "timestampMillis": "1742936546572", + "buildStep": { + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17", + "name": "vivado.impl.impl_1", + "logFile": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1/runme.log", + "commandLine": "", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Tue Mar 25 22:02:26 2025", + "timestampMillis": "1742936546572", + "status": { + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 01:52:48 2025", + "timestampMillis": "1742950368758", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1/system_diagram.json", + "name": "", + "fileType": "JSON", + "reportType": "SYSTEM_DIAGRAM_PLUS", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 01:58:41 2025", + "timestampMillis": "1742950721280", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/kernel_service.json", + "name": "", + "fileType": "JSON", + "reportType": "KERNEL_SERVICE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 01:58:41 2025", + "timestampMillis": "1742950721282", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/kernel_service.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "KERNEL_SERVICE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:05:09 2025", + "timestampMillis": "1742951109343", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1/dr_timing_summary.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY_FAIL", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:05:09 2025", + "timestampMillis": "1742951109346", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1/dr_timing_summary.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY_FAIL", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:05:09 2025", + "timestampMillis": "1742951109348", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/vivado/vpl/prj/prj.runs/impl_1/dr_timing_summary.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY_FAIL", + "cmdId": "" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002184", + "status": { + "cmdId": "ce88c619-cd2a-42ba-8a22-4f799c78e22f", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002202", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_slr_util_placed.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_SLR_UTIL_PLACED", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002202", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_routed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_UTILIZATION_ROUTE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002203", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_routed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "KERNEL_UTILIZATION_ROUTE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002203", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_routed.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_UTILIZATION_ROUTE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002203", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_slr_util_routed.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_SLR_UTIL_ROUTED", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002204", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_placed.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_UTILIZATION_PLACEMENT", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002204", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_placed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "KERNEL_UTILIZATION_PLACEMENT", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002204", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_synthed.xutil", + "name": "", + "fileType": "XUTIL", + "reportType": "KERNEL_UTILIZATION_SYNTHESIS", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002205", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_slr_util_placed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_SLR_UTIL_PLACED", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002205", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_synthed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_UTILIZATION_SYNTHESIS", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002205", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_slr_util_routed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_SLR_UTIL_ROUTED", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002206", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_routed.xutil", + "name": "", + "fileType": "XUTIL", + "reportType": "KERNEL_UTILIZATION_ROUTE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002206", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_synthed.pb", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_UTILIZATION_SYNTHESIS", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002206", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_full_util_placed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_UTILIZATION_PLACEMENT", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002207", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_synthed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "KERNEL_UTILIZATION_SYNTHESIS", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002207", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_kernel_util_placed.xutil", + "name": "", + "fileType": "XUTIL", + "reportType": "KERNEL_UTILIZATION_PLACEMENT", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002267", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/syn/ulp_hpu_msplit_3parts_3in3_1_0_synth_1_ulp_hpu_msplit_3parts_3in3_1_0_utilization_synth.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_UTILIZATION", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002267", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/syn/ulp_hpu_msplit_3parts_2in3_1_0_synth_1_ulp_hpu_msplit_3parts_2in3_1_0_utilization_synth.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_UTILIZATION", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002267", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/syn/ulp_hpu_msplit_3parts_1in3_1_0_synth_1_ulp_hpu_msplit_3parts_1in3_1_0_utilization_synth.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_UTILIZATION", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002268", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_system_diagram.json", + "name": "", + "fileType": "JSON", + "reportType": "SYSTEM_DIAGRAM_PLUS", + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002285", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_hw_bb_locked_timing_summary_routed.rpt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY", + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002300", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_hw_bb_locked_timing_summary_routed.rpx", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY", + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002301", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/imp/impl_1_hw_bb_locked_timing_summary_routed.rpv", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_REPORT_TIMING_SUMMARY_CONCISE", + "cmdId": "d4e342ad-5c5e-49f7-bcaa-4ca00eccbd17" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002318", + "status": { + "cmdId": "d20442de-939c-4a6f-9b9a-ddfda3d86cb7", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002346", + "status": { + "cmdId": "96427ed0-9564-4a4a-817d-5767b19576ce", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002348", + "buildStep": { + "cmdId": "2ecccd10-c47f-4aaa-b5cd-d6c8647b572f", + "name": "rtdgen", + "logFile": "", + "commandLine": "rtdgen", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002348", + "status": { + "cmdId": "2ecccd10-c47f-4aaa-b5cd-d6c8647b572f", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002350", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts_xml.rtd", + "name": "", + "fileType": "JSON", + "reportType": "XCLBIN_INFO", + "cmdId": "" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002351", + "buildStep": { + "cmdId": "d50b9e7c-2808-435d-bc1a-5f415fd1cfd9", + "name": "cf2sw", + "logFile": "", + "commandLine": "cf2sw -a /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/address_map.xml -sdsl /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/sdsl.dat -xclbin /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.xml -rtd /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.rtd -o /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.xml", + "args": [ + "-a", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/address_map.xml", + "-sdsl", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/sdsl.dat", + "-xclbin", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/xclbin_orig.xml", + "-rtd", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.rtd", + "-o", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.xml" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:02 2025", + "timestampMillis": "1742952002351", + "status": { + "cmdId": "d50b9e7c-2808-435d-bc1a-5f415fd1cfd9", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013044", + "status": { + "cmdId": "d50b9e7c-2808-435d-bc1a-5f415fd1cfd9", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013044", + "buildStep": { + "cmdId": "f7f79c44-5798-4e5b-a797-b6bceb7569e7", + "name": "rtdgen", + "logFile": "", + "commandLine": "writeSystemDiagram", + "args": [ + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.rtd", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/systemDiagramModelSlrBaseAddress.json" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013045", + "status": { + "cmdId": "f7f79c44-5798-4e5b-a797-b6bceb7569e7", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013049", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/systemDiagramModelSlrBaseAddress.json", + "name": "", + "fileType": "JSON", + "reportType": "SYSTEM_DIAGRAM_PLUS", + "cmdId": "" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013049", + "status": { + "cmdId": "f7f79c44-5798-4e5b-a797-b6bceb7569e7", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013049", + "buildStep": { + "cmdId": "779ee271-74da-4d52-a347-203fb158d528", + "name": "rtdgen", + "logFile": "", + "commandLine": "writeAutomationSummary", + "args": [ + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/automation_summary.txt" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013049", + "status": { + "cmdId": "779ee271-74da-4d52-a347-203fb158d528", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013051", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/automation_summary.txt", + "name": "", + "fileType": "TEXT", + "reportType": "VITIS_DESIGN_FLOW", + "cmdId": "" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013051", + "status": { + "cmdId": "779ee271-74da-4d52-a347-203fb158d528", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013051", + "status": { + "cmdId": "2ecccd10-c47f-4aaa-b5cd-d6c8647b572f", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013053", + "buildStep": { + "cmdId": "187eb425-0099-4350-a876-7bf49df83606", + "name": "xclbinutil", + "logFile": "", + "commandLine": "xclbinutil --add-section BITSTREAM:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/partial.bit --force --target hw --key-value SYS:dfx_enable:true --add-section :JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.rtd --append-section :JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/appendSection.rtd --add-section CLOCK_FREQ_TOPOLOGY:JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts_xml.rtd --add-section BUILD_METADATA:JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts_build.rtd --add-section EMBEDDED_METADATA:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.xml --add-section SYSTEM_METADATA:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/systemDiagramModelSlrBaseAddress.json --key-value SYS:PlatformVBNV:xilinx_u55c_gen3x16_xdma_3_202210_1 --output /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin", + "args": [ + "--add-section", + "BITSTREAM:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/partial.bit", + "--force", + "--target", + "hw", + "--key-value", + "SYS:dfx_enable:true", + "--add-section", + ":JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.rtd", + "--append-section", + ":JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/appendSection.rtd", + "--add-section", + "CLOCK_FREQ_TOPOLOGY:JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts_xml.rtd", + "--add-section", + "BUILD_METADATA:JSON:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts_build.rtd", + "--add-section", + "EMBEDDED_METADATA:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/hpu_msplit_3parts.xml", + "--add-section", + "SYSTEM_METADATA:RAW:/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/link/int/systemDiagramModelSlrBaseAddress.json", + "--key-value", + "SYS:PlatformVBNV:xilinx_u55c_gen3x16_xdma_3_202210_1", + "--output", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013053", + "status": { + "cmdId": "187eb425-0099-4350-a876-7bf49df83606", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013326", + "status": { + "cmdId": "187eb425-0099-4350-a876-7bf49df83606", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013328", + "buildStep": { + "cmdId": "d69687b2-69c8-414e-b41c-dae94f910cd3", + "name": "xclbinutilinfo", + "logFile": "", + "commandLine": "xclbinutil --quiet --force --info /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin.info --input /projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin", + "args": [ + "--quiet", + "--force", + "--info", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin.info", + "--input", + "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/hpu_msplit_3parts.xclbin" + ], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:13 2025", + "timestampMillis": "1742952013328", + "status": { + "cmdId": "d69687b2-69c8-414e-b41c-dae94f910cd3", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014017", + "status": { + "cmdId": "d69687b2-69c8-414e-b41c-dae94f910cd3", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_SubCmdStep", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014020", + "buildStep": { + "cmdId": "77aea7eb-2523-4ea9-a609-b4d45e330b6a", + "name": "generate_sc_driver", + "logFile": "", + "commandLine": "", + "args": [], + "iniFiles": [], + "cwd": "/projects/baroux/Fpga/fpga_u55c_syn/xrt" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014020", + "status": { + "cmdId": "77aea7eb-2523-4ea9-a609-b4d45e330b6a", + "state": "CS_RUNNING" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014020", + "status": { + "cmdId": "77aea7eb-2523-4ea9-a609-b4d45e330b6a", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014024", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/system_estimate_hpu_msplit_3parts.xtxt", + "name": "", + "fileType": "TEXT", + "reportType": "GLOBAL_SYSTEM_ESTIMATE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014027", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/logs/optraceViewer.html", + "name": "", + "fileType": "HTML", + "reportType": "OPERATION_TRACE", + "cmdId": "" + } +} + + +{ + "type": "ET_Status", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014027", + "status": { + "cmdId": "e88f97ad-c344-46ae-84b0-dceb8646f1dd", + "state": "CS_PASSED" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014092", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/reports/link/v++_link_hpu_msplit_3parts_guidance.html", + "name": "", + "fileType": "HTML", + "reportType": "GLOBAL_RULECHECK_GUIDANCE", + "cmdId": "" + } +} + + +{ + "type": "ET_Report", + "dateTimestamp": "Wed Mar 26 02:20:14 2025", + "timestampMillis": "1742952014092", + "report": { + "path": "/projects/baroux/Fpga/fpga_u55c_syn/xrt/output/hw/_tmp_vitis_xclbin/v++_link_hpu_msplit_3parts_guidance.pb3", + "name": "", + "fileType": "BINARY_PROTOBUF", + "reportType": "GLOBAL_RULECHECK_GUIDANCE", + "cmdId": "" + } +} + diff --git a/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_regif_core.toml b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_regif_core.toml new file mode 100644 index 000000000..bdfc2b104 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/u55c_gf64/hpu_regif_core.toml @@ -0,0 +1,622 @@ +# This is a sample example of register-map definition + +module_name="hpu_regif_core" +description="Hpu top-level register interface. Used by the host to retrieved RTL information, configure it and issue commands." +word_size_b = 32 +offset = 0x00 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.Xrt] +description="Vitis Required registers" +offset= 0x0 + + # Currently not in used -> Placeholder only +[section.Xrt.register.reserved] + description="Xrt reserved" + default={Cst=0x00} + owner="User" + read_access="Read" + write_access="Write" + +# ===================================================================================================================== +[section.info] +description="Contain all the RTL parameters used that have impact on associated SW" +offset= 0x10 + +[section.info.register.version] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="VERSION"} + +[section.info.register.ntt_architecture] + description="NTT architecture" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="NTT_CORE_ARCH"} + +[section.info.register.ntt_structure] + description="NTT structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix = { size_b=8, offset_b=0 , default={Param="R"}, description="NTT radix"} + field.psi = { size_b=8, offset_b=8 , default={Param="PSI"}, description="NTT psi"} + field.div = { size_b=8, offset_b=16, default={Param="BWD_PSI_DIV"}, description="NTT backward div"} + field.delta = { size_b=8, offset_b=24, default={Param="DELTA"}, description="NTT network delta (for wmm arch)"} + +[section.info.register.ntt_rdx_cut] + description="NTT radix cuts, in log2 unit (for gf64 arch)" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix_cut0 = { size_b=4, offset_b=0 , default={Param="NTT_RDX_CUT_S_0"}, description="NTT radix cut #0"} + field.radix_cut1 = { size_b=4, offset_b=4 , default={Param="NTT_RDX_CUT_S_1"}, description="NTT radix cut #1"} + field.radix_cut2 = { size_b=4, offset_b=8 , default={Param="NTT_RDX_CUT_S_2"}, description="NTT radix cut #2"} + field.radix_cut3 = { size_b=4, offset_b=12, default={Param="NTT_RDX_CUT_S_3"}, description="NTT radix cut #3"} + field.radix_cut4 = { size_b=4, offset_b=16, default={Param="NTT_RDX_CUT_S_4"}, description="NTT radix cut #4"} + field.radix_cut5 = { size_b=4, offset_b=20, default={Param="NTT_RDX_CUT_S_5"}, description="NTT radix cut #5"} + field.radix_cut6 = { size_b=4, offset_b=24, default={Param="NTT_RDX_CUT_S_6"}, description="NTT radix cut #6"} + field.radix_cut7 = { size_b=4, offset_b=28, default={Param="NTT_RDX_CUT_S_7"}, description="NTT radix cut #7"} + +[section.info.register.ntt_pbs] + description="Maximum number of PBS in the NTT pipeline" + owner="Parameter" + read_access="Read" + write_access="None" + field.batch_pbs_nb = { size_b=8, offset_b=0 , default={Param="BATCH_PBS_NB"}, description="Maximum number of PBS in the NTT pipe"} + field.total_pbs_nb = { size_b=8, offset_b=8 , default={Param="TOTAL_PBS_NB"}, description="Maximum number of PBS stored in PEP buffer"} + +[section.info.register.ntt_modulo] + description="Code associated to the NTT prime" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="MOD_NTT_NAME"} + +[section.info.register.application] + description="Code associated with the application" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="APPLICATION_NAME"} + +[section.info.register.ks_structure] + description="Key-switch structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.x = { size_b=8, offset_b=0 , default={Param="LBX"}, description="Number of coefficients on X dimension"} + field.y = { size_b=8, offset_b=8 , default={Param="LBY"}, description="Number of coefficients on Y dimension"} + field.z = { size_b=8, offset_b=16, default={Param="LBZ"}, description="Number of coefficients on Z dimension"} + +[section.info.register.ks_crypto_param] + description="Key-switch crypto parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.mod_ksk_w = { size_b=8, offset_b=0 , default={Param="MOD_KSK_W"}, description="Width of KSK modulo"} + field.ks_l = { size_b=8, offset_b=8 , default={Param="KS_L"}, description="Number of KS decomposition level"} + field.ks_b = { size_b=8, offset_b=16, default={Param="KS_B_W"}, description="Width of KS decomposition base"} + +[section.info.register.regf_structure] + description="Register file structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.reg_nb = { size_b=8, offset_b=0 , default={Param="REGF_REG_NB"}, description="Number of registers in regfile"} + field.coef_nb = { size_b=8, offset_b=8 , default={Param="REGF_COEF_NB"}, description="Number of coefficients at regfile interface"} + +[section.info.register.isc_structure] + description="Instruction scheduler structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.depth = { size_b=8, offset_b=0 , default={Param="ISC_DEPTH"}, description="Number of slots in ISC lookahead buffer."} + field.min_iop_size = { size_b=8, offset_b=8 , default={Param="MIN_IOP_SIZE"}, description="Minimum number of DOp per IOp to prevent sync_id overflow."} + +[section.info.register.pe_properties] + description="Processing elements parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.alu_nb = { size_b=8, offset_b=24 , default={Param="PEA_ALU_NB"}, description="Number of coefficients processed in parallel in pe_alu"} + field.pep_regf_period = { size_b=8, offset_b=16 , default={Param="PEP_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEP and regfile"} + field.pem_regf_period = { size_b=8, offset_b=8 , default={Param="PEM_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEM and regfile"} + field.pea_regf_period = { size_b=8, offset_b=0 , default={Param="PEA_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEA and regfile"} + +[section.info.register.bsk_structure] + description="BSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_cut_nb = { size_b=8, offset_b=8 , default={Param="BSK_CUT_NB"}, description="BSK cut nb"} + +[section.info.register.ksk_structure] + description="KSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.ksk_cut_nb = { size_b=8, offset_b=8 , default={Param="KSK_CUT_NB"}, description="KSK cut nb"} + +[section.info.register.hbm_axi4_nb] + description="Number of AXI4 connections to HBM" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_pc = { size_b=8, offset_b=0 , default={Param="BSK_PC"}, description="Number of HBM connections for BSK"} + field.ksk_pc = { size_b=8, offset_b=8, default={Param="KSK_PC"}, description="Number of HBM connections for KSK"} + field.pem_pc = { size_b=8, offset_b=16, default={Param="PEM_PC"}, description="Number of HBM connections for ciphertexts (PEM)"} + field.glwe_pc = { size_b=8, offset_b=24, default={Param="GLWE_PC"}, description="Number of HBM connections for GLWE"} + +[section.info.register.hbm_axi4_dataw_pem] + description="Ciphertext HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_PEM_DATA_W"} + +[section.info.register.hbm_axi4_dataw_glwe] + description="GLWE HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_GLWE_DATA_W"} + +[section.info.register.hbm_axi4_dataw_bsk] + description="BSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_BSK_DATA_W"} + +[section.info.register.hbm_axi4_dataw_ksk] + description="KSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_KSK_DATA_W"} + +# ===================================================================================================================== +[section.bpip] +offset= 0x200 +description="BPIP configuration" + +[section.bpip.register.use] + description="(1) Use BPIP mode, (0) use IPIP mode (default)" + owner="User" + read_access="Read" + write_access="Write" + field.use_bpip = { size_b=1, offset_b=0 , default={Cst=1}, description="use"} + field.use_opportunism = { size_b=1, offset_b=1 , default={Cst=0}, description="use opportunistic PBS flush"} + +[section.bpip.register.timeout] + description="Timeout for BPIP mode" + owner="User" + read_access="Read" + write_access="Write" + default={Cst=0xffffffff} + +# ===================================================================================================================== +[section.hbm_axi4_addr_1in3] +offset= 0x400 +description="HBM AXI4 connection address offset" + +[section.hbm_axi4_addr_1in3.register.ct] + description="Address offset for each ciphertext HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb","_pc1_lsb", "_pc1_msb"] + +[section.hbm_axi4_addr_1in3.register.glwe] + description="Address offset for each GLWE HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + + +[section.hbm_axi4_addr_1in3.register.ksk] + description="Address offset for each KSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb"] + + [section.hbm_axi4_addr_1in3.register.trc] + description="Address offset for each trace HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + + +# ===================================================================================================================== +[section.hbm_axi4_addr_3in3] +description="HBM AXI4 connection address offset" + +[section.hbm_axi4_addr_3in3.register.bsk] + description="Address offset for each BSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb"] + + +# ===================================================================================================================== +[section.status_1in3] +description="HPU status of part 1in3" +offset= 0x800 + +[section.status_1in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 1in3"} + +# ===================================================================================================================== +[section.status_3in3] +description="HPU status of parts 2in3 and 3in3" + +[section.status_3in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 3in3"} + +# ===================================================================================================================== +[section.ksk_avail] +description="KSK availability configuration" +offset= 0x1000 + +[section.ksk_avail.register.avail] + description="KSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.ksk_avail.register.reset] + description="KSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.bsk_avail] +description="BSK availability configuration" + +[section.bsk_avail.register.avail] + description="BSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.bsk_avail.register.reset] + description="BSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.runtime_1in3] +description="Runtime information" +offset= 0x2000 + +[section.runtime_1in3.register.pep_cmux_loop] + description="PEP: CMUX iteration loop number" + owner="Kernel" + read_access="Read" + write_access="None" + field.br_loop = { size_b=15, offset_b=0 , default={Cst=0}, description="PBS current BR-loop"} + field.br_loop_c = { size_b=1, offset_b=15 , default={Cst=0}, description="PBS current BR-loop parity"} + field.ks_loop = { size_b=15, offset_b=16 , default={Cst=0}, description="KS current KS-loop"} + field.ks_loop_c = { size_b=1, offset_b=31 , default={Cst=0}, description="KS current KS-loop parity"} + +[section.runtime_1in3.register.pep_pointer_0] + description="PEP: pointers (part 1)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pool_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pool_rp"} + field.pool_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pool_wp"} + field.ldg_pt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ldg_pt"} + field.ldb_pt = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ldb_pt"} + +[section.runtime_1in3.register.pep_pointer_1] + description="PEP: pointers (part 2)" + owner="Kernel" + read_access="Read" + write_access="None" + field.ks_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP ks_in_rp"} + field.ks_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP ks_in_wp"} + field.ks_out_rp = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ks_out_rp"} + field.ks_out_wp = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ks_out_wp"} + +[section.runtime_1in3.register.pep_pointer_2] + description="PEP: pointers (part 3)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pbs_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pbs_in_rp"} + field.pbs_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pbs_in_wp"} + field.ipip_flush_last_pbs_in_loop = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP IPIP flush last pbs_in_loop"} + +[section.runtime_1in3.register.isc_latest_instruction] + description="ISC: 4 latest instructions received ([0] is the most recent)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_0","_1","_2","_3"] + +[section.runtime_1in3.register.pep_seq_bpip_batch_cnt] + description="PEP: BPIP batch counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_flush_cnt] + description="PEP: BPIP batch triggered by a flush counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_timeout_cnt] + description="PEP: BPIP batch triggered by a timeout counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_waiting_batch_cnt] + description="PEP: BPIP batch that waits the trigger counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_filling_cnt] + description="PEP: Count batch with filled with a given number of CT (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_1","_2","_3","_4","_5","_6","_7","_8","_9","_10","_11","_12","_13","_14","_15","_16"] + +[section.runtime_1in3.register.pep_seq_ld_ack_cnt] + description="PEP: load BLWE ack counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_cmux_not_full_batch_cnt] + description="PEP: not full batch CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_ipip_flush_cnt] + description="PEP: IPIP flush CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldb_rcp_dur] + description="PEP: load BLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_req_dur] + description="PEP: load GLWE request max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_rcp_dur] + description="PEP: load GLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_load_ksk_rcp_dur] + description="PEP: load KSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + + +[section.runtime_1in3.register.pep_mmacc_sxt_rcp_dur] + description="PEP: MMACC SXT reception duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_req_dur] + description="PEP: MMACC SXT request duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_cmd_wait_b_dur] + description="PEP: MMACC SXT command wait for b duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_inst_cnt] + description="PEP: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ack_cnt] + description="PEP: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_inst_cnt] + description="PEM: load input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_ack_cnt] + description="PEM: load instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_inst_cnt] + description="PEM: store input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_ack_cnt] + description="PEM: store instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_inst_cnt] + description="PEA: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_ack_cnt] + description="PEA: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_inst_cnt] + description="ISC: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_ack_cnt] + description="ISC: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_info_0] + description="PEM: load first data)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_0","_pc0_1","_pc0_2","_pc0_3","_pc1_0","_pc1_1","_pc1_2","_pc1_3"] + +[section.runtime_1in3.register.pem_load_info_1] + description="PEM: load first address" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_lsb","_pc0_msb","_pc1_lsb","_pc1_msb"] + +[section.runtime_1in3.register.pem_store_info_0] + description="PEM: store info 0)" + owner="Kernel" + read_access="Read" + write_access="None" + field.cmd_vld = { size_b=1, offset_b=0 , default={Cst=0}, description="PEM_ST cmd vld"} + field.cmd_rdy = { size_b=1, offset_b=1 , default={Cst=0}, description="PEM_ST cmd rdy"} + field.pem_regf_rd_req_vld = { size_b=1, offset_b=2 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_vld"} + field.pem_regf_rd_req_rdy = { size_b=1, offset_b=3 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_rdy"} + field.brsp_fifo_in_vld = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST brsp_fifo_in_vld"} + field.brsp_fifo_in_rdy = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST brsp_fifo_in_rdy"} + field.rcp_fifo_in_vld = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST rcp_fifo_in_vld"} + field.rcp_fifo_in_rdy = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST rcp_fifo_in_rdy"} + field.r2_axi_vld = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST r2_axi_vld"} + field.r2_axi_rdy = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST r2_axi_rdy"} + field.c0_enough_location = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST c0_enough_location"} + +[section.runtime_1in3.register.pem_store_info_1] + description="PEM: store info 1" + owner="Kernel" + read_access="Read" + write_access="None" + field.s0_cmd_vld = { size_b=4, offset_b=0 , default={Cst=0}, description="PEM_ST s0_cmd_vld"} + field.s0_cmd_rdy = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST s0_cmd_rdy"} + field.m_axi_bvalid = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST m_axi_bvalid"} + field.m_axi_bready = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST m_axi_bready"} + field.m_axi_wvalid = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST m_axi_wvalid"} + field.m_axi_wready = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST m_axi_wready"} + field.m_axi_awvalid = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST m_axi_awvalid"} + field.m_axi_awready = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST m_axi_awready"} + +[section.runtime_1in3.register.pem_store_info_2] + description="PEM: store info 2" + owner="Kernel" + read_access="Read" + write_access="None" + field.c0_free_loc_cnt = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST c0_free_loc_cnt"} + field.brsp_bresp_cnt = { size_b=16, offset_b=16 , default={Cst=0}, description="PEM_ST brsp_bresp_cnt"} + +[section.runtime_1in3.register.pem_store_info_3] + description="PEM: store info 3" + owner="Kernel" + read_access="Read" + write_access="None" + field.brsp_ack_seen = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST brsp_ack_seen"} + field.c0_cmd_cnt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEM_ST c0_cmd_cnt"} + + +# ===================================================================================================================== +[section.runtime_3in3] +description="Runtime information" + +[section.runtime_3in3.register.pep_load_bsk_rcp_dur] + description="PEP: load BSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + +[section.runtime_3in3.register.pep_bskif_req_info_0] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_br_loop_rp = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK read pointer"} + field.req_br_loop_wp = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK write pointer"} + +[section.runtime_3in3.register.pep_bskif_req_info_1] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_prf_br_loop = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK prefetch pointer"} + field.req_parity = { size_b=1, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK pointer parity"} + field.req_assigned = { size_b=1, offset_b=31 , default={Cst=0}, description="PEP BSK_IF requester assignment"} + +# ===================================================================================================================== +[section.WorkAck] +description="Purpose of this section" +offset= 0x8000 + +[section.WorkAck.register.workq] + description="Insert work in workq and read status" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.WorkAck.register.ackq] + description="Pop ack from in ackq" + owner="Kernel" + read_access="ReadNotify" + write_access="None" diff --git a/backends/tfhe-hpu-backend/config_store/v80/Readme.md b/backends/tfhe-hpu-backend/config_store/v80/Readme.md new file mode 100644 index 000000000..ecf5014fc --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/Readme.md @@ -0,0 +1,74 @@ +NB: Versal don't have the pdi embedded in the configuration. Instead user is in charge of pdi upload in FPGA flash. +Thus, a given configuration could works on multiple pdi. + +# Fpga version @250MHz +This configuration as based on the following Fpga commit: +``` +commit ad668f931eff0c281a0848d43360da0b8813539a (HEAD -> dev/hpu_v80, origin/dev/hpu_v80, origin/baroux/dev/hpu_v80, baroux/dev/hpu_v80) +Merge: 1489024a f308f067 +Author: Baptiste Roux +Date: Fri Feb 14 19:02:53 2025 +0100 + + [MERGE] 'dev/hpu' into baroux/dev/hpu_v80 + + Retrieved CI bugfix from dev/hpu +``` +Tagged as `aved_v1.0` + +Built with the following command: (i.e. versal/run_syn_hpu_msplit_3parts_psi32.sh) +``` +TOP=top_hpu_assembly +TOP_MSPLIT=TOP_MSPLIT_1 +TOP_BATCH=TOP_BATCH_TOPhpu_BPBS12_TPBS32 +TOP_PCMAX=TOP_PCMAX_pem2_glwe1_bsk16_ksk16 +TOP_PC=TOP_PC_pem2_glwe1_bsk8_ksk16 +APPLICATION=APPLI_msg2_carry2_pfail64_132b_gaussian_1f72dba +NTT_MOD=NTT_MOD_goldilocks +NTT_CORE_ARCH=NTT_CORE_ARCH_gf64 +NTT_CORE_R_PSI=NTT_CORE_R2_PSI32 +NTT_CORE_RDX_CUT=NTT_CORE_RDX_CUT_n5c6 +NTT_CORE_DIV=NTT_CORE_DIV_1 +BSK_SLOT_CUT=BSK_SLOT8_CUT8 +KSK_SLOT_CUT=KSK_SLOT8_CUT16 +KSLB=KSLB_x3y64z3 +HPU_PART=HPU_PART_gf64 +AXI_DATA_W=AXI_DATA_W_256 +FPGA=FPGA_v80 + +just build $TOP new "-F TOP_MSPLIT $TOP_MSPLIT -F TOP_BATCH $TOP_BATCH -F TOP_PCMAX $TOP_PCMAX -F TOP_PC $TOP_PC -F APPLICATION $APPLICATION -F NTT_MOD $NTT_MOD -F NTT_CORE_ARCH $NTT_CORE_ARCH -F NTT_CORE_R_PSI $NTT_CORE_R_PSI -F NTT_CORE_RDX_CUT $NTT_CORE_RDX_CUT -F NTT_CORE_DIV $NTT_CORE_DIV -F BSK_SLOT_CUT $BSK_SLOT_CUT -F KSK_SLOT_CUT $KSK_SLOT_CUT -F KSLB $KSLB -F HPU_PART $HPU_PART -F AXI_DATA_W $AXI_DATA_W -F FPGA $FPGA" | tee build_out.log +``` + +# Fpga version @350MHz +This configuration as based on the following Fpga commit: +``` +commit d29dbeaccf09adfe0ee13e326f4633e14726b020 (HEAD -> baroux/dev/hpu_v80_2024.2, origin/baroux/dev/hpu_v80_2024.2) +Author: pgardratzama +Date: Tue Feb 11 16:12:10 2025 +0100 + + adds script to synthetize HPU 1 part PSI32 +``` +Mainly the that commit as above with flow modification from Pierre Gardrat to support Vivado 2024.2. +NB: Based on unofficial branch and thus not tagged + +Built with the following command: (i.e. versal/run_syn_hpu_1part_psi32.sh) +``` +TOP=fpga_top_hpu +TOP_MSPLIT=TOP_MSPLIT_1 +TOP_BATCH=TOP_BATCH_TOPhpu_BPBS12_TPBS32 +TOP_PCMAX=TOP_PCMAX_pem2_glwe1_bsk16_ksk16 +TOP_PC=TOP_PC_pem2_glwe1_bsk8_ksk16 +APPLICATION=APPLI_msg2_carry2_pfail64_132b_gaussian_1f72dba +NTT_MOD=NTT_MOD_goldilocks +NTT_CORE_ARCH=NTT_CORE_ARCH_gf64 +NTT_CORE_R_PSI=NTT_CORE_R2_PSI32 +NTT_CORE_RDX_CUT=NTT_CORE_RDX_CUT_n5c6 +NTT_CORE_DIV=NTT_CORE_DIV_1 +BSK_SLOT_CUT=BSK_SLOT8_CUT8 +KSK_SLOT_CUT=KSK_SLOT8_CUT16 +KSLB=KSLB_x3y64z3 +HPU_PART=HPU_PART_gf64 +AXI_DATA_W=AXI_DATA_W_256 +FPGA=FPGA_v80 + +just build $TOP new "-F TOP_MSPLIT $TOP_MSPLIT -F TOP_BATCH $TOP_BATCH -F TOP_PCMAX $TOP_PCMAX -F TOP_PC $TOP_PC -F APPLICATION $APPLICATION -F NTT_MOD $NTT_MOD -F NTT_CORE_ARCH $NTT_CORE_ARCH -F NTT_CORE_R_PSI $NTT_CORE_R_PSI -F NTT_CORE_RDX_CUT $NTT_CORE_RDX_CUT -F NTT_CORE_DIV $NTT_CORE_DIV -F BSK_SLOT_CUT $BSK_SLOT_CUT -F KSK_SLOT_CUT $KSK_SLOT_CUT -F KSLB $KSLB -F HPU_PART $HPU_PART -F AXI_DATA_W $AXI_DATA_W -F FPGA $FPGA" | tee build_out.log +``` diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_0.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_0.asm new file mode 100644 index 000000000..838beed9e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_0.asm @@ -0,0 +1,15 @@ +# CUST_0 +# Simple IOp to check the xfer between Hpu/Cpu +# Construct constant in dest slot -> 249 (0xf9) +SUB R0 R0 R0 +ADDS R0 R0 1 +ST TD[0].0 R0 +SUB R1 R1 R1 +ADDS R1 R1 2 +ST TD[0].1 R1 +SUB R2 R2 R2 +ADDS R2 R2 3 +ST TD[0].2 R2 +SUB R3 R3 R3 +ADDS R3 R3 3 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_1.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_1.asm new file mode 100644 index 000000000..3679e2c5f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_1.asm @@ -0,0 +1,11 @@ +# CUST_1 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_a +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_10.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_10.asm new file mode 100644 index 000000000..f591d66b3 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_10.asm @@ -0,0 +1,25 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 + Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +ADD R4 R5 R6 +ST TD[0].2 R4 + +; Dst[0].2 <- Src[0].2 + Src[1].2 +LD R9 TS[0].2 +LD R10 TS[1].2 +ADD R8 R9 R10 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 + Src[1].3 +LD R13 TS[0].3 +LD R14 TS[1].3 +ADD R12 R13 R14 +ST TD[0].3 R0 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_16.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_16.asm new file mode 100644 index 000000000..0b4cfe80f --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_16.asm @@ -0,0 +1,6 @@ +# CUST_16 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a.0) +LD R0 TS[0].0 +PBS_F R0 R0 PbsNone +ST TD[0].0 R0 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_17.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_17.asm new file mode 100644 index 000000000..bdb6711a7 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_17.asm @@ -0,0 +1,15 @@ +# CUST_17 +# Simple IOp to check PBS behavior +# Dest <- PBSNone(Src_a) +LD R0 TS[0].0 +PBS R0 R0 PbsNone +ST TD[0].0 R0 +LD R1 TS[0].1 +PBS R1 R1 PbsNone +ST TD[0].1 R1 +LD R2 TS[0].2 +PBS R2 R2 PbsNone +ST TD[0].2 R2 +LD R3 TS[0].3 +PBS_F R3 R3 PbsNone +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_18.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_18.asm new file mode 100644 index 000000000..c4b9a46a0 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_18.asm @@ -0,0 +1,23 @@ +; CUST_18 +; Simple IOp to check extraction pattern +; Correct result: +; * Dst[0,1] <- Src[0][0,1] +; * Dst[2,3] <- Src[1][0,1] + +; Pack Src[0][0,1] with a Mac and extract Carry/Msg in Dst[0][0,1] +LD R0 TS[0].0 +LD R1 TS[0].1 +MAC R3 R1 R0 4 +PBS R4 R3 PbsMsgOnly +PBS R5 R3 PbsCarryInMsg +ST TD[0].0 R4 +ST TD[0].1 R5 + +; Pack Src[1][0,1] with a Mac and extract Carry/Msg in Dst[0][2,3] +LD R10 TS[1].0 +LD R11 TS[1].1 +MAC R13 R11 R10 4 +PBS R14 R13 PbsMsgOnly +PBS R15 R13 PbsCarryInMsg +ST TD[0].2 R14 +ST TD[0].3 R15 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_19.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_19.asm new file mode 100644 index 000000000..0974347fa --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_19.asm @@ -0,0 +1,19 @@ +; CUST_19 +; Simple IOp to check PbsMl2 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- 0 +; * Dst[0][2] <- Src[0][0] +1 +; * Dst[0][3] <- 0 +; i.e Cust_19(0x2) => 0x32 + +; Construct a 0 for destination padding +SUB R16 R16 R16 + +; Apply PbsMl2 on Src[0] result goes in dest[0][0-3] (0-padded) +LD R0 TS[0].0 +PBS_ML2_F R0 R0 PbsTestMany2 +ST TD[0].0 R0 +ST TD[0].1 R16 +ST TD[0].2 R1 +ST TD[0].3 R16 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_2.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_2.asm new file mode 100644 index 000000000..bc8e0175e --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_2.asm @@ -0,0 +1,11 @@ +# CUST_2 +# Simple IOp to check the xfer between Hpu/Cpu +# Dest <- Src_b +LD R0 TS[1].0 +LD R1 TS[1].1 +LD R2 TS[1].2 +LD R3 TS[1].3 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_20.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_20.asm new file mode 100644 index 000000000..5f29f8ee5 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_20.asm @@ -0,0 +1,22 @@ +; CUST_20 +; Simple IOp to check PbsMl4 +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; i.e Cust_20(0x0) => 0xe4 + +SUB R16 R16 R16 +ST TD[0].0 R0 +ST TD[0].1 R0 +ST TD[0].2 R0 +ST TD[0].3 R0 + +; Apply PbsMl4 on Src[0] result goes in dest[0][0-3] +LD R0 TS[0].0 +PBS_ML4_F R0 R0 PbsTestMany4 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_21.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_21.asm new file mode 100644 index 000000000..5a601bbe6 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_21.asm @@ -0,0 +1,24 @@ +; CUST_21 +; Simple IOp to check PbsMl8 +; WARN: This operation required 16b ct width +; Correct result: +; * Dst[0][0] <- Src[0][0] +; * Dst[0][1] <- Src[0][0] +1 +; * Dst[0][2] <- Src[0][0] +2 +; * Dst[0][3] <- Src[0][0] +3 +; * Dst[0][4] <- Src[0][0] +4 +; * Dst[0][5] <- Src[0][0] +5 +; * Dst[0][6] <- Src[0][0] +6 +; * Dst[0][7] <- Src[0][0] +7 + +; Apply PbsMl8 on Src[0] result goes in dest[0][0-7] +LD R0 TS[0].0 +PBS_ML8_F R0 R0 PbsTestMany8 +ST TD[0].0 R0 +ST TD[0].1 R1 +ST TD[0].2 R2 +ST TD[0].3 R3 +ST TD[0].4 R4 +ST TD[0].5 R5 +ST TD[0].6 R6 +ST TD[0].7 R7 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_3.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_3.asm new file mode 100644 index 000000000..d13ca243c --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_3.asm @@ -0,0 +1,16 @@ +# CUST_3 +# Simple IOp to check isc behavior +# Generate obvious deps and check that isc correctly issued the dop +# Correct result must bu Dest <- Src[0] +LD R0 TS[0].0 +LD R1 TS[0].1 +LD R2 TS[0].2 +LD R3 TS[0].3 +PBS R4 R0 PbsNone +ST TD[0].0 R4 +PBS R4 R1 PbsNone +ST TD[0].1 R4 +PBS R4 R2 PbsNone +ST TD[0].2 R4 +PBS_F R4 R3 PbsNone +ST TD[0].3 R4 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_4.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_4.asm new file mode 100644 index 000000000..192068543 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_4.asm @@ -0,0 +1,264 @@ +# CUST_4 +# Just to check if this batch times out +LD R0 TS[0].31 +LD R1 TS[1].31 +LD R3 TS[0].27 +LD R4 TS[1].27 +LD R6 TS[0].30 +LD R7 TS[1].30 +LD R9 TS[0].28 +LD R10 TS[1].28 +LD R12 TS[0].29 +LD R13 TS[1].29 +LD R15 TS[0].23 +LD R16 TS[1].23 +LD R18 TS[0].26 +LD R19 TS[1].26 +LD R21 TS[0].24 +LD R22 TS[1].24 +LD R24 TS[0].20 +LD R25 TS[1].20 +LD R27 TS[0].13 +LD R28 TS[1].13 +LD R30 TS[0].25 +LD R31 TS[1].25 +LD R33 TS[0].22 +LD R34 TS[1].22 +LD R36 TS[0].17 +LD R37 TS[1].17 +LD R39 TS[0].19 +LD R40 TS[1].19 +LD R42 TS[0].15 +LD R43 TS[1].15 +LD R45 TS[0].12 +LD R46 TS[1].12 +LD R48 TS[0].7 +LD R49 TS[1].7 +LD R51 TS[0].6 +LD R52 TS[1].6 +LD R54 TS[0].10 +LD R55 TS[1].10 +LD R57 TS[0].14 +LD R58 TS[1].14 +LD R60 TS[0].11 +LD R61 TS[1].11 +ADD R2 R0 R1 +ADD R5 R3 R4 +LD R63 TS[0].18 +LD R3 TS[1].18 +ADD R8 R6 R7 +ST TH.0 R6 +ST TH.1 R7 +ADD R11 R9 R10 +ST TH.2 R11 +LD R9 TH.2 +ADD R14 R12 R13 +ST TH.3 R12 +ST TH.4 R13 +ADD R17 R15 R16 +ST TH.5 R17 +ADD R20 R18 R19 +ST TH.6 R18 +ST TH.7 R19 +LD R15 TH.5 +ADD R23 R21 R22 +ST TH.8 R23 +LD R21 TH.8 +ADD R26 R24 R25 +ST TH.9 R24 +ST TH.10 R25 +ADD R29 R27 R28 +ST TH.11 R29 +LD R27 TH.11 +ADD R32 R30 R31 +ST TH.12 R30 +ST TH.13 R31 +ADD R35 R33 R34 +ST TH.14 R35 +ADD R38 R36 R37 +ST TH.15 R36 +ST TH.16 R37 +LD R33 TH.14 +PBS_ML2 R0 R2 PbsManyGenProp +PBS_ML2 R6 R5 PbsManyGenProp +PBS_ML2 R10 R9 PbsManyGenProp +PBS_ML2 R12 R8 PbsManyGenProp +PBS_ML2 R16 R14 PbsManyGenProp +PBS_ML2 R18 R15 PbsManyGenProp +PBS_ML2 R22 R21 PbsManyGenProp +PBS_ML2 R24 R20 PbsManyGenProp +PBS_ML2 R28 R27 PbsManyGenProp +PBS_ML2 R30 R26 PbsManyGenProp +PBS_ML2 R34 R32 PbsManyGenProp +PBS_ML2_F R36 R33 PbsManyGenProp +ADD R41 R39 R40 +LD R39 TS[0].16 +LD R40 TS[1].16 +ST TH.17 R38 +ST TH.18 R33 +LD R33 TS[0].1 +ST TH.19 R32 +LD R32 TS[1].1 +ST TH.20 R26 +ST TH.21 R27 +LD R27 TS[0].21 +ST TH.22 R20 +LD R20 TS[1].21 +ST TH.23 R21 +ST TH.24 R15 +LD R15 TS[0].0 +ST TH.25 R14 +LD R14 TS[1].0 +ST TH.26 R8 +ST TH.27 R9 +LD R9 TS[0].3 +ST TH.28 R5 +LD R5 TS[1].3 +ST TH.29 R2 +ADD R44 R42 R43 +LD R42 TS[0].2 +LD R43 TS[1].2 +ST TH.30 R41 +ADD R47 R45 R46 +LD R45 TS[0].9 +LD R46 TS[1].9 +ST TH.31 R44 +ADD R50 R48 R49 +LD R48 TS[0].5 +LD R49 TS[1].5 +ST TH.32 R47 +ADD R53 R51 R52 +LD R51 TS[0].4 +LD R52 TS[1].4 +ST TH.33 R50 +ADD R56 R54 R55 +LD R54 TS[0].8 +LD R55 TS[1].8 +ST TH.34 R53 +ADD R59 R57 R58 +ADD R62 R60 R61 +ADD R4 R63 R3 +ADD R38 R39 R40 +ADD R26 R33 R32 +ADD R21 R27 R20 +ADD R8 R15 R14 +ADD R2 R9 R5 +ADD R41 R42 R43 +ADD R44 R45 R46 +ADD R47 R48 R49 +ADD R50 R51 R52 +ADD R53 R54 R55 +MAC R57 R11 R7 2 +LD R58 TH.31 +LD R63 TH.32 +LD R3 TH.17 +ST TH.35 R41 +LD R39 TH.30 +ST TH.36 R21 +ST TH.37 R47 +ST TH.38 R53 +ST TH.39 R44 +ST TH.40 R50 +ST TH.41 R0 +LD R27 TH.35 +ST TH.42 R12 +ST TH.43 R13 +LD R9 TH.39 +ST TH.44 R16 +ST TH.45 R17 +LD R5 TH.37 +ST TH.46 R18 +ST TH.47 R19 +ST TH.48 R6 +LD R6 TH.40 +ST TH.49 R22 +ST TH.50 R23 +ST TH.51 R10 +LD R10 TH.38 +ST TH.52 R24 +ST TH.53 R25 +ST TH.54 R28 +LD R28 TH.33 +ST TH.55 R30 +ST TH.56 R31 +ST TH.57 R29 +LD R29 TH.36 +ST TH.58 R34 +ST TH.59 R35 +ST TH.60 R36 +LD R36 TH.34 +PBS_ML2 R60 R58 PbsManyGenProp +PBS_ML2 R32 R38 PbsManyGenProp +PBS_ML2 R14 R63 PbsManyGenProp +PBS_ML2 R42 R8 PbsManyGenProp +PBS_ML2 R48 R3 PbsManyGenProp +PBS_ML2 R54 R62 PbsManyGenProp +PBS_ML2 R40 R39 PbsManyGenProp +PBS_ML2 R20 R4 PbsManyGenProp +PBS_ML2 R46 R59 PbsManyGenProp +PBS_ML2 R52 R26 PbsManyGenProp +PBS_ML2 R44 R56 PbsManyGenProp +PBS_ML2_F R50 R2 PbsManyGenProp +LD R11 TH.45 +ST TH.61 R37 +ST TH.62 R2 +LD R2 TH.53 +ST TH.63 R56 +LD R56 TH.59 +ST TH.64 R26 +ST TH.65 R59 +LD R59 TH.43 +ST TH.66 R4 +MAC R37 R11 R57 4 +MAC R26 R2 R56 2 +MAC R4 R59 R11 2 +MAC R2 R4 R57 4 +MAC R59 R33 R61 2 +LD R58 TH.57 +LD R62 TH.56 +ADDS R4 R42 0 +MAC R38 R47 R58 2 +MAC R63 R49 R59 4 +MAC R8 R21 R49 2 +MULS R3 R43 2 +ADDS R3 R3 0 +MAC R39 R62 R41 2 +MAC R42 R8 R59 4 +MAC R21 R53 R3 4 +PBS_ML2 R0 R27 PbsManyGenProp +PBS_ML2 R12 R9 PbsManyGenProp +PBS_ML2 R16 R5 PbsManyGenProp +PBS_ML2 R18 R6 PbsManyGenProp +PBS_ML2 R22 R10 PbsManyGenProp +PBS_ML2 R24 R28 PbsManyGenProp +PBS_ML2 R30 R29 PbsManyGenProp +PBS_ML2 R34 R36 PbsManyGenProp +PBS R11 R2 PbsReduceCarryPad +PBS R33 R4 PbsGenPropAdd +PBS R47 R3 PbsReduceCarry2 +PBS_F R49 R42 PbsReduceCarryPad +MAC R43 R1 R53 2 +ST TD[0].0 R33 +LD R29 TH.61 +MAC R8 R47 R52 4 +ADDS R27 R11 1 +MAC R9 R31 R39 4 +ADDS R5 R49 1 +MAC R6 R43 R3 4 +MAC R10 R45 R13 2 +MAC R28 R23 R25 2 +MAC R36 R29 R31 2 +MAC R2 R19 R51 2 +MAC R4 R35 R17 2 +MAC R1 R13 R28 4 +MAC R53 R10 R28 4 +MAC R47 R36 R39 4 +MAC R52 R17 R2 4 +MAC R11 R4 R2 4 +PBS R62 R21 PbsReduceCarry3 +PBS R42 R8 PbsGenPropAdd +PBS R33 R6 PbsReduceCarryPad +PBS R49 R53 PbsReduceCarryPad +PBS R43 R47 PbsReduceCarryPad +PBS_F R3 R11 PbsReduceCarryPad +MAC R45 R62 R0 4 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_8.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_8.asm new file mode 100644 index 000000000..c02eee9cd --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_8.asm @@ -0,0 +1,19 @@ +; CUST_8 +; Simple IOp to check the ALU operation +; Dst[0].0 <- Src[0].0 + Src[1].0 +LD R1 TS[0].0 +LD R2 TS[1].0 +ADD R0 R1 R2 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Src[1].1 +LD R5 TS[0].1 +LD R6 TS[1].1 +SUB R4 R5 R6 +ST TD[0].1 R4 + +; Dst[0].2 <- Src[0].2 + (Src[1].2 *4) +LD R9 TS[0].2 +LD R10 TS[1].2 +MAC R8 R9 R10 4 +ST TD[0].2 R8 diff --git a/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_9.asm b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_9.asm new file mode 100644 index 000000000..5e5cc4129 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/custom_iop/cust_9.asm @@ -0,0 +1,21 @@ +; CUST_9 +; Simple IOp to check the ALU Scalar operation +; Dst[0].0 <- Src[0].0 + Imm[0].0 +LD R1 TS[0].0 +ADDS R0 R1 TI[0].0 +ST TD[0].0 R0 + +; Dst[0].1 <- Src[0].1 - Imm[0].1 +LD R5 TS[0].1 +SUBS R4 R5 TI[0].1 +ST TD[0].1 R4 + +; Dst[0].2 <- Imm[0].2 - Src[0].2 +LD R9 TS[0].2 +SSUB R8 R9 TI[0].2 +ST TD[0].2 R8 + +; Dst[0].3 <- Src[0].3 * Imm[0].3 +LD R13 TS[0].3 +MULS R12 R13 TI[0].3 +ST TD[0].3 R12 diff --git a/backends/tfhe-hpu-backend/config_store/v80/hpu_config.toml b/backends/tfhe-hpu-backend/config_store/v80/hpu_config.toml new file mode 100644 index 000000000..a5fd2df84 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/hpu_config.toml @@ -0,0 +1,112 @@ + +[fpga] + regmap=["${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_cfg_3in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_1in3.toml", + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_regif_core_prc_3in3.toml"] + polling_us=10 +[fpga.ffi.V80] + ami_id=1 # First ami device in the list + qdma_h2c="/dev/qdma${V80_PCIE_DEV}001-MM-1" + qdma_c2h="/dev/qdma${V80_PCIE_DEV}001-MM-2" + +[rtl] + bpip_use = true + bpip_use_opportunism = true + bpip_timeout = 100_000 + +[board] + ct_mem = 32768 + ct_pc = [ + {Hbm= {pc=32}}, + {Hbm= {pc=33}}, + ] + heap_size = 16384 + + + lut_mem = 256 + lut_pc = {Hbm={pc=34}} + + fw_size= 16777216 # i.e. 16 MiB + fw_pc = {Ddr= {offset= 0x3900_0000}} # NB: Allocation must take place in the Discret DDR + + bsk_pc = [ + {Hbm={pc=8}}, + {Hbm={pc=12}}, + {Hbm={pc=24}}, + {Hbm={pc=28}}, + {Hbm={pc=40}}, + {Hbm={pc=44}}, + {Hbm={pc=56}}, + {Hbm={pc=60}} + ] + + ksk_pc = [ + {Hbm={pc=0}}, + {Hbm={pc=1}}, + {Hbm={pc=2}}, + {Hbm={pc=3}}, + {Hbm={pc=4}}, + {Hbm={pc=5}}, + {Hbm={pc=6}}, + {Hbm={pc=7}}, + {Hbm={pc=16}}, + {Hbm={pc=17}}, + {Hbm={pc=18}}, + {Hbm={pc=19}}, + {Hbm={pc=20}}, + {Hbm={pc=21}}, + {Hbm={pc=22}}, + {Hbm={pc=23}} + ] + + trace_pc = {Hbm={pc=35}} + trace_depth = 32 # In MB + +[firmware] + #implementation = "Ilp" + implementation = "Llt" + integer_w=[2,4,6,8,10,12,14,16,32,64,128] + min_batch_size = 11 + kogge_cfg = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/kogge_cfg.toml" + custom_iop.'IOP[0]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_0.asm" + custom_iop.'IOP[1]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_1.asm" + custom_iop.'IOP[2]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_2.asm" + custom_iop.'IOP[3]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_3.asm" + custom_iop.'IOP[4]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_4.asm" + custom_iop.'IOP[8]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_8.asm" + custom_iop.'IOP[9]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_9.asm" + custom_iop.'IOP[16]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_16.asm" + custom_iop.'IOP[17]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_17.asm" + custom_iop.'IOP[18]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_18.asm" + custom_iop.'IOP[19]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_19.asm" + custom_iop.'IOP[20]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_20.asm" + custom_iop.'IOP[21]' = "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/custom_iop/cust_21.asm" + +[firmware.op_cfg.default] + fill_batch_fifo = true + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MUL] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.MULS] + fill_batch_fifo = false + min_batch_size = false + use_tiers = false + flush_behaviour = "Patient" + flush = true + +[firmware.op_cfg.by_op.ERC_20] + fill_batch_fifo = true + min_batch_size = false + use_tiers = true + flush_behaviour = "Patient" + flush = true diff --git a/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_1in3.toml b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_1in3.toml new file mode 100644 index 000000000..bfdb80263 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_1in3.toml @@ -0,0 +1,256 @@ +module_name="hpu_regif_core_cfg_1in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x00 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_cfg_1in3] +description="entry_cfg_1in3 section with known value used for debug." +offset= 0x0 + +[section.entry_cfg_1in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x01010101} + +[section.entry_cfg_1in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x11111111} + +[section.entry_cfg_1in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x21212121} + + +[section.entry_cfg_1in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x31313131} + +# ===================================================================================================================== +[section.info] +description="RTL architecture parameters" +offset= 0x10 + +[section.info.register.version] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="VERSION"} + +[section.info.register.ntt_architecture] + description="NTT architecture" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="NTT_CORE_ARCH"} + +[section.info.register.ntt_structure] + description="NTT structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix = { size_b=8, offset_b=0 , default={Param="R"}, description="NTT radix"} + field.psi = { size_b=8, offset_b=8 , default={Param="PSI"}, description="NTT psi"} + field.div = { size_b=8, offset_b=16, default={Param="BWD_PSI_DIV"}, description="NTT backward div"} + field.delta = { size_b=8, offset_b=24, default={Param="DELTA"}, description="NTT network delta (for wmm arch)"} + +[section.info.register.ntt_rdx_cut] + description="NTT radix cuts, in log2 unit (for gf64 arch)" + owner="Parameter" + read_access="Read" + write_access="None" + field.radix_cut0 = { size_b=4, offset_b=0 , default={Param="NTT_RDX_CUT_S_0"}, description="NTT radix cut #0"} + field.radix_cut1 = { size_b=4, offset_b=4 , default={Param="NTT_RDX_CUT_S_1"}, description="NTT radix cut #1"} + field.radix_cut2 = { size_b=4, offset_b=8 , default={Param="NTT_RDX_CUT_S_2"}, description="NTT radix cut #2"} + field.radix_cut3 = { size_b=4, offset_b=12, default={Param="NTT_RDX_CUT_S_3"}, description="NTT radix cut #3"} + field.radix_cut4 = { size_b=4, offset_b=16, default={Param="NTT_RDX_CUT_S_4"}, description="NTT radix cut #4"} + field.radix_cut5 = { size_b=4, offset_b=20, default={Param="NTT_RDX_CUT_S_5"}, description="NTT radix cut #5"} + field.radix_cut6 = { size_b=4, offset_b=24, default={Param="NTT_RDX_CUT_S_6"}, description="NTT radix cut #6"} + field.radix_cut7 = { size_b=4, offset_b=28, default={Param="NTT_RDX_CUT_S_7"}, description="NTT radix cut #7"} + +[section.info.register.ntt_pbs] + description="Maximum number of PBS in the NTT pipeline" + owner="Parameter" + read_access="Read" + write_access="None" + field.batch_pbs_nb = { size_b=8, offset_b=0 , default={Param="BATCH_PBS_NB"}, description="Maximum number of PBS in the NTT pipe"} + field.total_pbs_nb = { size_b=8, offset_b=8 , default={Param="TOTAL_PBS_NB"}, description="Maximum number of PBS stored in PEP buffer"} + +[section.info.register.ntt_modulo] + description="Code associated to the NTT prime" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="MOD_NTT_NAME"} + +[section.info.register.application] + description="Code associated with the application" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="APPLICATION_NAME"} + +[section.info.register.ks_structure] + description="Key-switch structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.x = { size_b=8, offset_b=0 , default={Param="LBX"}, description="Number of coefficients on X dimension"} + field.y = { size_b=8, offset_b=8 , default={Param="LBY"}, description="Number of coefficients on Y dimension"} + field.z = { size_b=8, offset_b=16, default={Param="LBZ"}, description="Number of coefficients on Z dimension"} + +[section.info.register.ks_crypto_param] + description="Key-switch crypto parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.mod_ksk_w = { size_b=8, offset_b=0 , default={Param="MOD_KSK_W"}, description="Width of KSK modulo"} + field.ks_l = { size_b=8, offset_b=8 , default={Param="KS_L"}, description="Number of KS decomposition level"} + field.ks_b = { size_b=8, offset_b=16, default={Param="KS_B_W"}, description="Width of KS decomposition base"} + +[section.info.register.regf_structure] + description="Register file structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.reg_nb = { size_b=8, offset_b=0 , default={Param="REGF_REG_NB"}, description="Number of registers in regfile"} + field.coef_nb = { size_b=8, offset_b=8 , default={Param="REGF_COEF_NB"}, description="Number of coefficients at regfile interface"} + +[section.info.register.isc_structure] + description="Instruction scheduler structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.depth = { size_b=8, offset_b=0 , default={Param="ISC_DEPTH"}, description="Number of slots in ISC lookahead buffer."} + field.min_iop_size = { size_b=8, offset_b=8 , default={Param="MIN_IOP_SIZE"}, description="Minimum number of DOp per IOp to prevent sync_id overflow."} + +[section.info.register.pe_properties] + description="Processing elements parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.alu_nb = { size_b=8, offset_b=24 , default={Param="PEA_ALU_NB"}, description="Number of coefficients processed in parallel in pe_alu"} + field.pep_regf_period = { size_b=8, offset_b=16 , default={Param="PEP_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEP and regfile"} + field.pem_regf_period = { size_b=8, offset_b=8 , default={Param="PEM_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEM and regfile"} + field.pea_regf_period = { size_b=8, offset_b=0 , default={Param="PEA_REGF_PERIOD"}, description="Number of cycles between 2 consecutive data transfer between PEA and regfile"} + +[section.info.register.bsk_structure] + description="BSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_cut_nb = { size_b=8, offset_b=8 , default={Param="BSK_CUT_NB"}, description="BSK cut nb"} + +[section.info.register.ksk_structure] + description="KSK manager structure parameters" + owner="Parameter" + read_access="Read" + write_access="None" + field.ksk_cut_nb = { size_b=8, offset_b=8 , default={Param="KSK_CUT_NB"}, description="KSK cut nb"} + +[section.info.register.hbm_axi4_nb] + description="Number of AXI4 connections to HBM" + owner="Parameter" + read_access="Read" + write_access="None" + field.bsk_pc = { size_b=8, offset_b=0 , default={Param="BSK_PC"}, description="Number of HBM connections for BSK"} + field.ksk_pc = { size_b=8, offset_b=8, default={Param="KSK_PC"}, description="Number of HBM connections for KSK"} + field.pem_pc = { size_b=8, offset_b=16, default={Param="PEM_PC"}, description="Number of HBM connections for ciphertexts (PEM)"} + field.glwe_pc = { size_b=8, offset_b=24, default={Param="GLWE_PC"}, description="Number of HBM connections for GLWE"} + +[section.info.register.hbm_axi4_dataw_pem] + description="Ciphertext HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_PEM_DATA_W"} + +[section.info.register.hbm_axi4_dataw_glwe] + description="GLWE HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_GLWE_DATA_W"} + +[section.info.register.hbm_axi4_dataw_bsk] + description="BSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_BSK_DATA_W"} + +[section.info.register.hbm_axi4_dataw_ksk] + description="KSK HBM AXI4 connection data width" + owner="Parameter" + read_access="Read" + write_access="None" + default={Param="AXI4_KSK_DATA_W"} + + +# ===================================================================================================================== +[section.hbm_axi4_addr_1in3] +offset= 0x1000 +description="HBM AXI4 connection address offset" + +[section.hbm_axi4_addr_1in3.register.ct] + description="Address offset for each ciphertext HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb","_pc1_lsb", "_pc1_msb"] + +[section.hbm_axi4_addr_1in3.register.glwe] + description="Address offset for each GLWE HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + + +[section.hbm_axi4_addr_1in3.register.ksk] + description="Address offset for each KSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb", "_pc8_lsb", "_pc8_msb", "_pc9_lsb", "_pc9_msb", "_pc10_lsb", "_pc10_msb", "_pc11_lsb", "_pc11_msb", "_pc12_lsb", "_pc12_msb", "_pc13_lsb", "_pc13_msb", "_pc14_lsb", "_pc14_msb", "_pc15_lsb", "_pc15_msb"] + + [section.hbm_axi4_addr_1in3.register.trc] + description="Address offset for each trace HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb"] + +# ===================================================================================================================== +[section.bpip] +offset= 0x2000 +description="BPIP configuration" + +[section.bpip.register.use] + description="(1) Use BPIP mode, (0) use IPIP mode (default)" + owner="User" + read_access="Read" + write_access="Write" + field.use_bpip = { size_b=1, offset_b=0 , default={Cst=1}, description="use"} + field.use_opportunism = { size_b=1, offset_b=1 , default={Cst=0}, description="use opportunistic PBS flush"} + +[section.bpip.register.timeout] + description="Timeout for BPIP mode" + owner="User" + read_access="Read" + write_access="Write" + default={Cst=0xffffffff} diff --git a/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_3in3.toml b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_3in3.toml new file mode 100644 index 000000000..4afc095ab --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_cfg_3in3.toml @@ -0,0 +1,51 @@ +module_name="hpu_regif_core_cfg_3in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x20000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_cfg_3in3] +description="entry_cfg_3in3 section with known value used for debug." +offset= 0x0 + +[section.entry_cfg_3in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x03030303} + +[section.entry_cfg_3in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x13131313} + +[section.entry_cfg_3in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x23232323} + +[section.entry_cfg_3in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x33333333} + +# ===================================================================================================================== +[section.hbm_axi4_addr_3in3] +description="HBM AXI4 connection address offset" +offset= 0x10 + +[section.hbm_axi4_addr_3in3.register.bsk] + description="Address offset for each BSK HBM AXI4 connection" + owner="User" + read_access="Read" + write_access="Write" + duplicate=["_pc0_lsb", "_pc0_msb", "_pc1_lsb", "_pc1_msb", "_pc2_lsb", "_pc2_msb", "_pc3_lsb", "_pc3_msb", "_pc4_lsb", "_pc4_msb", "_pc5_lsb", "_pc5_msb", "_pc6_lsb", "_pc6_msb", "_pc7_lsb", "_pc7_msb", "_pc8_lsb", "_pc8_msb", "_pc9_lsb", "_pc9_msb", "_pc10_lsb", "_pc10_msb", "_pc11_lsb", "_pc11_msb", "_pc12_lsb", "_pc12_msb", "_pc13_lsb", "_pc13_msb", "_pc14_lsb", "_pc14_msb", "_pc15_lsb", "_pc15_msb"] diff --git a/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_1in3.toml b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_1in3.toml new file mode 100644 index 000000000..ef20175f8 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_1in3.toml @@ -0,0 +1,336 @@ +module_name="hpu_regif_core_prc_1in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x10000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_prc_1in3] +description="entry_prc_1in3 section with known value used for debug." +offset= 0x0 + +[section.entry_prc_1in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x02020202} + +[section.entry_prc_1in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x12121212} + +[section.entry_prc_1in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x22222222} + +[section.entry_prc_1in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x32323232} + +# ===================================================================================================================== +[section.status_1in3] +description="HPU status of part 1in3" +offset= 0x10 + +[section.status_1in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 1in3"} + +# ===================================================================================================================== +[section.ksk_avail] +description="KSK availability configuration" +offset= 0x1000 + +[section.ksk_avail.register.avail] + description="KSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.ksk_avail.register.reset] + description="KSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.runtime_1in3] +description="Runtime information" +offset= 0x2000 + +[section.runtime_1in3.register.pep_cmux_loop] + description="PEP: CMUX iteration loop number" + owner="Kernel" + read_access="Read" + write_access="None" + field.br_loop = { size_b=15, offset_b=0 , default={Cst=0}, description="PBS current BR-loop"} + field.br_loop_c = { size_b=1, offset_b=15 , default={Cst=0}, description="PBS current BR-loop parity"} + field.ks_loop = { size_b=15, offset_b=16 , default={Cst=0}, description="KS current KS-loop"} + field.ks_loop_c = { size_b=1, offset_b=31 , default={Cst=0}, description="KS current KS-loop parity"} + +[section.runtime_1in3.register.pep_pointer_0] + description="PEP: pointers (part 1)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pool_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pool_rp"} + field.pool_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pool_wp"} + field.ldg_pt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ldg_pt"} + field.ldb_pt = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ldb_pt"} + +[section.runtime_1in3.register.pep_pointer_1] + description="PEP: pointers (part 2)" + owner="Kernel" + read_access="Read" + write_access="None" + field.ks_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP ks_in_rp"} + field.ks_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP ks_in_wp"} + field.ks_out_rp = { size_b=8, offset_b=16 , default={Cst=0}, description="PEP ks_out_rp"} + field.ks_out_wp = { size_b=8, offset_b=24 , default={Cst=0}, description="PEP ks_out_wp"} + +[section.runtime_1in3.register.pep_pointer_2] + description="PEP: pointers (part 3)" + owner="Kernel" + read_access="Read" + write_access="None" + field.pbs_in_rp = { size_b=8, offset_b=0 , default={Cst=0}, description="PEP pbs_in_rp"} + field.pbs_in_wp = { size_b=8, offset_b=8 , default={Cst=0}, description="PEP pbs_in_wp"} + field.ipip_flush_last_pbs_in_loop = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP IPIP flush last pbs_in_loop"} + +[section.runtime_1in3.register.isc_latest_instruction] + description="ISC: 4 latest instructions received ([0] is the most recent)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_0","_1","_2","_3"] + +[section.runtime_1in3.register.pep_seq_bpip_batch_cnt] + description="PEP: BPIP batch counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_flush_cnt] + description="PEP: BPIP batch triggered by a flush counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_timeout_cnt] + description="PEP: BPIP batch triggered by a timeout counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_waiting_batch_cnt] + description="PEP: BPIP batch that waits the trigger counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_bpip_batch_filling_cnt] + description="PEP: Count batch with filled with a given number of CT (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_1","_2","_3","_4","_5","_6","_7","_8","_9","_10","_11","_12","_13","_14","_15","_16"] + +[section.runtime_1in3.register.pep_seq_ld_ack_cnt] + description="PEP: load BLWE ack counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_cmux_not_full_batch_cnt] + description="PEP: not full batch CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_seq_ipip_flush_cnt] + description="PEP: IPIP flush CMUX counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldb_rcp_dur] + description="PEP: load BLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_req_dur] + description="PEP: load GLWE request max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ldg_rcp_dur] + description="PEP: load GLWE reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_load_ksk_rcp_dur] + description="PEP: load KSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + + +[section.runtime_1in3.register.pep_mmacc_sxt_rcp_dur] + description="PEP: MMACC SXT reception duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_req_dur] + description="PEP: MMACC SXT request duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_mmacc_sxt_cmd_wait_b_dur] + description="PEP: MMACC SXT command wait for b duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_inst_cnt] + description="PEP: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pep_ack_cnt] + description="PEP: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_inst_cnt] + description="PEM: load input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_ack_cnt] + description="PEM: load instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_inst_cnt] + description="PEM: store input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_store_ack_cnt] + description="PEM: store instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_inst_cnt] + description="PEA: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pea_ack_cnt] + description="PEA: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_inst_cnt] + description="ISC: input instruction counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.isc_ack_cnt] + description="ISC: instruction acknowledge counter (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + +[section.runtime_1in3.register.pem_load_info_0] + description="PEM: load first data)" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_0","_pc0_1","_pc0_2","_pc0_3","_pc1_0","_pc1_1","_pc1_2","_pc1_3"] + +[section.runtime_1in3.register.pem_load_info_1] + description="PEM: load first address" + owner="Kernel" + read_access="Read" + write_access="None" + duplicate=["_pc0_lsb","_pc0_msb","_pc1_lsb","_pc1_msb"] + +[section.runtime_1in3.register.pem_store_info_0] + description="PEM: store info 0)" + owner="Kernel" + read_access="Read" + write_access="None" + field.cmd_vld = { size_b=1, offset_b=0 , default={Cst=0}, description="PEM_ST cmd vld"} + field.cmd_rdy = { size_b=1, offset_b=1 , default={Cst=0}, description="PEM_ST cmd rdy"} + field.pem_regf_rd_req_vld = { size_b=1, offset_b=2 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_vld"} + field.pem_regf_rd_req_rdy = { size_b=1, offset_b=3 , default={Cst=0}, description="PEM_ST pem_regf_rd_req_rdy"} + field.brsp_fifo_in_vld = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST brsp_fifo_in_vld"} + field.brsp_fifo_in_rdy = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST brsp_fifo_in_rdy"} + field.rcp_fifo_in_vld = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST rcp_fifo_in_vld"} + field.rcp_fifo_in_rdy = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST rcp_fifo_in_rdy"} + field.r2_axi_vld = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST r2_axi_vld"} + field.r2_axi_rdy = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST r2_axi_rdy"} + field.c0_enough_location = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST c0_enough_location"} + +[section.runtime_1in3.register.pem_store_info_1] + description="PEM: store info 1" + owner="Kernel" + read_access="Read" + write_access="None" + field.s0_cmd_vld = { size_b=4, offset_b=0 , default={Cst=0}, description="PEM_ST s0_cmd_vld"} + field.s0_cmd_rdy = { size_b=4, offset_b=4 , default={Cst=0}, description="PEM_ST s0_cmd_rdy"} + field.m_axi_bvalid = { size_b=4, offset_b=8 , default={Cst=0}, description="PEM_ST m_axi_bvalid"} + field.m_axi_bready = { size_b=4, offset_b=12 , default={Cst=0}, description="PEM_ST m_axi_bready"} + field.m_axi_wvalid = { size_b=4, offset_b=16 , default={Cst=0}, description="PEM_ST m_axi_wvalid"} + field.m_axi_wready = { size_b=4, offset_b=20 , default={Cst=0}, description="PEM_ST m_axi_wready"} + field.m_axi_awvalid = { size_b=4, offset_b=24 , default={Cst=0}, description="PEM_ST m_axi_awvalid"} + field.m_axi_awready = { size_b=4, offset_b=28 , default={Cst=0}, description="PEM_ST m_axi_awready"} + +[section.runtime_1in3.register.pem_store_info_2] + description="PEM: store info 2" + owner="Kernel" + read_access="Read" + write_access="None" + field.c0_free_loc_cnt = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST c0_free_loc_cnt"} + field.brsp_bresp_cnt = { size_b=16, offset_b=16 , default={Cst=0}, description="PEM_ST brsp_bresp_cnt"} + +[section.runtime_1in3.register.pem_store_info_3] + description="PEM: store info 3" + owner="Kernel" + read_access="Read" + write_access="None" + field.brsp_ack_seen = { size_b=16, offset_b=0 , default={Cst=0}, description="PEM_ST brsp_ack_seen"} + field.c0_cmd_cnt = { size_b=8, offset_b=16 , default={Cst=0}, description="PEM_ST c0_cmd_cnt"} diff --git a/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_3in3.toml b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_3in3.toml new file mode 100644 index 000000000..627f140c1 --- /dev/null +++ b/backends/tfhe-hpu-backend/config_store/v80/hpu_regif_core_prc_3in3.toml @@ -0,0 +1,100 @@ +module_name="hpu_regif_core_prc_3in3" +description="HPU top-level register interface. Used by the host to retrieve design information, and to configure it." +word_size_b = 32 +offset = 0x30000 +range = 0x10000 +ext_pkg = ["axi_if_common_param_pkg", "axi_if_shell_axil_pkg"] + +# ===================================================================================================================== +[section.entry_prc_3in3] +description="entry_prc_3in3 section with known value used for debug." +offset= 0x0 + +[section.entry_prc_3in3.register.dummy_val0] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x04040404} + +[section.entry_prc_3in3.register.dummy_val1] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x14141414} + +[section.entry_prc_3in3.register.dummy_val2] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x24242424} + +[section.entry_prc_3in3.register.dummy_val3] + description="RTL version" + owner="Parameter" + read_access="Read" + write_access="None" + default={Cst=0x34343434} + +# ===================================================================================================================== +[section.status_3in3] +description="HPU status of parts 2in3 and 3in3" +offset= 0x10 + +[section.status_3in3.register.error] + description="Error register (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.pbs = { size_b=32, offset_b=0 , default={Cst=0}, description="HPU error part 3in3"} + +# ===================================================================================================================== +[section.bsk_avail] +description="BSK availability configuration" +offset= 0x1000 + +[section.bsk_avail.register.avail] + description="BSK available bit" + owner="User" + read_access="Read" + write_access="Write" + field.avail = { size_b=1, offset_b=0 , default={Cst=0}, description="avail"} + +[section.bsk_avail.register.reset] + description="BSK reset sequence" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + field.request = { size_b=1, offset_b=0 , default={Cst=0}, description="request"} + field.done = { size_b=1, offset_b=31 , default={Cst=0}, description="done"} + +# ===================================================================================================================== +[section.runtime_3in3] +description="Runtime information" +offset= 0x2000 + +[section.runtime_3in3.register.pep_load_bsk_rcp_dur] + description="PEP: load BSK slice reception max duration (Could be reset by user)" + owner="Kernel" + read_access="Read" + write_access="WriteNotify" + duplicate=["_pc0","_pc1","_pc2","_pc3","_pc4","_pc5","_pc6","_pc7","_pc8","_pc9","_pc10","_pc11","_pc12","_pc13","_pc14","_pc15"] + +[section.runtime_3in3.register.pep_bskif_req_info_0] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_br_loop_rp = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK read pointer"} + field.req_br_loop_wp = { size_b=16, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK write pointer"} + +[section.runtime_3in3.register.pep_bskif_req_info_1] + description="PEP: BSK_IF: requester info 0" + owner="Kernel" + read_access="Read" + write_access="None" + field.req_prf_br_loop = { size_b=16, offset_b=0 , default={Cst=0}, description="PEP BSK_IF requester BSK prefetch pointer"} + field.req_parity = { size_b=1, offset_b=16 , default={Cst=0}, description="PEP BSK_IF requester BSK pointer parity"} + field.req_assigned = { size_b=1, offset_b=31 , default={Cst=0}, description="PEP BSK_IF requester assignment"} diff --git a/backends/tfhe-hpu-backend/figures/tfhe-hpu-backend.excalidraw.png b/backends/tfhe-hpu-backend/figures/tfhe-hpu-backend.excalidraw.png new file mode 100644 index 0000000000000000000000000000000000000000..ea0f7571e0ac5ba1d687a7623827737470a3a2fb GIT binary patch literal 275359 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

-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

!s2vby@Ah~=xTkiI4}g;8;0xq! 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

f$4QM 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%0D(iwMwX#R& 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

(LlayVBgsT+8nACS2%vOTqmK| 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%0Dbe~_P7%KSc8#PBmLeGSn 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 X -e------------ + // + // Ksk is sliced in one slot face over x. + // This slice is then decomposed in rectancles lby*lbz. + // These rectangle are iterated in natural order. + // Within this rectangle lbZ coefs are merged in one 64b coefs + // and iterated over y dim. + // Furthermore it's possible that ksk polyhedron isn't a multiple of lbx/lby/lbz. + // Incomplete rectangle are then extend with xx and iterate as usual + + let mut hw_idx = 0; + for outer_x in (0..lwe_k + 1).step_by(ks_p.lbx) { + for inner_x in 0..ks_p.lbx { + // -> Iterate over Slices + let raw_x = outer_x + inner_x; + let abs_x = if raw_x < (lwe_k + 1) { + Some(raw_x) + } else { + None + }; + + for outer_y in (0..(glwe_k * glwe_n)).step_by(ks_p.lby) { + for outer_z in (0..pbs_p.ks_level).step_by(ks_p.lbz) { + // -> Iterate over rectangles lby*lbz + for inner_y in 0..ks_p.lby { + let raw_y = outer_y + inner_y; + let abs_y = if raw_y < (glwe_k * glwe_n) { + // Hw-order expect y-dim to be in bitreverse + // Compute it inflight + // NB: raw_y represent the index over Y in [0; glwe_k*glwe_n] and + // the bitreverse must be only + // applied over glwe_n + // -> split raw_y in poly_y, coef_y and bitreverse only the coef_y + let poly_y = raw_y / glwe_n; + let coef_y = raw_y % glwe_n; + let brev_coef_y = rb_conv.idx_rev(coef_y); + let abs_y = poly_y * glwe_n + brev_coef_y; + Some(abs_y) + } else { + None + }; + + let pack_z: u64 = (0..ks_p.lbz).fold(0, |acc, inner_z| { + let raw_z = outer_z + inner_z; + let abs_z = if raw_z < pbs_p.ks_level { + Some(raw_z) + } else { + None + }; + let cur_coef = match (abs_x, abs_y, abs_z) { + (Some(x), Some(y), Some(z)) => { + *KskIndex { x, y, z }.coef_view(&cpu_ksk) + } + _ => Scalar::ZERO, /* At least one dimension overflow + * -> return 0 */ + }; + // NB: In Sw, the information is kept in MSB, but Hw required them + // in LSB Handle bit alignment + let coef_ralign = { + let coef_orig: u64 = cur_coef.cast_into(); + coef_orig >> (Scalar::BITS - ks_p.width) + }; + // println!("@{inner_z} => 0x{acc:x} [0x{coef_rounded_ralign:x}]"); + acc + (coef_ralign << (inner_z * ks_p.width)) + }); + hpu_ksk[hw_idx] = pack_z; + hw_idx += 1; + } + } + } + } + } + hpu_ksk + } +} + +/// Shuffling KSK in HW order required custom coefs interleaving. +/// The following structure enable OutOfOrder access of KSK coefs to ease +/// the interleaving description +/// Abstract tfhe-rs view from hw view (i.e polyhedron) +#[derive(Debug)] +struct KskIndex { + pub x: usize, + pub y: usize, + pub z: usize, +} + +impl KskIndex { + /// Ease out of order iteration over a ksk coefs. + fn coef_view<'a, Scalar: UnsignedInteger>( + self, + ksk: &'a LweKeyswitchKeyView, + ) -> &'a Scalar { + let decomp_level = ksk.decomposition_level_count().0; + let in_lwe_elem = ksk.input_key_lwe_dimension().0; + // NB: Decomposition is in reverse order in tfhe-rs (i.e MSB to LSB) + // -> However, inversion is already handled during keyswitching key generation + // Ksk coefs is order as follow (from outer dim to inner dim): + // * input_lwe_key_dim + // * decomp_lvl + // * out_lwe_key_size + &ksk.as_ref() + .split_into(in_lwe_elem) + .nth(self.y) + .unwrap() + .split_into(decomp_level) + .nth(self.z) + .unwrap()[self.x] + } + + /// Ease out of order mutable iteration over a ksk coefs. + fn coef_mut_view<'a, Scalar: UnsignedInteger>( + self, + ksk: &'a mut LweKeyswitchKeyMutView, + ) -> &'a mut Scalar { + let decomp_level = ksk.decomposition_level_count().0; + let in_lwe_elem = ksk.input_key_lwe_dimension().0; + // NB: Decomposition is in reverse order in tfhe-rs (i.e MSB to LSB) + // -> However, inversion is already handled during keyswitching key generation + // Ksk coefs is order as follow (from outer dim to inner dim): + // * input_lwe_key_dim + // * decomp_lvl + // * out_lwe_key_size + &mut ksk + .as_mut() + .split_into(in_lwe_elem) + .nth(self.y) + .unwrap() + .split_into(decomp_level) + .nth(self.z) + .unwrap()[self.x] + } +} + +impl<'a, Scalar> From> for LweKeyswitchKeyOwned +where + Scalar: UnsignedInteger + CastFrom, +{ + fn from(hpu_ksk: HpuLweKeyswitchKeyView<'a, u64>) -> Self { + let pbs_p = &hpu_ksk.params().pbs_params; + let ks_p = &hpu_ksk.params().ks_params; + + let mut cpu_ksk = Self::new( + Scalar::ZERO, + DecompositionBaseLog(pbs_p.ks_base_log), + DecompositionLevelCount(pbs_p.ks_level), + LweDimension(pbs_p.glwe_dimension * pbs_p.polynomial_size), + LweDimension(pbs_p.lwe_dimension), + CiphertextModulus::new(1_u128 << ks_p.width), + ); + + // Unshuffle Keyswitch key from Hw order to Cpu order + + // Allocate radix_basis converter + let params = hpu_ksk.params(); + let rb_conv = order::RadixBasis::new(params.ntt_params.radix, params.ntt_params.stg_nb); + + // Extract params inner values for ease of writing + let pbs_p = ¶ms.pbs_params; + let lwe_k = pbs_p.lwe_dimension; + let glwe_k = pbs_p.glwe_dimension; + let glwe_n = pbs_p.polynomial_size; + let ks_p = ¶ms.ks_params; + + // Revert transformation made in FromWith + let mut hw_idx = 0; + for outer_x in (0..lwe_k + 1).step_by(ks_p.lbx) { + for inner_x in 0..ks_p.lbx { + // -> Iterate over Slices + let raw_x = outer_x + inner_x; + let abs_x = if raw_x < (lwe_k + 1) { + Some(raw_x) + } else { + None + }; + + for outer_y in (0..(glwe_k * glwe_n)).step_by(ks_p.lby) { + for outer_z in (0..pbs_p.ks_level).step_by(ks_p.lbz) { + // -> Iterate over rectangles lby*lbz + for inner_y in 0..ks_p.lby { + let raw_y = outer_y + inner_y; + let abs_y = if raw_y < (glwe_k * glwe_n) { + // Hw-order expect y-dim to be in bitreverse + // Compute it inflight + // NB: raw_y represent the index over Y in [0; glwe_k*glwe_n] and + // the bitreverse must be only + // applied over glwe_n + // -> split raw_y in poly_y, coef_y and bitreverse only the coef_y + let poly_y = raw_y / glwe_n; + let coef_y = raw_y % glwe_n; + let brev_coef_y = rb_conv.idx_rev(coef_y); + let abs_y = poly_y * glwe_n + brev_coef_y; + Some(abs_y) + } else { + None + }; + + // Unpack over Z dimension + (0..ks_p.lbz).for_each(|inner_z| { + let raw_z = outer_z + inner_z; + let abs_z = if raw_z < pbs_p.ks_level { + Some(raw_z) + } else { + None + }; + + if let (Some(x), Some(y), Some(z)) = (abs_x, abs_y, abs_z) { + let mut cpu_ksk_view = cpu_ksk.as_mut_view(); + let cpu_coef = + KskIndex { x, y, z }.coef_mut_view(&mut cpu_ksk_view); + let hpu_val = (hpu_ksk[hw_idx] >> (inner_z * ks_p.width)) + & ((1_u64 << ks_p.width) - 1); + // Cpu expect value MSB Align + *cpu_coef = + Scalar::cast_from(hpu_val << (Scalar::BITS - ks_p.width)); + } + // Otherwise, at least one dimension overflow, it's padded with 0 in + // the Hw view => Skipped + }); + hw_idx += 1; + } + } + } + } + } + cpu_ksk + } +} diff --git a/tfhe/src/core_crypto/hpu/entities/mod.rs b/tfhe/src/core_crypto/hpu/entities/mod.rs new file mode 100644 index 000000000..c5af582af --- /dev/null +++ b/tfhe/src/core_crypto/hpu/entities/mod.rs @@ -0,0 +1,10 @@ +use super::algorithms; + +// Export tfhe-hpu-backend type for use external crate +pub use tfhe_hpu_backend::prelude::*; + +pub mod glwe_ciphertext; +pub mod glwe_lookuptable; +pub mod lwe_bootstrap_key; +pub mod lwe_ciphertext; +pub mod lwe_keyswitch_key; diff --git a/tfhe/src/core_crypto/hpu/mod.rs b/tfhe/src/core_crypto/hpu/mod.rs new file mode 100644 index 000000000..59bb298be --- /dev/null +++ b/tfhe/src/core_crypto/hpu/mod.rs @@ -0,0 +1,3 @@ +pub mod algorithms; +pub mod entities; +pub use entities::*; diff --git a/tfhe/src/core_crypto/mod.rs b/tfhe/src/core_crypto/mod.rs index a15ef7c01..aaeb6aa58 100644 --- a/tfhe/src/core_crypto/mod.rs +++ b/tfhe/src/core_crypto/mod.rs @@ -20,6 +20,10 @@ pub mod fft_impl; #[cfg(feature = "gpu")] pub mod gpu; + +#[cfg(feature = "hpu")] +pub mod hpu; + #[cfg(test)] pub mod keycache; diff --git a/tfhe/src/high_level_api/array/dynamic/booleans.rs b/tfhe/src/high_level_api/array/dynamic/booleans.rs index ff8354105..3f3b0ade8 100644 --- a/tfhe/src/high_level_api/array/dynamic/booleans.rs +++ b/tfhe/src/high_level_api/array/dynamic/booleans.rs @@ -113,6 +113,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -140,6 +144,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -174,6 +182,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } diff --git a/tfhe/src/high_level_api/array/dynamic/signed.rs b/tfhe/src/high_level_api/array/dynamic/signed.rs index 8f3d7b800..4bd0831ce 100644 --- a/tfhe/src/high_level_api/array/dynamic/signed.rs +++ b/tfhe/src/high_level_api/array/dynamic/signed.rs @@ -195,6 +195,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -222,6 +226,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -346,6 +354,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } diff --git a/tfhe/src/high_level_api/array/dynamic/unsigned.rs b/tfhe/src/high_level_api/array/dynamic/unsigned.rs index 149e03706..b8d6eae10 100644 --- a/tfhe/src/high_level_api/array/dynamic/unsigned.rs +++ b/tfhe/src/high_level_api/array/dynamic/unsigned.rs @@ -202,6 +202,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -229,6 +233,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } @@ -353,6 +361,10 @@ where Some(Device::CudaGpu) => { panic!("Not supported by Cuda devices") } + #[cfg(feature = "hpu")] + Some(Device::Hpu) => { + panic!("Not supported by Hpu devices") + } None => { panic!("{}", crate::high_level_api::errors::UninitializedServerKey); } diff --git a/tfhe/src/high_level_api/array/mod.rs b/tfhe/src/high_level_api/array/mod.rs index fb46e3e4f..8099e1ea0 100644 --- a/tfhe/src/high_level_api/array/mod.rs +++ b/tfhe/src/high_level_api/array/mod.rs @@ -382,6 +382,10 @@ pub fn fhe_uint_array_eq(lhs: &[FheUint], rhs: &[FheUint] let result = gpu_key.key.key.all_eq_slices(&tmp_lhs, &tmp_rhs, streams); FheBool::new(result, gpu_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support Array yet.") + } }) } @@ -422,6 +426,10 @@ pub fn fhe_uint_array_contains_sub_slice( .contains_sub_slice(&tmp_lhs, &tmp_pattern, streams); FheBool::new(result, gpu_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support Array yet.") + } }) } @@ -461,6 +469,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda does not support FheBool dot product") } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support FheBool dot product") + } }) } diff --git a/tfhe/src/high_level_api/booleans/base.rs b/tfhe/src/high_level_api/booleans/base.rs index db05aac24..523e0b2ab 100644 --- a/tfhe/src/high_level_api/booleans/base.rs +++ b/tfhe/src/high_level_api/booleans/base.rs @@ -25,6 +25,11 @@ use std::borrow::Borrow; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; use tfhe_versionable::Versionize; +#[cfg(feature = "hpu")] +use crate::integer::hpu::ciphertext::HpuRadixCiphertext; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::*; + /// The FHE boolean data type. /// /// # Example @@ -122,6 +127,8 @@ impl FheBool { InnerBoolean::Cpu(ct) => ct.into_raw_parts(), #[cfg(feature = "gpu")] InnerBoolean::Cuda(_) => unreachable!(), + #[cfg(feature = "hpu")] + InnerBoolean::Hpu(_) => unreachable!(), } } @@ -225,6 +232,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda does not support if_then_else with clear input") } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support if_then_else with clear input") + } }) } } @@ -272,6 +283,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda does not support if_then_else with clear input") } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support if_then_else with clear input") + } }) } } @@ -319,6 +334,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda does not support if_then_else with clear input") } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support if_then_else with clear input") + } }) } } @@ -366,6 +385,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda does not support if_then_else with clear input") } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support if_then_else with clear input") + } }) } } @@ -395,6 +418,10 @@ impl ScalarIfThenElse<&Self, &Self> for FheBool { let boolean_inner = CudaBooleanBlock(inner); (InnerBoolean::Cuda(boolean_inner), cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support if_then_else with clear input") + } }); Self::new(ciphertext, tag) } @@ -432,6 +459,30 @@ where FheUint::new(inner, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_then = ct_then.ciphertext.on_hpu(device); + let hpu_else = ct_else.ciphertext.on_hpu(device); + let hpu_cond = self.ciphertext.on_hpu(device); + + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_IF_THEN_ELSE; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_then.clone(), hpu_else.clone(), hpu_cond.clone()], + &[], + ) + .pop() + .unwrap(); + FheUint::new(hpu_result, device.tag.clone()) + } }) } } @@ -465,6 +516,10 @@ impl IfThenElse> for FheBool { FheInt::new(inner, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support signed integers") + } }) } } @@ -492,6 +547,10 @@ impl IfThenElse for FheBool { let boolean_inner = CudaBooleanBlock(inner); (InnerBoolean::Cuda(boolean_inner), cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bool if then else") + } }); Self::new(ciphertext, tag) } @@ -550,6 +609,10 @@ where let ciphertext = InnerBoolean::Cuda(inner); Self::new(ciphertext, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support FheBool::eq") + } }) } @@ -592,6 +655,10 @@ where let ciphertext = InnerBoolean::Cuda(inner); Self::new(ciphertext, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support FheBool::ne") + } }) } } @@ -636,6 +703,10 @@ impl FheEq for FheBool { ); (InnerBoolean::Cuda(inner), cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support FheBool::eq with a bool") + } }); Self::new(ciphertext, tag) } @@ -679,6 +750,10 @@ impl FheEq for FheBool { ); (InnerBoolean::Cuda(inner), cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support FheBool::ne with a bool") + } }); Self::new(ciphertext, tag) } @@ -759,6 +834,10 @@ where cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitand (&)") + } }); FheBool::new(ciphertext, tag) } @@ -843,6 +922,10 @@ where cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitor (|)") + } }); FheBool::new(ciphertext, tag) } @@ -927,6 +1010,10 @@ where cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitxor (^)") + } }); FheBool::new(ciphertext, tag) } @@ -1003,6 +1090,10 @@ impl BitAnd for &FheBool { cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("hpu does not bitand (&) with a bool") + } }); FheBool::new(ciphertext, tag) } @@ -1079,6 +1170,10 @@ impl BitOr for &FheBool { cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("hpu does not bitor (|) with a bool") + } }); FheBool::new(ciphertext, tag) } @@ -1155,6 +1250,10 @@ impl BitXor for &FheBool { cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("hpu does not bitxor (^) with a bool") + } }); FheBool::new(ciphertext, tag) } @@ -1353,6 +1452,10 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitand assign (&=)") + } }); } } @@ -1396,6 +1499,10 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitor assign (|=)") + } }); } } @@ -1439,6 +1546,10 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitxor assign (^=)") + } }); } } @@ -1476,6 +1587,10 @@ impl BitAndAssign for FheBool { streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitand assign (&=) with a bool") + } }); } } @@ -1513,6 +1628,10 @@ impl BitOrAssign for FheBool { streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitor assign (|=) with a bool") + } }); } } @@ -1550,6 +1669,10 @@ impl BitXorAssign for FheBool { streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitor assign (^=) with a bool") + } }); } } @@ -1619,6 +1742,10 @@ impl std::ops::Not for &FheBool { cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitnot (!)") + } }); FheBool::new(ciphertext, tag) } diff --git a/tfhe/src/high_level_api/booleans/encrypt.rs b/tfhe/src/high_level_api/booleans/encrypt.rs index c339a2b8d..696f707ec 100644 --- a/tfhe/src/high_level_api/booleans/encrypt.rs +++ b/tfhe/src/high_level_api/booleans/encrypt.rs @@ -101,6 +101,10 @@ impl FheTryTrivialEncrypt for FheBool { )); (ct, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support trivial encryption") + } }); Ok(Self::new(ciphertext, tag)) } diff --git a/tfhe/src/high_level_api/booleans/inner.rs b/tfhe/src/high_level_api/booleans/inner.rs index 49b22edb3..56bbc74de 100644 --- a/tfhe/src/high_level_api/booleans/inner.rs +++ b/tfhe/src/high_level_api/booleans/inner.rs @@ -12,11 +12,22 @@ use crate::Device; use serde::{Deserializer, Serializer}; use tfhe_versionable::{Unversionize, UnversionizeError, Versionize, VersionizeOwned}; +#[cfg(feature = "hpu")] +use crate::high_level_api::keys::HpuTaggedDevice; +#[cfg(feature = "gpu")] +use crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock; +#[cfg(feature = "gpu")] +use crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext; +#[cfg(feature = "hpu")] +use crate::integer::hpu::ciphertext::HpuRadixCiphertext; + /// Enum that manages the current inner representation of a boolean. pub(in crate::high_level_api) enum InnerBoolean { Cpu(BooleanBlock), #[cfg(feature = "gpu")] Cuda(crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock), + #[cfg(feature = "hpu")] + Hpu(HpuRadixCiphertext), } impl Clone for InnerBoolean { @@ -27,6 +38,8 @@ impl Clone for InnerBoolean { Self::Cuda(inner) => { with_thread_local_cuda_streams(|streams| Self::Cuda(inner.duplicate(streams))) } + #[cfg(feature = "hpu")] + Self::Hpu(inner) => Self::Hpu(inner.clone()), } } } @@ -39,6 +52,8 @@ impl serde::Serialize for InnerBoolean { Self::Cpu(cpu_ct) => cpu_ct.serialize(serializer), #[cfg(feature = "gpu")] Self::Cuda(_) => self.on_cpu().serialize(serializer), + #[cfg(feature = "hpu")] + Self::Hpu(_) => self.on_cpu().serialize(serializer), } } } @@ -57,9 +72,7 @@ impl<'de> serde::Deserialize<'de> for InnerBoolean { // Only CPU data are serialized so we only versionize the CPU type. #[derive(serde::Serialize, serde::Deserialize)] #[cfg_attr(dylint_lib = "tfhe_lints", allow(serialize_without_versionize))] -pub(crate) struct InnerBooleanVersionOwned( - ::VersionedOwned, -); +pub(crate) struct InnerBooleanVersionOwned(::VersionedOwned); impl Versionize for InnerBoolean { type Versioned<'vers> = InnerBooleanVersionedOwned; @@ -85,7 +98,7 @@ impl Unversionize for InnerBoolean { fn unversionize(versioned: Self::VersionedOwned) -> Result { match versioned { InnerBooleanVersionedOwned::V0(v0) => { - let mut unversioned = Self::Cpu(crate::integer::BooleanBlock::unversionize(v0.0)?); + let mut unversioned = Self::Cpu(BooleanBlock::unversionize(v0.0)?); unversioned.move_to_device_of_server_key_if_set(); Ok(unversioned) } @@ -106,12 +119,21 @@ impl From for } } +#[cfg(feature = "hpu")] +impl From for InnerBoolean { + fn from(value: HpuRadixCiphertext) -> Self { + Self::Hpu(value) + } +} + impl InnerBoolean { pub(crate) fn current_device(&self) -> Device { match self { Self::Cpu(_) => Device::Cpu, #[cfg(feature = "gpu")] Self::Cuda(_) => Device::CudaGpu, + #[cfg(feature = "hpu")] + Self::Hpu(_) => Device::Hpu, } } @@ -126,6 +148,8 @@ impl InnerBoolean { MaybeCloned::Cloned(ct.to_boolean_block(streams)) }) } + #[cfg(feature = "hpu")] + Self::Hpu(ct) => MaybeCloned::Cloned(ct.to_boolean_block()), } } @@ -135,32 +159,38 @@ impl InnerBoolean { pub(crate) fn on_gpu( &self, streams: &CudaStreams, - ) -> MaybeCloned<'_, crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext> { - match self { - Self::Cpu(ct) => with_thread_local_cuda_streams(|streams| { - let ct_as_radix = crate::integer::RadixCiphertext::from(vec![ct.0.clone()]); - let cuda_ct = - crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext::from_radix_ciphertext( - &ct_as_radix, - streams, - ); - MaybeCloned::Cloned(cuda_ct) - }), - #[cfg(feature = "gpu")] - Self::Cuda(ct) => { - if ct.gpu_indexes() == streams.gpu_indexes() { - MaybeCloned::Borrowed(ct.as_ref()) - } else { - MaybeCloned::Cloned(ct.duplicate(streams).0) + ) -> MaybeCloned<'_, CudaUnsignedRadixCiphertext> { + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_radix = match self { + Self::Cuda(gpu_radix) => { + if gpu_radix.gpu_indexes() == streams.gpu_indexes() { + return MaybeCloned::Borrowed(&gpu_radix.0); } + return MaybeCloned::Cloned(gpu_radix.duplicate(streams).0); } - } + _ => self.on_cpu(), + }; + + let gpu_radix = CudaBooleanBlock::from_boolean_block(&cpu_radix, streams); + MaybeCloned::Cloned(gpu_radix.0) + } + + #[cfg(feature = "hpu")] + pub(crate) fn on_hpu(&self, device: &HpuTaggedDevice) -> MaybeCloned<'_, HpuRadixCiphertext> { + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_radix = match self { + Self::Hpu(hpu_radix) => return MaybeCloned::Borrowed(hpu_radix), + _ => self.on_cpu(), + }; + + let hpu_ct = HpuRadixCiphertext::from_boolean_ciphertext(&cpu_radix, &device.device); + MaybeCloned::Cloned(hpu_ct) } pub(crate) fn as_cpu_mut(&mut self) -> &mut BooleanBlock { match self { Self::Cpu(block) => block, - #[cfg(feature = "gpu")] + #[cfg(any(feature = "gpu", feature = "hpu"))] _ => { self.move_to_device(Device::Cpu); self.as_cpu_mut() @@ -170,84 +200,94 @@ impl InnerBoolean { #[cfg(feature = "gpu")] #[track_caller] - pub(crate) fn as_gpu_mut( - &mut self, - streams: &CudaStreams, - ) -> &mut crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext { + pub(crate) fn as_gpu_mut(&mut self, streams: &CudaStreams) -> &mut CudaUnsignedRadixCiphertext { use crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock; + let cpu_radix = if let Self::Cuda(cuda_ct) = self { + if cuda_ct.gpu_indexes() != streams.gpu_indexes() { + *cuda_ct = cuda_ct.duplicate(streams); + } + return &mut cuda_ct.0; + } else { + self.on_cpu() + }; + + let cuda_ct = CudaBooleanBlock::from_boolean_block(&cpu_radix, streams); + *self = Self::Cuda(cuda_ct); + let Self::Cuda(cuda_ct) = self else { + unreachable!() + }; + &mut cuda_ct.0 + } + + #[cfg(feature = "gpu")] + pub(crate) fn into_cpu(self) -> BooleanBlock { match self { - Self::Cpu(cpu_ct) => { - let ct_as_radix = crate::integer::RadixCiphertext::from(vec![cpu_ct.0.clone()]); - let cuda_ct = crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext::from_radix_ciphertext(&ct_as_radix, streams); - let cuda_ct = CudaBooleanBlock::from_cuda_radix_ciphertext(cuda_ct.ciphertext); - *self = Self::Cuda(cuda_ct); - let Self::Cuda(cuda_ct) = self else { - unreachable!() - }; - &mut cuda_ct.0 - } - Self::Cuda(cuda_ct) => { - if cuda_ct.gpu_indexes() != streams.gpu_indexes() { - *cuda_ct = cuda_ct.duplicate(streams); - } - &mut cuda_ct.0 + Self::Cpu(cpu_ct) => cpu_ct, + #[cfg(feature = "gpu")] + Self::Cuda(ct) => { + with_thread_local_cuda_streams_for_gpu_indexes(ct.gpu_indexes(), |streams| { + ct.to_boolean_block(streams) + }) } + #[cfg(feature = "hpu")] + Self::Hpu(hpu_ct) => hpu_ct.to_boolean_block(), } } #[cfg(feature = "gpu")] - pub(crate) fn into_gpu( - self, - streams: &CudaStreams, - ) -> crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock { - match self { - Self::Cpu(cpu_ct) => with_thread_local_cuda_streams(|streams| { - crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock::from_boolean_block( - &cpu_ct, streams, - ) - }), - Self::Cuda(ct) => ct.move_to_stream(streams), - } + pub(crate) fn into_gpu(self, streams: &CudaStreams) -> CudaBooleanBlock { + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_bool = match self { + Self::Cuda(gpu_bool) => return gpu_bool.move_to_stream(streams), + _ => self.into_cpu(), + }; + CudaBooleanBlock::from_boolean_block(&cpu_bool, streams) } #[allow(clippy::needless_pass_by_ref_mut)] - pub(crate) fn move_to_device(&mut self, device: Device) { - match (&self, device) { - (Self::Cpu(_), Device::Cpu) => { - // Nothing to do, we already are on the correct device - } + pub(crate) fn move_to_device(&mut self, target_device: Device) { + let current_device = self.current_device(); + + if current_device == target_device { #[cfg(feature = "gpu")] - (Self::Cuda(cuda_ct), Device::CudaGpu) => { - // We are on a GPU, but it may not be the correct one - let new = with_thread_local_cuda_streams(|streams| { - if cuda_ct.gpu_indexes() == streams.gpu_indexes() { - None - } else { - Some(cuda_ct.duplicate(streams)) + // We may not be on the correct Cuda device + if let Self::Cuda(cuda_ct) = self { + with_thread_local_cuda_streams(|streams| { + if cuda_ct.gpu_indexes() != streams.gpu_indexes() { + *cuda_ct = cuda_ct.duplicate(streams); } - }); - if let Some(ct) = new { - *self = Self::Cuda(ct); - } + }) + } + return; + } + + // The logic is that the common device is the CPU, all other devices + // know how to transfer from and to CPU. + + // So we first transfer to CPU + let cpu_ct = self.on_cpu(); + + // Then we can transfer the desired device + match target_device { + Device::Cpu => { + let _ = cpu_ct; } #[cfg(feature = "gpu")] - (Self::Cpu(ct), Device::CudaGpu) => { + Device::CudaGpu => { let new_inner = with_thread_local_cuda_streams(|streams| { crate::integer::gpu::ciphertext::boolean_value::CudaBooleanBlock::from_boolean_block( - ct, - streams, + &cpu_ct, streams, ) }); *self = Self::Cuda(new_inner); } - #[cfg(feature = "gpu")] - (Self::Cuda(ct), Device::Cpu) => { - let new_inner = - with_thread_local_cuda_streams_for_gpu_indexes(ct.gpu_indexes(), |streams| { - ct.to_boolean_block(streams) - }); - *self = Self::Cpu(new_inner); + #[cfg(feature = "hpu")] + Device::Hpu => { + let hpu_ct = global_state::with_thread_local_hpu_device(|device| { + HpuRadixCiphertext::from_boolean_ciphertext(&cpu_ct, &device.device) + }); + *self = Self::Hpu(hpu_ct); } } } diff --git a/tfhe/src/high_level_api/booleans/oprf.rs b/tfhe/src/high_level_api/booleans/oprf.rs index 270ef554a..f3a2836ed 100644 --- a/tfhe/src/high_level_api/booleans/oprf.rs +++ b/tfhe/src/high_level_api/booleans/oprf.rs @@ -13,7 +13,7 @@ use tfhe_csprng::seeders::Seed; impl FheBool { /// Generates an encrypted boolean /// taken uniformly using the given seed. - /// The encryted value is oblivious to the server. + /// The encrypted value is oblivious to the server. /// It can be useful to make server random generation deterministic. /// /// ```rust @@ -53,6 +53,10 @@ impl FheBool { cuda_key.tag.clone(), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support random bool generation") + } }); Self::new(ciphertext, tag) } diff --git a/tfhe/src/high_level_api/booleans/squashed_noise.rs b/tfhe/src/high_level_api/booleans/squashed_noise.rs index 3587680f0..70b9916b7 100644 --- a/tfhe/src/high_level_api/booleans/squashed_noise.rs +++ b/tfhe/src/high_level_api/booleans/squashed_noise.rs @@ -106,8 +106,8 @@ impl InnerSquashedNoiseBoolean { (Self::Cpu(_), Device::Cpu) => { // Nothing to do, we already are on the correct device } - #[cfg(feature = "gpu")] - _ => panic!("Cuda devices do not support noise squashing yet"), + #[cfg(any(feature = "gpu", feature = "hpu"))] + _ => panic!("Cuda/Hpu devices do not support noise squashing yet"), } } @@ -180,6 +180,10 @@ impl SquashNoise for FheBool { InternalServerKey::Cuda(_) => Err(crate::error!( "Cuda devices do not support noise squashing yet" )), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + Err(crate::error!("Hpu devices do not support noise squashing")) + } }) } } diff --git a/tfhe/src/high_level_api/compact_list.rs b/tfhe/src/high_level_api/compact_list.rs index 3a71ec403..6415b4a9c 100644 --- a/tfhe/src/high_level_api/compact_list.rs +++ b/tfhe/src/high_level_api/compact_list.rs @@ -199,7 +199,7 @@ impl CompactCiphertextList { inner, tag: self.tag.clone(), }), - #[cfg(feature = "gpu")] + #[cfg(any(feature = "gpu", feature = "hpu"))] Some(_) => Err(crate::Error::new("Expected a CPU server key".to_string())), }) } @@ -314,7 +314,7 @@ mod zk { inner: expander, tag: self.tag.clone(), }), - #[cfg(feature = "gpu")] + #[cfg(any(feature = "gpu", feature = "hpu"))] Some(_) => Err(crate::Error::new("Expected a CPU server key".to_string())), }) } @@ -346,7 +346,7 @@ mod zk { inner: expander, tag: self.tag.clone(), }), - #[cfg(feature = "gpu")] + #[cfg(any(feature = "gpu", feature = "hpu"))] Some(_) => Err(crate::Error::new("Expected a CPU server key".to_string())), }) } diff --git a/tfhe/src/high_level_api/compressed_ciphertext_list.rs b/tfhe/src/high_level_api/compressed_ciphertext_list.rs index 45849923c..cc18c6029 100644 --- a/tfhe/src/high_level_api/compressed_ciphertext_list.rs +++ b/tfhe/src/high_level_api/compressed_ciphertext_list.rs @@ -26,7 +26,7 @@ use crate::integer::gpu::ciphertext::CudaRadixCiphertext; use crate::named::Named; use crate::prelude::{CiphertextList, Tagged}; use crate::shortint::Ciphertext; -use crate::{FheBool, FheInt, FheUint, Tag}; +use crate::{Device, FheBool, FheInt, FheUint, Tag}; impl HlCompressible for FheUint { fn compress_into(self, messages: &mut Vec<(ToBeCompressed, DataKind)>) { @@ -42,6 +42,10 @@ impl HlCompressible for FheUint { let kind = DataKind::Unsigned(blocks.info.blocks.len()); messages.push((ToBeCompressed::Cuda(blocks), kind)); } + #[cfg(feature = "hpu")] + crate::high_level_api::integers::unsigned::RadixCiphertext::Hpu(_) => { + panic!("HPU does not support compression"); + } } } } @@ -74,6 +78,8 @@ impl HlCompressible for FheBool { let kind = DataKind::Boolean; messages.push((ToBeCompressed::Cuda(cuda_bool.0.ciphertext), kind)); } + #[cfg(feature = "hpu")] + InnerBoolean::Hpu(_) => panic!("HPU does not support compression"), } } } @@ -209,6 +215,10 @@ impl CompressedCiphertextListBuilder { } }) } + #[cfg(feature = "hpu")] + Some(InternalServerKey::Hpu(_)) => Err(crate::Error::new( + "Hpu does not support compression".to_string(), + )), None => Err(UninitializedServerKey.into()), }) } @@ -255,38 +265,45 @@ impl InnerCompressedCiphertextList { } } - fn move_to_device(&mut self, device: crate::Device) { - let new_value = match (&self, device) { - (Self::Cpu(_), crate::Device::Cpu) => None, + #[allow(clippy::needless_pass_by_ref_mut)] + fn move_to_device(&mut self, target_device: Device) { + let current_device = self.current_device(); + + if current_device == target_device { #[cfg(feature = "gpu")] - (Self::Cuda(cuda_ct), crate::Device::CudaGpu) => { + // We may not be on the correct Cuda device + if let Self::Cuda(cuda_ct) = self { with_thread_local_cuda_streams(|streams| { - if cuda_ct.gpu_indexes() == streams.gpu_indexes() { - None - } else { - Some(Self::Cuda(cuda_ct.duplicate(streams))) + if cuda_ct.gpu_indexes() != streams.gpu_indexes() { + *cuda_ct = cuda_ct.duplicate(streams); } }) } - #[cfg(feature = "gpu")] - (Self::Cuda(cuda_ct), crate::Device::Cpu) => { - let cpu_ct = with_thread_local_cuda_streams_for_gpu_indexes( - cuda_ct.gpu_indexes(), - |streams| cuda_ct.to_compressed_ciphertext_list(streams), - ); - Some(Self::Cpu(cpu_ct)) + return; + } + + // The logic is that the common device is the CPU, all other devices + // know how to transfer from and to CPU. + + // So we first transfer to CPU + let cpu_ct = self.on_cpu(); + + // Then we can transfer the desired device + match target_device { + Device::Cpu => { + let _ = cpu_ct; } #[cfg(feature = "gpu")] - (Self::Cpu(cpu_ct), crate::Device::CudaGpu) => { - let cuda_ct = with_thread_local_cuda_streams(|streams| { + Device::CudaGpu => { + let new_inner = with_thread_local_cuda_streams(|streams| { cpu_ct.to_cuda_compressed_ciphertext_list(streams) }); - Some(Self::Cuda(cuda_ct)) + *self = Self::Cuda(new_inner); + } + #[cfg(feature = "hpu")] + Device::Hpu => { + panic!("HPU does not support compression"); } - }; - - if let Some(v) = new_value { - *self = v; } } @@ -468,6 +485,10 @@ impl CiphertextList for CompressedCiphertextList { } ct }), + #[cfg(feature = "hpu")] + Some(InternalServerKey::Hpu(_)) => { + panic!("HPU does not support compression"); + } None => Err(UninitializedServerKey.into()), }) } diff --git a/tfhe/src/high_level_api/config.rs b/tfhe/src/high_level_api/config.rs index d5cabaf53..a8ede6b54 100644 --- a/tfhe/src/high_level_api/config.rs +++ b/tfhe/src/high_level_api/config.rs @@ -13,6 +13,13 @@ pub struct Config { } impl Config { + #[cfg(feature = "hpu")] + pub fn from_hpu_device(hpu_device: &tfhe_hpu_backend::prelude::HpuDevice) -> Self { + let pbs_params = + crate::shortint::parameters::KeySwitch32PBSParameters::from(hpu_device.params()); + ConfigBuilder::with_custom_parameters(pbs_params).build() + } + pub fn public_key_encryption_parameters( &self, ) -> Result diff --git a/tfhe/src/high_level_api/global_state.rs b/tfhe/src/high_level_api/global_state.rs index f4f3e9a3d..e4e0fb1d4 100644 --- a/tfhe/src/high_level_api/global_state.rs +++ b/tfhe/src/high_level_api/global_state.rs @@ -129,11 +129,7 @@ pub(in crate::high_level_api) fn device_of_internal_keys() -> Option crate::Device::Cpu, - #[cfg(feature = "gpu")] - InternalServerKey::Cuda(_) => crate::Device::CudaGpu, - }) + cell.as_ref().map(InternalServerKey::device) }) } @@ -146,6 +142,8 @@ pub(in crate::high_level_api) fn tag_of_internal_server_key() -> crate::Result cpu_key.tag.clone(), #[cfg(feature = "gpu")] InternalServerKey::Cuda(cuda_key) => cuda_key.tag.clone(), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(hpu_device) => hpu_device.tag.clone(), }) }) } @@ -162,13 +160,15 @@ where .as_ref() .ok_or(UninitializedServerKey) .unwrap_display(); - match key { - InternalServerKey::Cpu(key) => func(key), - #[cfg(feature = "gpu")] - InternalServerKey::Cuda(_) => { - panic!("Cpu key requested but only cuda key is available") - } - } + #[allow(irrefutable_let_patterns, reason = "It depends on hardware features")] + let InternalServerKey::Cpu(cpu_key) = key + else { + panic!( + "Cpu key requested but only the key for {:?} is available", + key.device() + ) + }; + func(cpu_key) }) } @@ -185,12 +185,13 @@ where .as_ref() .ok_or(UninitializedServerKey) .unwrap_display(); - match key { - InternalServerKey::Cuda(key) => func(key), - InternalServerKey::Cpu(_) => { - panic!("Cuda key requested but only cpu key is available") - } - } + let InternalServerKey::Cuda(cuda_key) = key else { + panic!( + "Cpu key requested but only the key for {:?} is available", + key.device() + ) + }; + func(cuda_key) }) } @@ -307,3 +308,27 @@ mod gpu { } } } + +#[cfg(feature = "hpu")] +pub(in crate::high_level_api) use hpu::with_thread_local_hpu_device; + +#[cfg(feature = "hpu")] +mod hpu { + use super::*; + + use crate::high_level_api::keys::HpuTaggedDevice; + + use super::INTERNAL_KEYS; + + pub(in crate::high_level_api) fn with_thread_local_hpu_device(func: F) -> R + where + F: FnOnce(&HpuTaggedDevice) -> R, + { + INTERNAL_KEYS.with_borrow(|keys| { + let Some(InternalServerKey::Hpu(device)) = keys else { + panic!("Hpu device was requested but it is not available") + }; + func(device) + }) + } +} diff --git a/tfhe/src/high_level_api/integers/oprf.rs b/tfhe/src/high_level_api/integers/oprf.rs index 1c40f5e70..1fb33dec2 100644 --- a/tfhe/src/high_level_api/integers/oprf.rs +++ b/tfhe/src/high_level_api/integers/oprf.rs @@ -6,6 +6,7 @@ use crate::high_level_api::keys::InternalServerKey; #[cfg(feature = "gpu")] use crate::integer::gpu::ciphertext::{CudaSignedRadixCiphertext, CudaUnsignedRadixCiphertext}; use crate::{FheInt, Seed}; + impl FheUint { /// Generates an encrypted unsigned integer /// taken uniformly in its full range using the given seed. @@ -50,6 +51,10 @@ impl FheUint { Self::new(d_ct, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } /// Generates an encrypted `num_block` blocks unsigned integer @@ -99,6 +104,10 @@ impl FheUint { ); Self::new(d_ct, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -148,6 +157,10 @@ impl FheInt { Self::new(d_ct, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -199,6 +212,10 @@ impl FheInt { ); Self::new(d_ct, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/signed/base.rs b/tfhe/src/high_level_api/integers/signed/base.rs index 675b19e94..3d989607c 100644 --- a/tfhe/src/high_level_api/integers/signed/base.rs +++ b/tfhe/src/high_level_api/integers/signed/base.rs @@ -204,6 +204,10 @@ where .abs(&*self.ciphertext.on_gpu(streams), streams); Self::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -240,6 +244,10 @@ where .is_even(&*self.ciphertext.on_gpu(streams), streams); FheBool::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -276,6 +284,10 @@ where .is_odd(&*self.ciphertext.on_gpu(streams), streams); FheBool::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -321,6 +333,10 @@ where ); crate::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -366,6 +382,10 @@ where ); crate::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -411,6 +431,10 @@ where ); crate::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -456,6 +480,10 @@ where ); crate::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -493,6 +521,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support count_ones yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -530,6 +562,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support count_zeros yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -577,6 +613,10 @@ where ); crate::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -634,6 +674,10 @@ where FheBool::new(is_ok, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -708,6 +752,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support reverse yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -755,6 +803,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support if_then_else yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet."); + } }) } @@ -816,6 +868,10 @@ where ); Self::new(new_ciphertext, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -860,6 +916,10 @@ where ); Self::new(new_ciphertext, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -907,6 +967,10 @@ where ); Self::new(inner, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/signed/encrypt.rs b/tfhe/src/high_level_api/integers/signed/encrypt.rs index d4e284fd5..446331d15 100644 --- a/tfhe/src/high_level_api/integers/signed/encrypt.rs +++ b/tfhe/src/high_level_api/integers/signed/encrypt.rs @@ -121,6 +121,8 @@ where ); Ok(Self::new(inner, cuda_key.tag.clone())) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => panic!("Hpu does not currently support signed operation"), }) } } diff --git a/tfhe/src/high_level_api/integers/signed/inner.rs b/tfhe/src/high_level_api/integers/signed/inner.rs index 7b24f6d0f..85a98cde1 100644 --- a/tfhe/src/high_level_api/integers/signed/inner.rs +++ b/tfhe/src/high_level_api/integers/signed/inner.rs @@ -240,12 +240,15 @@ impl SignedRadixCiphertext { } #[cfg(feature = "gpu")] (Self::Cuda(ct), Device::Cpu) => { - let new_inner = - with_thread_local_cuda_streams_for_gpu_indexes(ct.gpu_indexes(), |streams| { - ct.to_signed_radix_ciphertext(streams) - }); + let new_inner = with_thread_local_cuda_streams(|streams| { + ct.to_signed_radix_ciphertext(streams) + }); *self = Self::Cpu(new_inner); } + #[cfg(feature = "hpu")] + (_, Device::Hpu) => { + panic!("Hpu device do not support signed integer yet",) + } } } diff --git a/tfhe/src/high_level_api/integers/signed/ops.rs b/tfhe/src/high_level_api/integers/signed/ops.rs index ebac40a4d..f388aaee0 100644 --- a/tfhe/src/high_level_api/integers/signed/ops.rs +++ b/tfhe/src/high_level_api/integers/signed/ops.rs @@ -106,6 +106,10 @@ where Self::new(inner, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -152,6 +156,10 @@ where ); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -198,6 +206,10 @@ where ); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -255,6 +267,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -294,6 +310,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -359,6 +379,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -398,6 +422,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -437,6 +465,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -476,6 +508,10 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -561,6 +597,10 @@ where FheInt::::new(r, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -640,6 +680,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -683,6 +727,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -726,6 +774,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -767,6 +819,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -808,6 +864,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -849,6 +909,10 @@ generic_integer_impl_operation!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -899,6 +963,10 @@ generic_integer_impl_operation!( .div(&*lhs.ciphertext.on_gpu(streams), &*rhs.ciphertext.on_gpu(streams), streams); FheInt::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -950,6 +1018,10 @@ generic_integer_impl_operation!( .rem(&*lhs.ciphertext.on_gpu(streams), &*rhs.ciphertext.on_gpu(streams), streams); FheInt::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1063,6 +1135,10 @@ generic_integer_impl_shift_rotate!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1107,6 +1183,10 @@ generic_integer_impl_shift_rotate!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1151,6 +1231,10 @@ generic_integer_impl_shift_rotate!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1195,6 +1279,10 @@ generic_integer_impl_shift_rotate!( FheInt::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1247,6 +1335,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1294,6 +1386,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1341,6 +1437,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1386,6 +1486,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1431,6 +1535,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1476,6 +1584,10 @@ where ); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1528,6 +1640,10 @@ where *cuda_lhs = cuda_result; }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1580,6 +1696,10 @@ where *cuda_lhs = cuda_result; }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1635,6 +1755,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1689,6 +1813,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1744,6 +1872,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1799,6 +1931,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1870,6 +2006,10 @@ where .neg(&*self.ciphertext.on_gpu(streams), streams); FheInt::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1939,6 +2079,10 @@ where .bitnot(&*self.ciphertext.on_gpu(streams), streams); FheInt::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/signed/overflowing_ops.rs b/tfhe/src/high_level_api/integers/signed/overflowing_ops.rs index 840d1c443..097cc2834 100644 --- a/tfhe/src/high_level_api/integers/signed/overflowing_ops.rs +++ b/tfhe/src/high_level_api/integers/signed/overflowing_ops.rs @@ -64,6 +64,10 @@ where FheBool::new(overflow, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -160,6 +164,10 @@ where FheBool::new(overflow, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -294,6 +302,10 @@ where FheBool::new(overflow, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -389,6 +401,10 @@ where FheBool::new(overflow, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -477,6 +493,10 @@ where InternalServerKey::Cuda(_) => { todo!("Cuda devices do not support signed integer"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/signed/scalar_ops.rs b/tfhe/src/high_level_api/integers/signed/scalar_ops.rs index 6dc5aa41c..00713c02d 100644 --- a/tfhe/src/high_level_api/integers/signed/scalar_ops.rs +++ b/tfhe/src/high_level_api/integers/signed/scalar_ops.rs @@ -64,6 +64,10 @@ where Self::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -112,6 +116,10 @@ where Self::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -159,6 +167,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -200,6 +212,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -246,6 +262,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -286,6 +306,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -326,6 +350,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -366,6 +394,10 @@ where FheBool::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -422,6 +454,10 @@ macro_rules! generic_integer_impl_scalar_div_rem { <$concrete_type>::new(r, cuda_key.tag.clone()), ) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -463,6 +499,10 @@ macro_rules! define_scalar_rotate_shifts { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -492,6 +532,10 @@ macro_rules! define_scalar_rotate_shifts { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -521,6 +565,10 @@ macro_rules! define_scalar_rotate_shifts { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -550,6 +598,10 @@ macro_rules! define_scalar_rotate_shifts { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -576,6 +628,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_left_shift_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -602,6 +658,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_right_shift_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -628,6 +688,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_rotate_left_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -654,6 +718,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_rotate_right_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -769,6 +837,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -798,6 +870,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -827,6 +903,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -857,6 +937,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -886,6 +970,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -916,6 +1004,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -945,6 +1037,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -975,6 +1071,10 @@ macro_rules! define_scalar_ops { }); SignedRadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1021,6 +1121,10 @@ macro_rules! define_scalar_ops { SignedRadixCiphertext::Cuda(result) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1115,6 +1219,10 @@ macro_rules! define_scalar_ops { .scalar_add_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1146,6 +1254,10 @@ macro_rules! define_scalar_ops { .scalar_sub_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1173,6 +1285,10 @@ macro_rules! define_scalar_ops { .scalar_mul_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1201,6 +1317,10 @@ macro_rules! define_scalar_ops { .scalar_bitand_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1228,6 +1348,10 @@ macro_rules! define_scalar_ops { .scalar_bitor_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1254,6 +1378,10 @@ macro_rules! define_scalar_ops { .scalar_bitxor_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1278,7 +1406,11 @@ macro_rules! define_scalar_ops { let cuda_lhs = lhs.ciphertext.as_gpu_mut(streams); let cuda_result = cuda_key.pbs_key().signed_scalar_div(&cuda_lhs, rhs, streams); *cuda_lhs = cuda_result; - }) + }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1303,7 +1435,11 @@ macro_rules! define_scalar_ops { let cuda_lhs = lhs.ciphertext.as_gpu_mut(streams); let cuda_result = cuda_key.pbs_key().signed_scalar_rem(&cuda_lhs, rhs, streams); *cuda_lhs = cuda_result; - }) + }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, diff --git a/tfhe/src/high_level_api/integers/signed/squashed_noise.rs b/tfhe/src/high_level_api/integers/signed/squashed_noise.rs index 7459014ea..a15e51c7e 100644 --- a/tfhe/src/high_level_api/integers/signed/squashed_noise.rs +++ b/tfhe/src/high_level_api/integers/signed/squashed_noise.rs @@ -114,8 +114,8 @@ impl InnerSquashedNoiseSignedRadixCiphertext { (Self::Cpu(_), Device::Cpu) => { // Nothing to do, we already are on the correct device } - #[cfg(feature = "gpu")] - _ => panic!("Cuda devices do not support noise squashing yet"), + #[cfg(any(feature = "gpu", feature = "hpu"))] + _ => panic!("Cuda/Hpu devices do not support noise squashing yet"), } } @@ -199,6 +199,10 @@ impl SquashNoise for FheInt { InternalServerKey::Cuda(_) => Err(crate::error!( "Cuda devices do not support noise squashing yet" )), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + Err(crate::error!("Hpu devices do not support noise squashing")) + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/base.rs b/tfhe/src/high_level_api/integers/unsigned/base.rs index 972ff5423..9aec3a358 100644 --- a/tfhe/src/high_level_api/integers/unsigned/base.rs +++ b/tfhe/src/high_level_api/integers/unsigned/base.rs @@ -9,7 +9,7 @@ use crate::high_level_api::global_state::with_thread_local_cuda_streams; use crate::high_level_api::integers::signed::{FheInt, FheIntId}; use crate::high_level_api::integers::IntegerId; use crate::high_level_api::keys::InternalServerKey; -use crate::high_level_api::traits::Tagged; +use crate::high_level_api::traits::{FheWait, Tagged}; use crate::high_level_api::{global_state, Device}; use crate::integer::block_decomposition::{DecomposableInto, RecomposableFrom}; #[cfg(feature = "gpu")] @@ -25,6 +25,11 @@ use crate::GpuIndex; use crate::{FheBool, ServerKey, Tag}; use std::marker::PhantomData; +#[cfg(feature = "hpu")] +use crate::high_level_api::traits::{FheHpu, HpuHandle}; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::*; + #[derive(Debug)] pub enum GenericIntegerBlockError { NumberOfBlocks(usize, usize), @@ -147,6 +152,56 @@ where } } +impl FheWait for FheUint +where + Id: FheUintId, +{ + fn wait(&self) { + self.ciphertext.wait() + } +} + +#[cfg(feature = "hpu")] +impl FheHpu for FheUint +where + Id: FheUintId, +{ + fn iop_exec(iop: &hpu_asm::AsmIOpcode, src: HpuHandle<&Self>) -> HpuHandle { + use crate::integer::hpu::ciphertext::HpuRadixCiphertext; + global_state::with_thread_local_hpu_device(|device| { + let mut srcs = Vec::new(); + for n in src.native.iter() { + srcs.push(n.ciphertext.on_hpu(device).clone()); + } + for b in src.boolean.iter() { + srcs.push(b.ciphertext.on_hpu(device).clone()); + } + + let (opcode, proto) = { + ( + iop.opcode(), + &iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_res = HpuRadixCiphertext::exec(proto, opcode, &srcs, &src.imm); + HpuHandle { + native: hpu_res + .iter() + .filter(|x| !x.0.is_boolean()) + .map(|x| Self::new(x.clone(), device.tag.clone())) + .collect::>(), + boolean: hpu_res + .iter() + .filter(|x| x.0.is_boolean()) + .map(|x| FheBool::new(x.clone(), device.tag.clone())) + .collect::>(), + imm: Vec::new(), + } + }) + } +} + impl FheUint where Id: FheUintId, @@ -217,12 +272,12 @@ where /// slice is empty #[cfg(feature = "gpu")] pub fn gpu_indexes(&self) -> &[GpuIndex] { + #[allow(clippy::match_wildcard_for_single_variants)] match &self.ciphertext { - RadixCiphertext::Cpu(_) => &[], RadixCiphertext::Cuda(cuda_ct) => cuda_ct.gpu_indexes(), + _ => &[], } } - /// Returns a FheBool that encrypts `true` if the value is even /// /// # Example @@ -256,6 +311,10 @@ where .is_even(&*self.ciphertext.on_gpu(streams), streams); FheBool::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -292,6 +351,10 @@ where .is_odd(&*self.ciphertext.on_gpu(streams), streams); FheBool::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -430,6 +493,10 @@ where ); super::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -475,6 +542,10 @@ where ); super::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -520,6 +591,10 @@ where ); super::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -565,6 +640,10 @@ where ); super::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -602,6 +681,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support count_ones yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -639,6 +722,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support count_zeros yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -686,6 +773,10 @@ where ); super::FheUint32::new(result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -743,6 +834,10 @@ where FheBool::new(is_ok, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -826,6 +921,10 @@ where Err(crate::Error::new("Output type does not have enough bits to represent all possible output values".to_string())) } }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -902,6 +1001,10 @@ where Err(crate::Error::new("Output type does not have enough bits to represent all possible output values".to_string())) } }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -938,6 +1041,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support reverse yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -984,6 +1091,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support if_then_else yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet."); + } }) } @@ -1023,6 +1134,10 @@ where cuda_key.key.key.carry_modulus, cuda_key.key.key.message_modulus, ), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }); // Check number of blocks @@ -1109,6 +1224,10 @@ where ); Self::new(casted, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1153,6 +1272,10 @@ where ); Self::new(casted, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1197,6 +1320,10 @@ where ); Self::new(inner, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/encrypt.rs b/tfhe/src/high_level_api/integers/unsigned/encrypt.rs index 593e580e6..a67ff9839 100644 --- a/tfhe/src/high_level_api/integers/unsigned/encrypt.rs +++ b/tfhe/src/high_level_api/integers/unsigned/encrypt.rs @@ -123,6 +123,10 @@ where ); Ok(Self::new(inner, cuda_key.tag.clone())) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support trivial encryption") + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/inner.rs b/tfhe/src/high_level_api/integers/unsigned/inner.rs index 2ca75142f..b2789752d 100644 --- a/tfhe/src/high_level_api/integers/unsigned/inner.rs +++ b/tfhe/src/high_level_api/integers/unsigned/inner.rs @@ -7,16 +7,24 @@ use crate::high_level_api::global_state; use crate::high_level_api::global_state::{ with_thread_local_cuda_streams, with_thread_local_cuda_streams_for_gpu_indexes, }; +#[cfg(feature = "hpu")] +use crate::high_level_api::keys::HpuTaggedDevice; #[cfg(feature = "gpu")] use crate::integer::gpu::ciphertext::{CudaIntegerRadixCiphertext, CudaUnsignedRadixCiphertext}; +#[cfg(feature = "hpu")] +use crate::integer::hpu::ciphertext::HpuRadixCiphertext; use crate::Device; use serde::{Deserializer, Serializer}; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::*; use tfhe_versionable::{Unversionize, UnversionizeError, Versionize, VersionizeOwned}; pub(crate) enum RadixCiphertext { Cpu(crate::integer::RadixCiphertext), #[cfg(feature = "gpu")] Cuda(crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext), + #[cfg(feature = "hpu")] + Hpu(HpuRadixCiphertext), } impl From for RadixCiphertext { @@ -32,6 +40,13 @@ impl From for Radi } } +#[cfg(feature = "hpu")] +impl From for RadixCiphertext { + fn from(value: HpuRadixCiphertext) -> Self { + Self::Hpu(value) + } +} + impl Clone for RadixCiphertext { fn clone(&self) -> Self { match self { @@ -40,6 +55,24 @@ impl Clone for RadixCiphertext { Self::Cuda(inner) => { with_thread_local_cuda_streams(|streams| Self::Cuda(inner.duplicate(streams))) } + #[cfg(feature = "hpu")] + Self::Hpu(inner) => { + // NB: Hpu backends flavor behavs differently regarding memory. + // Some of them has duplicated memory on Host with sync mechanism. + // But it's not the case for all. + // To prevent special cases, all the "deep" clone are made on HPU side + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_MEMCPY; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + let deep_clone = HpuRadixCiphertext::exec(proto, opcode, &[inner.clone()], &[]) + .pop() + .expect("IOP_MEMCPY must return 1 operand"); + Self::Hpu(deep_clone) + } } } } @@ -107,11 +140,23 @@ impl Unversionize for RadixCiphertext { } impl RadixCiphertext { + pub(crate) fn wait(&self) { + match self { + Self::Cpu(_) => {} + #[cfg(feature = "gpu")] + Self::Cuda(_) => {} + #[cfg(feature = "hpu")] + Self::Hpu(hpu_ct) => hpu_ct.0.wait(), + } + } + pub(crate) fn current_device(&self) -> Device { match self { Self::Cpu(_) => Device::Cpu, #[cfg(feature = "gpu")] Self::Cuda(_) => Device::CudaGpu, + #[cfg(feature = "hpu")] + Self::Hpu(_) => Device::Hpu, } } @@ -121,11 +166,14 @@ impl RadixCiphertext { match self { Self::Cpu(ct) => MaybeCloned::Borrowed(ct), #[cfg(feature = "gpu")] - Self::Cuda(ct) => { - with_thread_local_cuda_streams_for_gpu_indexes(ct.gpu_indexes(), |streams| { - let cpu_ct = ct.to_radix_ciphertext(streams); - MaybeCloned::Cloned(cpu_ct) - }) + Self::Cuda(ct) => with_thread_local_cuda_streams(|streams| { + let cpu_ct = ct.to_radix_ciphertext(streams); + MaybeCloned::Cloned(cpu_ct) + }), + #[cfg(feature = "hpu")] + Self::Hpu(hpu_ct) => { + let cpu_inner = hpu_ct.to_radix_ciphertext(); + MaybeCloned::Cloned(cpu_inner) } } } @@ -136,30 +184,38 @@ impl RadixCiphertext { pub(crate) fn on_gpu( &self, streams: &CudaStreams, - ) -> MaybeCloned<'_, crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext> { - match self { - Self::Cpu(ct) => with_thread_local_cuda_streams(|streams| { - let ct = - crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext::from_radix_ciphertext( - ct, streams, - ); - MaybeCloned::Cloned(ct) - }), - #[cfg(feature = "gpu")] - Self::Cuda(ct) => { - if ct.gpu_indexes() == streams.gpu_indexes() { - MaybeCloned::Borrowed(ct) - } else { - MaybeCloned::Cloned(ct.duplicate(streams)) + ) -> MaybeCloned<'_, CudaUnsignedRadixCiphertext> { + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_radix = match self { + Self::Cuda(gpu_radix) => { + if gpu_radix.gpu_indexes() == streams.gpu_indexes() { + return MaybeCloned::Borrowed(gpu_radix); } + return MaybeCloned::Cloned(gpu_radix.duplicate(streams)); } - } + _ => self.on_cpu(), + }; + + let gpu_radix = CudaUnsignedRadixCiphertext::from_radix_ciphertext(&cpu_radix, streams); + MaybeCloned::Cloned(gpu_radix) + } + + #[cfg(feature = "hpu")] + pub(crate) fn on_hpu(&self, device: &HpuTaggedDevice) -> MaybeCloned<'_, HpuRadixCiphertext> { + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_radix = match self { + Self::Hpu(hpu_radix) => return MaybeCloned::Borrowed(hpu_radix), + _ => self.on_cpu(), + }; + + let hpu_ct = HpuRadixCiphertext::from_radix_ciphertext(&cpu_radix, &device.device); + MaybeCloned::Cloned(hpu_ct) } pub(crate) fn as_cpu_mut(&mut self) -> &mut crate::integer::RadixCiphertext { match self { Self::Cpu(radix_ct) => radix_ct, - #[cfg(feature = "gpu")] + #[cfg(any(feature = "gpu", feature = "hpu"))] _ => { self.move_to_device(Device::Cpu); self.as_cpu_mut() @@ -168,25 +224,36 @@ impl RadixCiphertext { } #[cfg(feature = "gpu")] - pub(crate) fn as_gpu_mut( - &mut self, - streams: &CudaStreams, - ) -> &mut crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext { - match self { - Self::Cpu(cpu_ct) => { - let cuda_ct = crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext::from_radix_ciphertext(cpu_ct, streams); - *self = Self::Cuda(cuda_ct); - let Self::Cuda(cuda_ct) = self else { - unreachable!() - }; - cuda_ct - } - Self::Cuda(cuda_ct) => { - if cuda_ct.gpu_indexes() != streams.gpu_indexes() { - *cuda_ct = cuda_ct.duplicate(streams); - } - cuda_ct + pub(crate) fn as_gpu_mut(&mut self, streams: &CudaStreams) -> &mut CudaUnsignedRadixCiphertext { + let cpu_radix = if let Self::Cuda(cuda_ct) = self { + if cuda_ct.gpu_indexes() != streams.gpu_indexes() { + *cuda_ct = cuda_ct.duplicate(streams); } + return cuda_ct; + } else { + self.on_cpu() + }; + + let cuda_ct = CudaUnsignedRadixCiphertext::from_radix_ciphertext(&cpu_radix, streams); + *self = Self::Cuda(cuda_ct); + let Self::Cuda(cuda_ct) = self else { + unreachable!() + }; + cuda_ct + } + + #[cfg(feature = "hpu")] + pub(crate) fn as_hpu_mut(&mut self, device: &HpuTaggedDevice) -> &mut HpuRadixCiphertext { + if let Self::Hpu(radix_ct) = self { + radix_ct + } else { + let cpu_ct = self.on_cpu(); + let hpu_ct = HpuRadixCiphertext::from_radix_ciphertext(&cpu_ct, &device.device); + *self = Self::Hpu(hpu_ct); + let Self::Hpu(hpu_ct) = self else { + unreachable!() + }; + hpu_ct } } @@ -199,55 +266,74 @@ impl RadixCiphertext { ct.to_radix_ciphertext(streams) }) } + #[cfg(feature = "hpu")] + Self::Hpu(hpu_ct) => hpu_ct.to_radix_ciphertext(), } } #[cfg(feature = "gpu")] pub(crate) fn into_gpu(self, streams: &CudaStreams) -> CudaUnsignedRadixCiphertext { - match self { - Self::Cpu(cpu_ct) => { - CudaUnsignedRadixCiphertext::from_radix_ciphertext(&cpu_ct, streams) - } - Self::Cuda(ct) => ct.move_to_stream(streams), + #[allow(clippy::match_wildcard_for_single_variants)] + let cpu_radix = match self { + Self::Cuda(gpu_radix) => return gpu_radix.move_to_stream(streams), + _ => self.into_cpu(), + }; + CudaUnsignedRadixCiphertext::from_radix_ciphertext(&cpu_radix, streams) + } + + #[cfg(feature = "hpu")] + pub(crate) fn into_hpu(self, device: &HpuTaggedDevice) -> HpuRadixCiphertext { + if let Self::Hpu(radix_ct) = self { + radix_ct + } else { + let cpu_ct = self.on_cpu(); + HpuRadixCiphertext::from_radix_ciphertext(&cpu_ct, &device.device) } } #[allow(clippy::needless_pass_by_ref_mut)] - pub(crate) fn move_to_device(&mut self, device: Device) { - match (&self, device) { - (Self::Cpu(_), Device::Cpu) => { - // Nothing to do, we already are on the correct device - } + pub(crate) fn move_to_device(&mut self, target_device: Device) { + let current_device = self.current_device(); + + if current_device == target_device { #[cfg(feature = "gpu")] - (Self::Cuda(cuda_ct), Device::CudaGpu) => { - // We are on a GPU, but it may not be the correct one - let new = with_thread_local_cuda_streams(|streams| { - if cuda_ct.gpu_indexes() == streams.gpu_indexes() { - None - } else { - Some(cuda_ct.duplicate(streams)) + // We may not be on the correct Cuda device + if let Self::Cuda(cuda_ct) = self { + with_thread_local_cuda_streams(|streams| { + if cuda_ct.gpu_indexes() != streams.gpu_indexes() { + *cuda_ct = cuda_ct.duplicate(streams); } - }); - if let Some(ct) = new { - *self = Self::Cuda(ct); - } + }) + } + return; + } + + // The logic is that the common device is the CPU, all other devices + // know how to transfer from and to CPU. + + // So we first transfer to CPU + let cpu_ct = self.on_cpu(); + + // Then we can transfer the desired device + match target_device { + Device::Cpu => { + let _ = cpu_ct; } #[cfg(feature = "gpu")] - (Self::Cpu(ct), Device::CudaGpu) => { + Device::CudaGpu => { let new_inner = with_thread_local_cuda_streams(|streams| { crate::integer::gpu::ciphertext::CudaUnsignedRadixCiphertext::from_radix_ciphertext( - ct, streams, + &cpu_ct, streams, ) }); *self = Self::Cuda(new_inner); } - #[cfg(feature = "gpu")] - (Self::Cuda(ct), Device::Cpu) => { - let new_inner = - with_thread_local_cuda_streams_for_gpu_indexes(ct.gpu_indexes(), |streams| { - ct.to_radix_ciphertext(streams) - }); - *self = Self::Cpu(new_inner); + #[cfg(feature = "hpu")] + Device::Hpu => { + let hpu_ct = global_state::with_thread_local_hpu_device(|device| { + HpuRadixCiphertext::from_radix_ciphertext(&cpu_ct, &device.device) + }); + *self = Self::Hpu(hpu_ct); } } } diff --git a/tfhe/src/high_level_api/integers/unsigned/ops.rs b/tfhe/src/high_level_api/integers/unsigned/ops.rs index 1efe9cce8..dd7678372 100644 --- a/tfhe/src/high_level_api/integers/unsigned/ops.rs +++ b/tfhe/src/high_level_api/integers/unsigned/ops.rs @@ -1,6 +1,7 @@ // Ask clippy not to worry about this // this is the pattern we use for the macros #![allow(clippy::redundant_closure_call)] + use super::inner::RadixCiphertext; #[cfg(feature = "gpu")] use crate::high_level_api::details::MaybeCloned; @@ -17,12 +18,16 @@ use crate::high_level_api::traits::{ }; #[cfg(feature = "gpu")] use crate::integer::gpu::ciphertext::CudaIntegerRadixCiphertext; +#[cfg(feature = "hpu")] +use crate::integer::hpu::ciphertext::HpuRadixCiphertext; use crate::{FheBool, FheUint}; use std::borrow::Borrow; use std::ops::{ Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Div, DivAssign, Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, Sub, SubAssign, }; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::*; impl std::iter::Sum for FheUint where @@ -93,6 +98,15 @@ where }); Self::new(inner, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let mut iter = iter; + let mut result = iter.next().unwrap().ciphertext.into_hpu(device); + for o in iter { + result += o.ciphertext.into_hpu(device); + } + Self::new(result, device.tag.clone()) + } }) } } @@ -186,6 +200,21 @@ where Self::new(inner, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let mut iter = iter; + let first = iter.next().unwrap().ciphertext.on_hpu(device); + + let Some(second) = iter.next() else { + return Self::new(first.clone(), device.tag.clone()); + }; + + let mut result = &*first + &*second.ciphertext.on_hpu(device); + for o in iter { + result += &*o.ciphertext.on_hpu(device); + } + Self::new(result, device.tag.clone()) + } }) } } @@ -232,6 +261,10 @@ where ); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -278,6 +311,10 @@ where ); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -335,6 +372,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_EQ; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } @@ -374,6 +433,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_NEQ; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } } @@ -439,6 +520,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_LT; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } @@ -478,6 +581,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_LTE; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } @@ -517,6 +642,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_GT; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } @@ -556,6 +703,28 @@ where ); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + let (opcode, proto) = { + let asm_iop = &hpu_asm::iop::IOP_CMP_GTE; + ( + asm_iop.opcode(), + &asm_iop.format().expect("Unspecified IOP format").proto, + ) + }; + // These clones are cheap are they are just Arc + let hpu_result = HpuRadixCiphertext::exec( + proto, + opcode, + &[hpu_lhs.clone(), hpu_rhs.clone()], + &[], + ) + .pop() + .unwrap(); + FheBool::new(hpu_result, device.tag.clone()) + } }) } } @@ -642,6 +811,10 @@ where FheUint::::new(inner_result.1, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -723,6 +896,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs + &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -766,6 +945,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs - &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -809,6 +994,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs * &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -850,6 +1041,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs & &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -891,6 +1088,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs | &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -932,6 +1135,12 @@ generic_integer_impl_operation!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = lhs.ciphertext.on_hpu(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + FheUint::new(&*hpu_lhs ^ &*hpu_rhs, device.tag.clone()) + } }) } }, @@ -982,6 +1191,10 @@ generic_integer_impl_operation!( .div(&*lhs.ciphertext.on_gpu(streams), &*rhs.ciphertext.on_gpu(streams), streams); FheUint::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1033,6 +1246,10 @@ generic_integer_impl_operation!( .rem(&*lhs.ciphertext.on_gpu(streams), &*rhs.ciphertext.on_gpu(streams), streams); FheUint::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1146,6 +1363,10 @@ generic_integer_impl_shift_rotate!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1190,6 +1411,10 @@ generic_integer_impl_shift_rotate!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1234,6 +1459,10 @@ generic_integer_impl_shift_rotate!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1278,6 +1507,10 @@ generic_integer_impl_shift_rotate!( FheUint::new(inner_result, cuda_key.tag.clone()) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -1328,6 +1561,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs += &*hpu_rhs; + } }) } } @@ -1373,6 +1612,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs -= &*hpu_rhs; + } }) } } @@ -1418,6 +1663,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs *= &*hpu_rhs; + } }) } } @@ -1461,6 +1712,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs &= &*hpu_rhs; + } }) } } @@ -1504,6 +1761,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs |= &*hpu_rhs; + } }) } } @@ -1547,6 +1810,12 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let hpu_lhs = self.ciphertext.as_hpu_mut(device); + let hpu_rhs = rhs.ciphertext.on_hpu(device); + *hpu_lhs ^= &*hpu_rhs; + } }) } } @@ -1595,6 +1864,10 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1643,6 +1916,10 @@ where streams, ); }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1698,6 +1975,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1752,6 +2033,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1807,6 +2092,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1862,6 +2151,10 @@ where ); }); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -1941,6 +2234,10 @@ where .neg(&*self.ciphertext.on_gpu(streams), streams); FheUint::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -2010,6 +2307,10 @@ where .bitnot(&*self.ciphertext.on_gpu(streams), streams); FheUint::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support bitnot (operator `!`)") + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/overflowing_ops.rs b/tfhe/src/high_level_api/integers/unsigned/overflowing_ops.rs index 9b2e274d4..20aa6dbf7 100644 --- a/tfhe/src/high_level_api/integers/unsigned/overflowing_ops.rs +++ b/tfhe/src/high_level_api/integers/unsigned/overflowing_ops.rs @@ -64,6 +64,10 @@ where FheBool::new(inner_result.1, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -160,6 +164,10 @@ where FheBool::new(inner_result.1, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -296,6 +304,10 @@ where FheBool::new(inner_result.1, cuda_key.tag.clone()), ) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -384,6 +396,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support overflowing_add yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -471,6 +487,10 @@ where InternalServerKey::Cuda(_) => { todo!("Cuda devices do not support overflowing_mul"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/scalar_ops.rs b/tfhe/src/high_level_api/integers/unsigned/scalar_ops.rs index 6f93356ef..c86e727ce 100644 --- a/tfhe/src/high_level_api/integers/unsigned/scalar_ops.rs +++ b/tfhe/src/high_level_api/integers/unsigned/scalar_ops.rs @@ -68,6 +68,10 @@ where .scalar_eq(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -107,6 +111,10 @@ where .scalar_ne(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -152,6 +160,10 @@ where .scalar_lt(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -191,6 +203,10 @@ where .scalar_le(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -230,6 +246,10 @@ where .scalar_gt(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -269,6 +289,10 @@ where .scalar_ge(&*self.ciphertext.on_gpu(streams), rhs, streams); FheBool::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -316,6 +340,10 @@ where .scalar_max(&*self.ciphertext.on_gpu(streams), rhs, streams); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -363,6 +391,10 @@ where .scalar_min(&*self.ciphertext.on_gpu(streams), rhs, streams); Self::new(inner_result, cuda_key.tag.clone()) }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -413,6 +445,10 @@ where InternalServerKey::Cuda(_) => { panic!("Cuda devices do not support bitslice yet"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -504,6 +540,10 @@ macro_rules! generic_integer_impl_scalar_div_rem { <$concrete_type>::new(r, cuda_key.tag.clone()) ) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } } }) } @@ -680,6 +720,10 @@ macro_rules! define_scalar_rotate_shifts { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -709,6 +753,10 @@ macro_rules! define_scalar_rotate_shifts { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -738,6 +786,10 @@ macro_rules! define_scalar_rotate_shifts { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -767,6 +819,10 @@ macro_rules! define_scalar_rotate_shifts { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -793,6 +849,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_left_shift_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -819,6 +879,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_right_shift_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -845,6 +909,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_rotate_left_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -871,6 +939,10 @@ macro_rules! define_scalar_rotate_shifts { .scalar_rotate_right_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -987,6 +1059,13 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.on_hpu(device); + let rhs = u128::try_from(rhs).unwrap(); + + RadixCiphertext::Hpu(&*lhs + rhs) + } }) } }, @@ -1016,6 +1095,13 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.on_hpu(device); + let rhs = u128::try_from(rhs).unwrap(); + + RadixCiphertext::Hpu(&*lhs - rhs) + } }) } }, @@ -1045,6 +1131,13 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.on_hpu(device); + let rhs = u128::try_from(rhs).unwrap(); + + RadixCiphertext::Hpu(&*lhs * rhs) + } }) } }, @@ -1075,6 +1168,10 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1104,6 +1201,10 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1134,6 +1235,10 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1163,6 +1268,10 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1193,6 +1302,10 @@ macro_rules! define_scalar_ops { }); RadixCiphertext::Cuda(inner_result) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1238,6 +1351,10 @@ macro_rules! define_scalar_ops { RadixCiphertext::Cuda(result) }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1332,6 +1449,13 @@ macro_rules! define_scalar_ops { .scalar_add_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.as_hpu_mut(device); + let rhs = u128::try_from(rhs).unwrap(); + + *lhs += rhs; + } }) } }, @@ -1363,6 +1487,13 @@ macro_rules! define_scalar_ops { .scalar_sub_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.as_hpu_mut(device); + let rhs = u128::try_from(rhs).unwrap(); + + *lhs -= rhs; + } }) } }, @@ -1390,6 +1521,13 @@ macro_rules! define_scalar_ops { .scalar_mul_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(device) => { + let lhs = lhs.ciphertext.as_hpu_mut(device); + let rhs = u128::try_from(rhs).unwrap(); + + *lhs *= rhs; + } }) } }, @@ -1418,6 +1556,10 @@ macro_rules! define_scalar_ops { .scalar_bitand_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1445,6 +1587,10 @@ macro_rules! define_scalar_ops { .scalar_bitor_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1471,6 +1617,10 @@ macro_rules! define_scalar_ops { .scalar_bitxor_assign(lhs.ciphertext.as_gpu_mut(streams), rhs, streams); }) } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1495,7 +1645,11 @@ macro_rules! define_scalar_ops { let cuda_lhs = lhs.ciphertext.as_gpu_mut(streams); let cuda_result = cuda_key.pbs_key().scalar_div(&cuda_lhs, rhs, streams); *cuda_lhs = cuda_result; - }) + }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, @@ -1520,7 +1674,11 @@ macro_rules! define_scalar_ops { let cuda_lhs = lhs.ciphertext.as_gpu_mut(streams); let cuda_result = cuda_key.pbs_key().scalar_rem(&cuda_lhs, rhs, streams); *cuda_lhs = cuda_result; - }) + }), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + panic!("Hpu does not support this operation yet.") + } }) } }, diff --git a/tfhe/src/high_level_api/integers/unsigned/squashed_noise.rs b/tfhe/src/high_level_api/integers/unsigned/squashed_noise.rs index 8ec971ee9..d03d50131 100644 --- a/tfhe/src/high_level_api/integers/unsigned/squashed_noise.rs +++ b/tfhe/src/high_level_api/integers/unsigned/squashed_noise.rs @@ -109,8 +109,8 @@ impl InnerSquashedNoiseRadixCiphertext { (Self::Cpu(_), Device::Cpu) => { // Nothing to do, we already are on the correct device } - #[cfg(feature = "gpu")] - _ => panic!("Cuda devices do not support noise squashing yet"), + #[cfg(any(feature = "gpu", feature = "hpu"))] + _ => panic!("Cuda/Hpu devices do not support noise squashing yet"), } } @@ -194,6 +194,10 @@ impl SquashNoise for FheUint { InternalServerKey::Cuda(_) => Err(crate::error!( "Cuda devices do not support noise squashing yet" )), + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_device) => { + Err(crate::error!("Hpu devices do not support noise squashing")) + } }) } } diff --git a/tfhe/src/high_level_api/integers/unsigned/tests/cpu.rs b/tfhe/src/high_level_api/integers/unsigned/tests/cpu.rs index e4d06d3c9..6a41700bf 100644 --- a/tfhe/src/high_level_api/integers/unsigned/tests/cpu.rs +++ b/tfhe/src/high_level_api/integers/unsigned/tests/cpu.rs @@ -74,12 +74,48 @@ fn test_uint64_quickstart() { super::test_case_uint64_quickstart(&client_key); } +#[test] +fn test_uint32_arith() { + let client_key = setup_default_cpu(); + super::test_case_uint32_arith(&client_key); +} + +#[test] +fn test_uint32_arith_assign() { + let client_key = setup_default_cpu(); + super::test_case_uint32_arith_assign(&client_key); +} + +#[test] +fn test_uint32_scalar_arith() { + let client_key = setup_default_cpu(); + super::test_case_uint32_scalar_arith(&client_key); +} + +#[test] +fn test_uint32_scalar_arith_assign() { + let client_key = setup_default_cpu(); + super::test_case_uint32_scalar_arith_assign(&client_key); +} + +#[test] +fn test_uint32_clone() { + let client_key = setup_default_cpu(); + super::test_case_clone(&client_key); +} + #[test] fn test_uint8_compare() { let client_key = setup_default_cpu(); super::test_case_uint8_compare(&client_key); } +#[test] +fn test_uint8_compare_scalar() { + let client_key = setup_default_cpu(); + super::test_case_uint8_compare_scalar(&client_key); +} + #[test] fn test_uint32_shift() { let client_key = setup_default_cpu(); @@ -104,6 +140,18 @@ fn test_uint32_bitwise() { super::test_case_uint32_bitwise(&client_key); } +#[test] +fn test_uint32_bitwise_assign() { + let client_key = setup_default_cpu(); + super::test_case_uint32_bitwise_assign(&client_key); +} + +#[test] +fn test_uint32_scalar_bitwise() { + let client_key = setup_default_cpu(); + super::test_case_uint32_scalar_bitwise(&client_key); +} + #[test] fn test_uint32_rotate() { let client_key = setup_default_cpu(); diff --git a/tfhe/src/high_level_api/integers/unsigned/tests/gpu.rs b/tfhe/src/high_level_api/integers/unsigned/tests/gpu.rs index 0fc510370..16ee8238f 100644 --- a/tfhe/src/high_level_api/integers/unsigned/tests/gpu.rs +++ b/tfhe/src/high_level_api/integers/unsigned/tests/gpu.rs @@ -91,6 +91,18 @@ fn test_uint32_bitwise_gpu_multibit() { super::test_case_uint32_bitwise(&client_key); } +#[test] +fn test_uint32_scalar_bitwise_gpu() { + let client_key = setup_default_gpu(); + super::test_case_uint32_scalar_bitwise(&client_key); +} + +#[test] +fn test_uint32_scalar_bitwise_gpu_multibit() { + let client_key = setup_gpu(Some(PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS)); + super::test_case_uint32_scalar_bitwise(&client_key); +} + #[test] fn test_if_then_else_gpu() { let client_key = setup_default_gpu(); diff --git a/tfhe/src/high_level_api/integers/unsigned/tests/hpu.rs b/tfhe/src/high_level_api/integers/unsigned/tests/hpu.rs new file mode 100644 index 000000000..a802118e6 --- /dev/null +++ b/tfhe/src/high_level_api/integers/unsigned/tests/hpu.rs @@ -0,0 +1,84 @@ +use std::sync::LazyLock; +use tfhe_hpu_backend::prelude::HpuDevice; + +use crate::{set_server_key, ClientKey, CompressedServerKey, Config}; + +fn setup_hpu(hpu_device_cfg_path: &str) -> ClientKey { + let hpu_device = HpuDevice::from_config(hpu_device_cfg_path); + + let config = Config::from_hpu_device(&hpu_device); + + // Generate Keys + let cks = ClientKey::generate(config); + let csks = CompressedServerKey::new(&cks); + + set_server_key((hpu_device, csks)); + + cks +} + +static HPU_CLIENT_KEY: LazyLock = LazyLock::new(|| { + let config_name = std::env::var("HPU_CONFIG").unwrap(); + let backend_dir = std::env::var("HPU_BACKEND_DIR").unwrap(); + let config_path = format!("{backend_dir}/config_store/{config_name}/hpu_config.toml"); + + setup_hpu(&config_path) +}); + +fn setup_default_hpu() -> ClientKey { + HPU_CLIENT_KEY.clone() +} + +#[test] +fn test_uint8_quickstart_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint8_quickstart(&client_key); +} + +#[test] +fn test_uint64_quickstart_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint64_quickstart(&client_key); +} + +#[test] +fn test_uint8_compare_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint8_compare(&client_key); +} + +#[test] +fn test_uint32_bitwise() { + let client_key = setup_default_hpu(); + super::test_case_uint32_bitwise(&client_key); +} + +#[test] +fn test_uint32_arith_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint32_arith(&client_key); +} + +#[test] +fn test_uint32_arith_assign_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint32_arith_assign(&client_key); +} + +#[test] +fn test_uint32_scalar_arith_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint32_scalar_arith(&client_key); +} + +#[test] +fn test_uint32_scalar_arith_assign_hpu() { + let client_key = setup_default_hpu(); + super::test_case_uint32_scalar_arith_assign(&client_key); +} + +#[test] +fn test_uint32_clone_hpu() { + let client_key = setup_default_hpu(); + super::test_case_clone(&client_key); +} diff --git a/tfhe/src/high_level_api/integers/unsigned/tests/mod.rs b/tfhe/src/high_level_api/integers/unsigned/tests/mod.rs index 42005cc30..6c5758f09 100644 --- a/tfhe/src/high_level_api/integers/unsigned/tests/mod.rs +++ b/tfhe/src/high_level_api/integers/unsigned/tests/mod.rs @@ -7,6 +7,8 @@ use rand::{thread_rng, Rng}; mod cpu; #[cfg(feature = "gpu")] pub(crate) mod gpu; +#[cfg(feature = "hpu")] +mod hpu; fn test_case_uint8_quickstart(client_key: &ClientKey) { let clear_a = 27u8; @@ -58,6 +60,43 @@ fn test_case_uint64_quickstart(cks: &ClientKey) { assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); } +fn test_case_clone(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); + + let a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + let b = FheUint32::try_encrypt(clear_b, cks).unwrap(); + + let c = &a + &b; + + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); + + // We expect clones to be full clones and not some incremented ref-count + let mut cloned_a = a.clone(); + + let decrypted: u32 = cloned_a.decrypt(cks); + assert_eq!(decrypted, clear_a); + let decrypted: u32 = b.decrypt(cks); + assert_eq!(decrypted, clear_b); + + let c = &cloned_a + &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); + + cloned_a += &b; + + let decrypted: u32 = cloned_a.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); + let decrypted: u32 = b.decrypt(cks); + assert_eq!(decrypted, clear_b); + let decrypted: u32 = a.decrypt(cks); + assert_eq!(decrypted, clear_a); + let decrypted: u32 = b.decrypt(cks); + assert_eq!(decrypted, clear_b); +} + fn test_case_uint8_trivial(client_key: &ClientKey) { let a = FheUint8::try_encrypt_trivial(234u8).unwrap(); @@ -65,6 +104,94 @@ fn test_case_uint8_trivial(client_key: &ClientKey) { assert_eq!(clear, 234); } +fn test_case_uint32_arith(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); + + let a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + let b = FheUint32::try_encrypt(clear_b, cks).unwrap(); + + let c = &a + &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); + + let c = &a - &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_sub(clear_b)); + + let c = &a * &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_mul(clear_b)); +} + +fn test_case_uint32_arith_assign(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let mut clear_a = rng.gen::(); + let clear_b = rng.gen::(); + + let mut a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + let b = FheUint32::try_encrypt(clear_b, cks).unwrap(); + + a += &b; + clear_a = clear_a.wrapping_add(clear_b); + let decrypted: u32 = a.decrypt(cks); + assert_eq!(decrypted, clear_a); + + a -= &b; + let decrypted: u32 = a.decrypt(cks); + clear_a = clear_a.wrapping_sub(clear_b); + assert_eq!(decrypted, clear_a); + + a *= &b; + let decrypted: u32 = a.decrypt(cks); + clear_a = clear_a.wrapping_mul(clear_b); + assert_eq!(decrypted, clear_a); +} + +fn test_case_uint32_scalar_arith(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); + + let a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + + let c = &a + clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_add(clear_b)); + + let c = &a - clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_sub(clear_b)); + + let c = &a * clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a.wrapping_mul(clear_b)); +} + +fn test_case_uint32_scalar_arith_assign(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let mut clear_a = rng.gen::(); + let clear_b = rng.gen::(); + + let mut a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + + a += clear_b; + clear_a = clear_a.wrapping_add(clear_b); + let decrypted: u32 = a.decrypt(cks); + assert_eq!(decrypted, clear_a); + + a -= clear_b; + let decrypted: u32 = a.decrypt(cks); + clear_a = clear_a.wrapping_sub(clear_b); + assert_eq!(decrypted, clear_a); + + a *= clear_b; + let decrypted: u32 = a.decrypt(cks); + clear_a = clear_a.wrapping_mul(clear_b); + assert_eq!(decrypted, clear_a); +} + fn test_case_uint256_trivial(client_key: &ClientKey) { let clear_a = U256::from(u128::MAX); let a = FheUint256::try_encrypt_trivial(clear_a).unwrap(); @@ -74,97 +201,101 @@ fn test_case_uint256_trivial(client_key: &ClientKey) { #[allow(clippy::eq_op)] fn test_case_uint8_compare(client_key: &ClientKey) { - let clear_a = 27u8; - let clear_b = 128u8; + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); let a = FheUint8::encrypt(clear_a, client_key); let b = FheUint8::encrypt(clear_b, client_key); - // Test comparing encrypted with encrypted - { - let result = &a.eq(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a == clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.eq(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a == clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.eq(&a); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a == clear_a; - assert_eq!(decrypted_result, clear_result); + let result = &a.eq(&a); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a == clear_a; + assert_eq!(decrypted_result, clear_result); - let result = &a.ne(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a != clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.ne(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a != clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.ne(&a); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a != clear_a; - assert_eq!(decrypted_result, clear_result); + let result = &a.ne(&a); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a != clear_a; + assert_eq!(decrypted_result, clear_result); - let result = &a.le(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a <= clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.le(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a <= clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.lt(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a < clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.lt(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a < clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.ge(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a >= clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.ge(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a >= clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.gt(&b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a > clear_b; - assert_eq!(decrypted_result, clear_result); - } + let result = &a.gt(&b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a > clear_b; + assert_eq!(decrypted_result, clear_result); +} - // Test comparing encrypted with clear - { - let result = &a.eq(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a == clear_b; - assert_eq!(decrypted_result, clear_result); +#[allow(clippy::eq_op)] +fn test_case_uint8_compare_scalar(client_key: &ClientKey) { + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); - let result = &a.eq(clear_a); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a == clear_a; - assert_eq!(decrypted_result, clear_result); + let a = FheUint8::encrypt(clear_a, client_key); - let result = &a.ne(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a != clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.eq(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a == clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.ne(clear_a); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a != clear_a; - assert_eq!(decrypted_result, clear_result); + let result = &a.eq(clear_a); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a == clear_a; + assert_eq!(decrypted_result, clear_result); - let result = &a.le(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a <= clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.ne(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a != clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.lt(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a < clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.ne(clear_a); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a != clear_a; + assert_eq!(decrypted_result, clear_result); - let result = &a.ge(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a >= clear_b; - assert_eq!(decrypted_result, clear_result); + let result = &a.le(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a <= clear_b; + assert_eq!(decrypted_result, clear_result); - let result = &a.gt(clear_b); - let decrypted_result = result.decrypt(client_key); - let clear_result = clear_a > clear_b; - assert_eq!(decrypted_result, clear_result); - } + let result = &a.lt(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a < clear_b; + assert_eq!(decrypted_result, clear_result); + + let result = &a.ge(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a >= clear_b; + assert_eq!(decrypted_result, clear_result); + + let result = &a.gt(clear_b); + let decrypted_result = result.decrypt(client_key); + let clear_result = clear_a > clear_b; + assert_eq!(decrypted_result, clear_result); } fn test_case_uint32_shift(cks: &ClientKey) { @@ -226,65 +357,72 @@ fn test_case_uint32_bitwise(cks: &ClientKey) { let a = FheUint32::try_encrypt(clear_a, cks).unwrap(); let b = FheUint32::try_encrypt(clear_b, cks).unwrap(); - // encrypted bitwise - { - let c = &a | &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a | clear_b); + let c = &a | &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a | clear_b); - let c = &a & &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a & clear_b); + let c = &a & &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a & clear_b); - let c = &a ^ &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a ^ clear_b); + let c = &a ^ &b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a ^ clear_b); +} - let mut c = a.clone(); - c |= &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a | clear_b); +fn test_case_uint32_bitwise_assign(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let mut clear_a = rng.gen::(); + let clear_b = rng.gen::(); - let mut c = a.clone(); - c &= &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a & clear_b); + let mut a = FheUint32::try_encrypt(clear_a, cks).unwrap(); + let b = FheUint32::try_encrypt(clear_b, cks).unwrap(); - let mut c = a.clone(); - c ^= &b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a ^ clear_b); - } + a &= &b; + clear_a &= clear_b; + let decrypted: u32 = a.decrypt(cks); + assert_eq!(decrypted, clear_a); - // clear bitwise - { - let c = &a | b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a | clear_b); + a |= &b; + let decrypted: u32 = a.decrypt(cks); + clear_a |= clear_b; + assert_eq!(decrypted, clear_a); - let c = &a & clear_b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a & clear_b); + a ^= &b; + let decrypted: u32 = a.decrypt(cks); + clear_a ^= clear_b; + assert_eq!(decrypted, clear_a); +} - let c = &a ^ clear_b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a ^ clear_b); +fn test_case_uint32_scalar_bitwise(cks: &ClientKey) { + let mut rng = rand::thread_rng(); + let clear_a = rng.gen::(); + let clear_b = rng.gen::(); - let mut c = a.clone(); - c |= clear_b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a | clear_b); + let a = FheUint32::try_encrypt(clear_a, cks).unwrap(); - let mut c = a.clone(); - c &= clear_b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a & clear_b); + let c = &a & clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a & clear_b); - let mut c = a; - c ^= clear_b; - let decrypted: u32 = c.decrypt(cks); - assert_eq!(decrypted, clear_a ^ clear_b); - } + let c = &a ^ clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a ^ clear_b); + + let mut c = a.clone(); + c |= clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a | clear_b); + + let mut c = a.clone(); + c &= clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a & clear_b); + + let mut c = a; + c ^= clear_b; + let decrypted: u32 = c.decrypt(cks); + assert_eq!(decrypted, clear_a ^ clear_b); } fn test_case_uint32_rotate(cks: &ClientKey) { diff --git a/tfhe/src/high_level_api/keys/mod.rs b/tfhe/src/high_level_api/keys/mod.rs index c803b4557..cd2e633cc 100644 --- a/tfhe/src/high_level_api/keys/mod.rs +++ b/tfhe/src/high_level_api/keys/mod.rs @@ -12,6 +12,8 @@ pub use key_switching_key::KeySwitchingKey; pub use public::{CompactPublicKey, CompressedCompactPublicKey, CompressedPublicKey, PublicKey}; #[cfg(feature = "gpu")] pub use server::CudaServerKey; +#[cfg(feature = "hpu")] +pub(in crate::high_level_api) use server::HpuTaggedDevice; pub(crate) use server::InternalServerKey; pub use server::{CompressedServerKey, ServerKey}; diff --git a/tfhe/src/high_level_api/keys/server.rs b/tfhe/src/high_level_api/keys/server.rs index 26bb0703e..c0dd431e8 100644 --- a/tfhe/src/high_level_api/keys/server.rs +++ b/tfhe/src/high_level_api/keys/server.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "hpu")] +pub(in crate::high_level_api) use hpu::HpuTaggedDevice; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::HpuDevice; use tfhe_versionable::Versionize; use super::ClientKey; @@ -18,7 +22,7 @@ use crate::prelude::Tagged; use crate::shortint::MessageModulus; #[cfg(feature = "gpu")] use crate::GpuIndex; -use crate::Tag; +use crate::{Device, Tag}; use std::sync::Arc; /// Key of the server @@ -391,6 +395,20 @@ pub enum InternalServerKey { Cpu(ServerKey), #[cfg(feature = "gpu")] Cuda(CudaServerKey), + #[cfg(feature = "hpu")] + Hpu(HpuTaggedDevice), +} + +impl InternalServerKey { + pub(crate) fn device(&self) -> Device { + match self { + Self::Cpu(_) => Device::Cpu, + #[cfg(feature = "gpu")] + Self::Cuda(_) => Device::CudaGpu, + #[cfg(feature = "hpu")] + Self::Hpu(_) => Device::Hpu, + } + } } impl std::fmt::Debug for InternalServerKey { @@ -399,6 +417,8 @@ impl std::fmt::Debug for InternalServerKey { Self::Cpu(_) => f.debug_tuple("Cpu").finish(), #[cfg(feature = "gpu")] Self::Cuda(_) => f.debug_tuple("Cuda").finish(), + #[cfg(feature = "hpu")] + Self::Hpu(_) => f.debug_tuple("Hpu").finish(), } } } @@ -415,6 +435,29 @@ impl From for InternalServerKey { } } +#[cfg(feature = "hpu")] +mod hpu { + use super::*; + + pub struct HpuTaggedDevice { + // The device holds the keys (there can only be one set of keys) + // So we attach the tag to it instead of the key + pub(in crate::high_level_api) device: Box, + pub(in crate::high_level_api) tag: Tag, + } + + impl From<(HpuDevice, CompressedServerKey)> for InternalServerKey { + fn from((device, csks): (HpuDevice, CompressedServerKey)) -> Self { + let CompressedServerKey { integer_key, tag } = csks; + crate::integer::hpu::init_device(&device, integer_key.key).expect("Invalid key"); + Self::Hpu(HpuTaggedDevice { + device: Box::new(device), + tag, + }) + } + } +} + use crate::high_level_api::keys::inner::IntegerServerKeyConformanceParams; impl ParameterSetConformant for ServerKey { diff --git a/tfhe/src/high_level_api/mod.rs b/tfhe/src/high_level_api/mod.rs index 04413acdd..4e4c60f37 100644 --- a/tfhe/src/high_level_api/mod.rs +++ b/tfhe/src/high_level_api/mod.rs @@ -164,6 +164,8 @@ pub enum Device { Cpu, #[cfg(feature = "gpu")] CudaGpu, + #[cfg(feature = "hpu")] + Hpu, } #[derive(FromRepr, Copy, Clone, PartialEq, Eq, Debug)] diff --git a/tfhe/src/high_level_api/prelude.rs b/tfhe/src/high_level_api/prelude.rs index f4fa28772..9ad9c1dea 100644 --- a/tfhe/src/high_level_api/prelude.rs +++ b/tfhe/src/high_level_api/prelude.rs @@ -8,10 +8,12 @@ //! ``` pub use crate::high_level_api::traits::{ BitSlice, CiphertextList, DivRem, FheDecrypt, FheEncrypt, FheEq, FheKeyswitch, FheMax, FheMin, - FheOrd, FheTrivialEncrypt, FheTryEncrypt, FheTryTrivialEncrypt, IfThenElse, OverflowingAdd, - OverflowingMul, OverflowingSub, RotateLeft, RotateLeftAssign, RotateRight, RotateRightAssign, - ScalarIfThenElse, SquashNoise, Tagged, + FheOrd, FheTrivialEncrypt, FheTryEncrypt, FheTryTrivialEncrypt, FheWait, IfThenElse, + OverflowingAdd, OverflowingMul, OverflowingSub, RotateLeft, RotateLeftAssign, RotateRight, + RotateRightAssign, ScalarIfThenElse, SquashNoise, Tagged, }; +#[cfg(feature = "hpu")] +pub use crate::high_level_api::traits::{FheHpu, HpuHandle}; pub use crate::conformance::ParameterSetConformant; pub use crate::core_crypto::prelude::{CastFrom, CastInto}; diff --git a/tfhe/src/high_level_api/strings/ascii/comp.rs b/tfhe/src/high_level_api/strings/ascii/comp.rs index 3fdf8e8ab..643821fbb 100644 --- a/tfhe/src/high_level_api/strings/ascii/comp.rs +++ b/tfhe/src/high_level_api/strings/ascii/comp.rs @@ -18,6 +18,10 @@ impl FheEq<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings eq"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -33,6 +37,10 @@ impl FheEq<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ne"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -48,6 +56,10 @@ impl FheEq<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings eq"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -61,6 +73,10 @@ impl FheEq<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ne"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -78,6 +94,10 @@ impl FheOrd<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings lt"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -93,6 +113,10 @@ impl FheOrd<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings le"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -108,6 +132,10 @@ impl FheOrd<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings gt"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -123,6 +151,10 @@ impl FheOrd<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ge"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -138,6 +170,10 @@ impl FheOrd<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings lt"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -151,6 +187,10 @@ impl FheOrd<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings le"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -164,6 +204,10 @@ impl FheOrd<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings gt"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } @@ -177,6 +221,10 @@ impl FheOrd<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ge"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -216,6 +264,10 @@ impl FheEqIgnoreCase for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings eq_ignore_case"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } @@ -255,6 +307,10 @@ impl FheEqIgnoreCase for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings eq_ignore_case"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("Hpu does not support this operation yet.") + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/contains.rs b/tfhe/src/high_level_api/strings/ascii/contains.rs index 02870865d..c89f2aff3 100644 --- a/tfhe/src/high_level_api/strings/ascii/contains.rs +++ b/tfhe/src/high_level_api/strings/ascii/contains.rs @@ -40,6 +40,10 @@ impl FheStringMatching<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings contains"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support contains"); + } }) } @@ -77,6 +81,10 @@ impl FheStringMatching<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings starts_with"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support starts_with"); + } }) } @@ -114,6 +122,10 @@ impl FheStringMatching<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ends_with"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support ends_with"); + } }) } } @@ -131,6 +143,10 @@ impl FheStringMatching<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings contains"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support contains"); + } }) } @@ -146,6 +162,10 @@ impl FheStringMatching<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings starts_with"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support starts_with"); + } }) } @@ -161,6 +181,10 @@ impl FheStringMatching<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings ends_with"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support ends_with"); + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/find.rs b/tfhe/src/high_level_api/strings/ascii/find.rs index adf425b6b..57348f248 100644 --- a/tfhe/src/high_level_api/strings/ascii/find.rs +++ b/tfhe/src/high_level_api/strings/ascii/find.rs @@ -46,6 +46,10 @@ impl FheStringFind<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings find"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings find"); + } }) } @@ -89,6 +93,10 @@ impl FheStringFind<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings rfind"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings rfind"); + } }) } } @@ -132,6 +140,10 @@ impl FheStringFind<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings find"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings find"); + } }) } @@ -173,6 +185,10 @@ impl FheStringFind<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings rfind"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings rfind"); + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/mod.rs b/tfhe/src/high_level_api/strings/ascii/mod.rs index 6da06db2d..54863612a 100644 --- a/tfhe/src/high_level_api/strings/ascii/mod.rs +++ b/tfhe/src/high_level_api/strings/ascii/mod.rs @@ -377,6 +377,8 @@ impl<'a> FheTryTrivialEncrypt> for FheAsciiString { } #[cfg(feature = "gpu")] Some(InternalServerKey::Cuda(_)) => Err(crate::error!("CUDA does not support string")), + #[cfg(feature = "hpu")] + Some(InternalServerKey::Hpu(_)) => Err(crate::error!("Hpu does not support string")), None => Err(UninitializedServerKey.into()), }) } diff --git a/tfhe/src/high_level_api/strings/ascii/no_pattern.rs b/tfhe/src/high_level_api/strings/ascii/no_pattern.rs index e898a2465..f9dd468b3 100644 --- a/tfhe/src/high_level_api/strings/ascii/no_pattern.rs +++ b/tfhe/src/high_level_api/strings/ascii/no_pattern.rs @@ -105,6 +105,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings len"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings len"); + } }) } @@ -148,7 +152,11 @@ impl FheAsciiString { } #[cfg(feature = "gpu")] InternalServerKey::Cuda(_) => { - panic!("gpu does not support strings len"); + panic!("gpu does not support strings is_empty"); + } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings is_empty"); } }) } @@ -180,6 +188,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings to_lowercase"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings to_lowercase"); + } }) } @@ -210,6 +222,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings to_uppercase"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings to_uppercase"); + } }) } @@ -246,6 +262,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings concatenating"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings concatenating"); + } }) } } @@ -304,6 +324,10 @@ impl FheStringRepeat for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings repeat"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings repeat"); + } }) } } @@ -351,6 +375,10 @@ impl FheStringRepeat<(FheUint16, u16)> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings repeat"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings repeat"); + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/replace.rs b/tfhe/src/high_level_api/strings/ascii/replace.rs index acbabc9ad..7f2ad8918 100644 --- a/tfhe/src/high_level_api/strings/ascii/replace.rs +++ b/tfhe/src/high_level_api/strings/ascii/replace.rs @@ -45,6 +45,10 @@ impl FheStringReplace<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replace"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replace"); + } }) } } @@ -87,6 +91,10 @@ impl FheStringReplace<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replace"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replace"); + } }) } } @@ -125,6 +133,10 @@ impl FheStringReplaceN<&Self, u16> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replacen"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replacen"); + } }) } } @@ -145,6 +157,10 @@ impl FheStringReplaceN<&Self, (FheUint16, u16)> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replacen"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replacen"); + } }) } } @@ -183,6 +199,10 @@ impl FheStringReplaceN<&ClearString, u16> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replacen"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replacen"); + } }) } } @@ -203,6 +223,11 @@ impl FheStringReplaceN<&ClearString, (FheUint16, u16)> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strings replacen"); } + + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings replacen"); + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/strip.rs b/tfhe/src/high_level_api/strings/ascii/strip.rs index 53047f123..e97a4b4e0 100644 --- a/tfhe/src/high_level_api/strings/ascii/strip.rs +++ b/tfhe/src/high_level_api/strings/ascii/strip.rs @@ -49,6 +49,10 @@ impl FheStringStrip<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strip_prefix"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings strip_prefix"); + } }) } @@ -95,6 +99,10 @@ impl FheStringStrip<&Self> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strip_suffix"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings string_suffix"); + } }) } } @@ -143,6 +151,10 @@ impl FheStringStrip<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strip_prefix"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings strip_prefix"); + } }) } @@ -189,6 +201,10 @@ impl FheStringStrip<&ClearString> for FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support strip_suffix"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support strings strip_suffix"); + } }) } } diff --git a/tfhe/src/high_level_api/strings/ascii/trim.rs b/tfhe/src/high_level_api/strings/ascii/trim.rs index a0dd59b57..84909c64d 100644 --- a/tfhe/src/high_level_api/strings/ascii/trim.rs +++ b/tfhe/src/high_level_api/strings/ascii/trim.rs @@ -32,6 +32,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support trim_start"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support trim_start"); + } }) } @@ -64,6 +68,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support trim_end"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support trim_end"); + } }) } @@ -96,6 +104,10 @@ impl FheAsciiString { InternalServerKey::Cuda(_) => { panic!("gpu does not support trim"); } + #[cfg(feature = "hpu")] + InternalServerKey::Hpu(_) => { + panic!("hpu does not support trim"); + } }) } } diff --git a/tfhe/src/high_level_api/tests/tags_on_entities.rs b/tfhe/src/high_level_api/tests/tags_on_entities.rs index 24a75031e..ff46180e9 100644 --- a/tfhe/src/high_level_api/tests/tags_on_entities.rs +++ b/tfhe/src/high_level_api/tests/tags_on_entities.rs @@ -215,6 +215,10 @@ fn test_tag_propagation( set_server_key(sks); } + #[cfg(feature = "hpu")] + Device::Hpu => { + todo!() + } } // Check encrypting regular ct with client key diff --git a/tfhe/src/high_level_api/traits.rs b/tfhe/src/high_level_api/traits.rs index 6dfa69385..3fbf577ce 100644 --- a/tfhe/src/high_level_api/traits.rs +++ b/tfhe/src/high_level_api/traits.rs @@ -219,3 +219,27 @@ pub trait SizeOnGpu { pub trait AddAssignSizeOnGpu { fn get_add_assign_size_on_gpu(&self, amount: Rhs) -> u64; } + +/// Trait used to have a generic way of waiting Hw accelerator result +pub trait FheWait { + fn wait(&self); +} + +/// Struct used to have a generic way of starting custom Hpu IOp +#[cfg(feature = "hpu")] +pub struct HpuHandle { + pub native: Vec, + pub boolean: Vec, + pub imm: Vec, +} + +#[cfg(feature = "hpu")] +pub trait FheHpu +where + Self: Sized, +{ + fn iop_exec( + iop: &tfhe_hpu_backend::prelude::hpu_asm::AsmIOpcode, + src: HpuHandle<&Self>, + ) -> HpuHandle; +} diff --git a/tfhe/src/integer/bigint/static_unsigned.rs b/tfhe/src/integer/bigint/static_unsigned.rs index 927f6f8e8..b060ec21f 100644 --- a/tfhe/src/integer/bigint/static_unsigned.rs +++ b/tfhe/src/integer/bigint/static_unsigned.rs @@ -484,3 +484,15 @@ impl Numeric for StaticUnsignedBigInt { impl UnsignedNumeric for StaticUnsignedBigInt { type NumericSignedType = super::static_signed::StaticSignedBigInt; } + +impl TryFrom> for u128 { + type Error = &'static str; + + fn try_from(value: StaticUnsignedBigInt) -> Result { + if N > 2 && value.0[2..].iter().any(|e| *e != 0) { + Err("Value is too big to be converted to u128") + } else { + Ok(Self::cast_from(value)) + } + } +} diff --git a/tfhe/src/integer/hpu/ciphertext/mod.rs b/tfhe/src/integer/hpu/ciphertext/mod.rs new file mode 100644 index 000000000..2d96d6783 --- /dev/null +++ b/tfhe/src/integer/hpu/ciphertext/mod.rs @@ -0,0 +1,240 @@ +use hpu_asm::iop::*; +use tfhe_hpu_backend::prelude::*; + +use crate::core_crypto::prelude::{CreateFrom, LweCiphertextOwned}; +use crate::integer::{BooleanBlock, RadixCiphertext}; +use crate::shortint::ciphertext::{Degree, NoiseLevel}; +use crate::shortint::parameters::KeySwitch32PBSParameters; +use crate::shortint::{AtomicPatternKind, Ciphertext}; + +/// Simple wrapper over HpuVar +/// Add method to convert from/to cpu radix ciphertext +#[derive(Clone)] +pub struct HpuRadixCiphertext(pub(crate) HpuVarWrapped); + +impl HpuRadixCiphertext { + fn new(hpu_var: HpuVarWrapped) -> Self { + Self(hpu_var) + } + + /// Create a Hpu Radix ciphertext based on a Cpu one. + /// + /// No transfer with FPGA will occur until an operation on the HpuRadixCiphertext is requested + pub fn from_radix_ciphertext(cpu_ct: &RadixCiphertext, device: &HpuDevice) -> Self { + let params = device.params().clone(); + + let hpu_ct = cpu_ct + .blocks + .iter() + .map(|blk| HpuLweCiphertextOwned::create_from(blk.ct.as_view(), params.clone())) + .collect::>(); + + Self(device.new_var_from(hpu_ct, VarMode::Native)) + } + + /// Create a Cpu radix ciphertext copy from a Hpu one. + pub fn to_radix_ciphertext(&self) -> RadixCiphertext { + // NB: We clone the inner part of HpuRadixCiphertext but it is not costly since + // it's wrapped inside an Arc + let hpu_ct = self.0.clone().into_ct(); + let cpu_ct = hpu_ct + .into_iter() + .map(|ct| { + let pbs_p = KeySwitch32PBSParameters::from(ct.params()); + let cpu_ct = LweCiphertextOwned::from(ct.as_view()); + // Hpu output clean ciphertext without carry + Ciphertext::new( + cpu_ct, + Degree::new(pbs_p.message_modulus.0 - 1), + NoiseLevel::NOMINAL, + pbs_p.message_modulus, + pbs_p.carry_modulus, + AtomicPatternKind::KeySwitch32, + ) + }) + .collect::>(); + RadixCiphertext { blocks: cpu_ct } + } + + /// Create a Hpu boolean ciphertext based on a Cpu one. + /// + /// No transfer with FPGA will occur until an operation on the HpuRadixCiphertext is requested + pub fn from_boolean_ciphertext(cpu_ct: &BooleanBlock, device: &HpuDevice) -> Self { + let params = device.params().clone(); + + let hpu_ct = vec![HpuLweCiphertextOwned::create_from( + cpu_ct.0.ct.as_view(), + params, + )]; + Self(device.new_var_from(hpu_ct, VarMode::Bool)) + } + + /// Create a Cpu boolean block from a Hpu one + /// + /// # Panics + /// + /// This function panic if the underlying RadixCiphertext does not encrypt 0 or 1 + pub fn to_boolean_block(&self) -> BooleanBlock { + assert!( + self.0.is_boolean(), + "Error try to extract boolean value from invalid ciphertext" + ); + let mut boolean_ct = self + .to_radix_ciphertext() + .blocks + .into_iter() + .next() + .unwrap(); + boolean_ct.degree = Degree::new(1); + BooleanBlock::new_unchecked(boolean_ct) + } +} + +// Use to easily build HpuCmd exec request directly on HpuRadixCiphertext +impl std::ops::Deref for HpuRadixCiphertext { + type Target = HpuVarWrapped; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl HpuRadixCiphertext { + pub fn exec( + proto: &IOpProto, + opcode: IOpcode, + rhs_ct: &[Self], + rhs_imm: &[HpuImm], + ) -> Vec { + let rhs_var = rhs_ct.iter().map(|x| x.0.clone()).collect::>(); + let res_var = HpuCmd::exec(proto, opcode, &rhs_var, rhs_imm); + res_var.into_iter().map(Self::new).collect::>() + } + + pub fn exec_assign(proto: &IOpProto, opcode: IOpcode, rhs_ct: &[Self], rhs_imm: &[HpuImm]) { + let rhs_var = rhs_ct.iter().map(|x| x.0.clone()).collect::>(); + HpuCmd::exec_assign(proto, opcode, &rhs_var, rhs_imm) + } +} + +// Below we map common Hpu operation to std::ops rust trait ------------------- +#[macro_export] +/// Easily map an Hpu operation to std::ops rust trait +macro_rules! map_ct_ct { + ($hpu_op: ident -> $rust_op: literal) => { + ::paste::paste! { + impl std::ops::[<$rust_op:camel>] for HpuRadixCiphertext { + type Output = Self; + + fn [<$rust_op:lower>](self, rhs: Self) -> Self::Output { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + let res = HpuCmd::exec(proto, opcode, &[self.0, rhs.0], &[]); + Self::Output::new(res[0].clone()) + } + } + + impl<'a> std::ops::[<$rust_op:camel>] for &'a HpuRadixCiphertext { + type Output = HpuRadixCiphertext; + + fn [<$rust_op:lower>](self, rhs: Self) -> Self::Output { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + let res = HpuCmd::exec(proto, opcode, &[self.0.clone(), rhs.0.clone()], &[]); + Self::Output::new(res[0].clone()) + } + } + + + impl std::ops::[<$rust_op:camel Assign>] for HpuRadixCiphertext { + fn [<$rust_op:lower _assign>](&mut self, rhs: Self) { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + HpuCmd::exec_assign(proto, opcode, &[self.0.clone(), rhs.0], &[]) + } + } + + impl<'a> std::ops::[<$rust_op:camel Assign>]<&'a Self> for HpuRadixCiphertext { + fn [<$rust_op:lower _assign>](&mut self, rhs: &'a Self) { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + HpuCmd::exec_assign(proto, opcode, &[self.0.clone(), rhs.0.clone()], &[]) + } + } + } + }; +} +macro_rules! map_ct_scalar { + ($hpu_op: ident -> $rust_op: literal) => { + ::paste::paste! { + impl std::ops::[<$rust_op:camel>] for HpuRadixCiphertext { + type Output = Self; + + fn [<$rust_op:lower>](self, rhs: u128) -> Self::Output { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + let res = HpuCmd::exec(proto, opcode, &[self.0], &[rhs]); + Self::Output::new(res[0].clone()) + } + } + + impl<'a> std::ops::[<$rust_op:camel>] for &'a HpuRadixCiphertext { + type Output = HpuRadixCiphertext; + + fn [<$rust_op:lower>](self, rhs: u128) -> Self::Output { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + let res = HpuCmd::exec(proto, opcode, &[self.0.clone()], &[rhs]); + Self::Output::new(res[0].clone()) + } + } + + impl std::ops::[<$rust_op:camel Assign>] for HpuRadixCiphertext { + fn [<$rust_op:lower _assign>](&mut self, rhs: u128) { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + HpuCmd::exec_assign(proto, opcode, &[self.0.clone()], &[rhs]) + } + } + } + }; +} + +macro_rules! map_scalar_ct { + ($hpu_op: ident -> $rust_op: literal) => { + ::paste::paste! { + impl std::ops::[<$rust_op:camel>] for u128 { + type Output = HpuRadixCiphertext; + + fn [<$rust_op:lower>](self, rhs: HpuRadixCiphertext) -> Self::Output { + let opcode = $hpu_op.opcode(); + let proto = &$hpu_op.format().expect("Bind to std::ops a unspecified IOP").proto; + + let res = HpuCmd::exec(proto, opcode, &[rhs.0], &[self]); + Self::Output::new(res[0].clone()) + } + } + } + }; +} + +map_ct_ct!(IOP_ADD -> "Add"); +map_ct_ct!(IOP_SUB -> "Sub"); +map_ct_ct!(IOP_MUL -> "Mul"); +map_ct_ct!(IOP_BW_AND -> "BitAnd"); +map_ct_ct!(IOP_BW_OR -> "BitOr"); +map_ct_ct!(IOP_BW_XOR -> "BitXor"); + +map_ct_scalar!(IOP_ADDS -> "Add"); +map_scalar_ct!(IOP_ADDS -> "Add"); +map_ct_scalar!(IOP_SUBS -> "Sub"); +map_scalar_ct!(IOP_SSUB -> "Sub"); +map_ct_scalar!(IOP_MULS -> "Mul"); +map_scalar_ct!(IOP_MULS -> "Mul"); diff --git a/tfhe/src/integer/hpu/mod.rs b/tfhe/src/integer/hpu/mod.rs new file mode 100644 index 000000000..eb02fcdf0 --- /dev/null +++ b/tfhe/src/integer/hpu/mod.rs @@ -0,0 +1,78 @@ +use crate::core_crypto::prelude::CreateFrom; +use crate::shortint::parameters::KeySwitch32PBSParameters; +use tfhe_hpu_backend::prelude::*; + +use super::CompressedServerKey; +pub mod ciphertext; + +/// Utility function for HpuDevice initialisation +/// Init from Compressed material +pub fn init_device(device: &HpuDevice, server_key: CompressedServerKey) -> crate::Result<()> { + let params = device.params().clone(); + let tfhe_params = KeySwitch32PBSParameters::from(¶ms); + + let ap_key = match server_key.key.compressed_ap_server_key { + crate::shortint::atomic_pattern::compressed::CompressedAtomicPatternServerKey::Standard(_) => { + Err("Hpu not support Standard keys. Required a KeySwitch32 keys") + } + crate::shortint::atomic_pattern::compressed::CompressedAtomicPatternServerKey::KeySwitch32(keys) => Ok(keys), + }?; + + // Extract and convert bsk + let bsk = match ap_key.bootstrapping_key() { + crate::shortint::server_key::ShortintCompressedBootstrappingKey::Classic { + bsk, .. + } => { + let bsk = bsk + .clone() // TODO fix API this shouldn't be needed + .decompress_into_lwe_bootstrap_key(); + + // Check that given key is compliant with current device configuration + if tfhe_params.lwe_dimension != bsk.input_lwe_dimension() { + return Err("BootstrappingKey has incompatible input_lwe_dimension".into()); + } + if tfhe_params.glwe_dimension.to_glwe_size() != bsk.glwe_size() { + return Err("BootstrappingKey has incompatible glwe_size".into()); + } + if tfhe_params.polynomial_size != bsk.polynomial_size() { + return Err("BootstrappingKey has incompatible polynomial size".into()); + } + if tfhe_params.pbs_base_log != bsk.decomposition_base_log() { + return Err("BootstrappingKey has incompatible decomposition_base_log".into()); + } + if tfhe_params.pbs_level != bsk.decomposition_level_count() { + return Err("BootstrappingKey has incompatible decomposition_level_count".into()); + } + if tfhe_params.ciphertext_modulus != bsk.ciphertext_modulus() { + return Err("BootstrappingKey has incompatible ciphertext_modulus".into()); + } + Ok(bsk) + } + crate::shortint::server_key::ShortintCompressedBootstrappingKey::MultiBit { .. } => { + Err("Hpu currently not support multibit. Required a Classic BSK") + } + }?; + let hpu_bsk = HpuLweBootstrapKeyOwned::create_from(bsk.as_view(), params.clone()); + // Extract and convert ksk + let ksk = ap_key + .key_switching_key() + .clone() // TODO fix API this shouldn't be needed + .decompress_into_lwe_keyswitch_key(); + // Check that given key is compliant with current device configuration + if tfhe_params.ks_base_log != ksk.decomposition_base_log() { + return Err("KeyswitchingKey has incompatible decomposition_base_log".into()); + } + if tfhe_params.ks_level != ksk.decomposition_level_count() { + return Err("KeyswitchingKey has incompatible decomposition_level_count".into()); + } + let hpu_ksk = HpuLweKeyswitchKeyOwned::create_from(ksk.as_view(), params); + + // Upload them on Hpu and configure internal Fw/Lut + device.init( + hpu_bsk, + hpu_ksk, + crate::core_crypto::hpu::glwe_lookuptable::create_hpu_lookuptable, + ); + + Ok(()) +} diff --git a/tfhe/src/integer/keycache.rs b/tfhe/src/integer/keycache.rs index f29d7fdf6..53ebb315f 100644 --- a/tfhe/src/integer/keycache.rs +++ b/tfhe/src/integer/keycache.rs @@ -5,10 +5,25 @@ use crate::shortint::atomic_pattern::AtomicPatternParameters; #[cfg(feature = "experimental")] use crate::shortint::{PBSParameters, WopbsParameters}; +#[cfg(feature = "hpu")] +use std::sync::{Mutex, OnceLock}; +#[cfg(feature = "hpu")] +use tfhe_hpu_backend::prelude::*; + #[derive(Default)] -pub struct IntegerKeyCache; +pub struct IntegerKeyCache { + #[cfg(feature = "hpu")] + hpu_device: OnceLock>, +} impl IntegerKeyCache { + pub const fn new() -> Self { + Self { + #[cfg(feature = "hpu")] + hpu_device: OnceLock::new(), + } + } + pub fn get_from_params

(&self, params: P, key_kind: IntegerKeyKind) -> (ClientKey, ServerKey) where P: Into, @@ -38,6 +53,45 @@ impl IntegerKeyCache { (client_key, server_key) } + + #[cfg(feature = "hpu")] + pub fn get_hpu_device

(&self, param: P) -> &Mutex + where + P: Into + crate::keycache::NamedParam + Clone, + { + let hpu_device = self.hpu_device.get_or_init(|| { + // Instantiate HpuDevice -------------------------------------------------- + let hpu_device = { + let config_file = ShellString::new( + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_config.toml".to_string(), + ); + HpuDevice::from_config(&config_file.expand()) + }; + // Check compatibility with key + let hpu_pbs_params = + crate::shortint::parameters::KeySwitch32PBSParameters::from(hpu_device.params()); + assert_eq!( + param.clone().into(), + crate::shortint::AtomicPatternParameters::from(hpu_pbs_params), + "Error: Current Hpu device isn't compatible with {}", + param.name() + ); + + // Get current client key + let (cks, _) = self.get_from_params(param, IntegerKeyKind::Radix); + // Generate associated compressed ServerKey + let sks_compressed = super::CompressedServerKey::new_radix_compressed_server_key(&cks); + + // Init Hpu device with server key and firmware + crate::integer::hpu::init_device(&hpu_device, sks_compressed).expect("Invalid key"); + Mutex::new(hpu_device) + }); + + // Sanitize memory to prevent side-effect between tests + hpu_device.lock().unwrap().mem_sanitizer(); + + hpu_device + } } #[derive(Default)] @@ -67,6 +121,6 @@ impl WopbsKeyCache { } } -pub static KEY_CACHE: IntegerKeyCache = IntegerKeyCache; +pub static KEY_CACHE: IntegerKeyCache = IntegerKeyCache::new(); #[cfg(feature = "experimental")] pub static KEY_CACHE_WOPBS: WopbsKeyCache = WopbsKeyCache; diff --git a/tfhe/src/integer/mod.rs b/tfhe/src/integer/mod.rs index 708246512..9c1fa54c1 100755 --- a/tfhe/src/integer/mod.rs +++ b/tfhe/src/integer/mod.rs @@ -73,6 +73,9 @@ pub mod wopbs; #[cfg(feature = "gpu")] pub mod gpu; +#[cfg(feature = "hpu")] +pub mod hpu; + #[cfg(feature = "zk-pok")] pub use ciphertext::ProvenCompactCiphertextList; diff --git a/tfhe/src/lib.rs b/tfhe/src/lib.rs index 30876dc55..0319b0fe0 100644 --- a/tfhe/src/lib.rs +++ b/tfhe/src/lib.rs @@ -158,3 +158,7 @@ pub use error::{Error, ErrorKind}; pub type Result = std::result::Result; pub use tfhe_versionable::{Unversionize, Versionize}; + +/// Export tfhe-hpu-backend for external use +#[cfg(feature = "hpu")] +pub use tfhe_hpu_backend; diff --git a/tfhe/src/shortint/engine/mod.rs b/tfhe/src/shortint/engine/mod.rs index 3b5db3e29..27dfe2597 100644 --- a/tfhe/src/shortint/engine/mod.rs +++ b/tfhe/src/shortint/engine/mod.rs @@ -94,8 +94,11 @@ where assert_eq!(accumulator.polynomial_size(), polynomial_size); assert_eq!(accumulator.glwe_size(), glwe_size); + // NB: Following path will not go `power_of_two_scaling_to_native_torus` + // Thus keep value MSB aligned without considering real delta + // i.e force modulus to be native let output_encoding = ShortintEncoding { - ciphertext_modulus: accumulator.ciphertext_modulus(), + ciphertext_modulus: CiphertextModulus::new_native(), message_modulus: output_message_modulus, carry_modulus: output_carry_modulus, padding_bit: PaddingBit::Yes, diff --git a/tfhe/src/shortint/keycache.rs b/tfhe/src/shortint/keycache.rs index ce184a0c0..9d7d25b90 100644 --- a/tfhe/src/shortint/keycache.rs +++ b/tfhe/src/shortint/keycache.rs @@ -393,6 +393,7 @@ named_params_impl!( ShortintParameterSet => LEGACY_WOPBS_ONLY_2_BLOCKS_PARAM_MESSAGE_7_CARRY_0_KS_PBS, LEGACY_WOPBS_ONLY_2_BLOCKS_PARAM_MESSAGE_7_CARRY_1_KS_PBS, LEGACY_WOPBS_ONLY_2_BLOCKS_PARAM_MESSAGE_8_CARRY_0_KS_PBS, + // Coverage #[cfg(tarpaulin)] COVERAGE_PARAM_MESSAGE_2_CARRY_2_KS_PBS, @@ -406,6 +407,11 @@ named_params_impl!( ShortintParameterSet => COVERAGE_PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_PBS_KS_GAUSSIAN_2M64, #[cfg(tarpaulin)] COVERAGE_PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_KS_PBS_GAUSSIAN_2M64, + + #[cfg(feature ="hpu")] + V1_2_HPU_PARAM_MESSAGE_2_CARRY_2_KS32_PBS_GAUSSIAN_2M64, + #[cfg(feature ="hpu")] + V1_2_HPU_PARAM_MESSAGE_2_CARRY_2_KS32_PBS_TUNIFORM_2M64, ); impl NamedParam for ClassicPBSParameters { diff --git a/tfhe/src/shortint/parameters/hpu.rs b/tfhe/src/shortint/parameters/hpu.rs new file mode 100644 index 000000000..f7ebd8608 --- /dev/null +++ b/tfhe/src/shortint/parameters/hpu.rs @@ -0,0 +1,60 @@ +//! Implement bridge between native tfhe Parameters and Hpu one +use tfhe_hpu_backend::prelude::*; + +use crate::shortint::parameters::{ + CiphertextModulus32, DynamicDistribution, KeySwitch32PBSParameters, +}; +use crate::shortint::prelude::*; + +#[allow(clippy::fallible_impl_from)] +impl From<&HpuParameters> for KeySwitch32PBSParameters { + fn from(value: &HpuParameters) -> Self { + let lwe_noise_distribution = match value.pbs_params.lwe_noise_distribution { + HpuNoiseDistributionInput::GaussianStdDev(std_dev) => { + DynamicDistribution::new_gaussian_from_std_dev(StandardDev(std_dev)) + } + HpuNoiseDistributionInput::TUniformBound(log2_bound) => { + DynamicDistribution::new_t_uniform(log2_bound) + } + }; + let glwe_noise_distribution = match value.pbs_params.glwe_noise_distribution { + HpuNoiseDistributionInput::GaussianStdDev(std_dev) => { + DynamicDistribution::new_gaussian_from_std_dev(StandardDev(std_dev)) + } + HpuNoiseDistributionInput::TUniformBound(log2_bound) => { + DynamicDistribution::new_t_uniform(log2_bound) + } + }; + + Self { + lwe_dimension: LweDimension(value.pbs_params.lwe_dimension), + glwe_dimension: GlweDimension(value.pbs_params.glwe_dimension), + polynomial_size: PolynomialSize(value.pbs_params.polynomial_size), + lwe_noise_distribution, + glwe_noise_distribution, + pbs_base_log: DecompositionBaseLog(value.pbs_params.pbs_base_log), + pbs_level: DecompositionLevelCount(value.pbs_params.pbs_level), + ks_base_log: DecompositionBaseLog(value.pbs_params.ks_base_log), + ks_level: DecompositionLevelCount(value.pbs_params.ks_level), + message_modulus: MessageModulus(1 << value.pbs_params.message_width), + carry_modulus: CarryModulus(1 << value.pbs_params.carry_width), + max_noise_level: MaxNoiseLevel::new(5), + log2_p_fail: -64.0, // TODO fixme + post_keyswitch_ciphertext_modulus: CiphertextModulus32::try_new_power_of_2( + value.ks_params.width, + ) + .unwrap(), + ciphertext_modulus: CiphertextModulus::try_new_power_of_2( + value.pbs_params.ciphertext_width, + ) + .unwrap(), + modulus_switch_noise_reduction_params: None, + } + } +} + +impl From for KeySwitch32PBSParameters { + fn from(value: HpuParameters) -> Self { + Self::from(&value) + } +} diff --git a/tfhe/src/shortint/parameters/mod.rs b/tfhe/src/shortint/parameters/mod.rs index e53b92de2..737884645 100644 --- a/tfhe/src/shortint/parameters/mod.rs +++ b/tfhe/src/shortint/parameters/mod.rs @@ -30,6 +30,8 @@ pub mod classic; pub mod compact_public_key_only; #[cfg(tarpaulin)] pub mod coverage_parameters; +#[cfg(feature = "hpu")] +pub mod hpu; pub mod key_switching; pub mod ks32; pub mod list_compression; diff --git a/tfhe/src/shortint/parameters/v1_2/hpu.rs b/tfhe/src/shortint/parameters/v1_2/hpu.rs new file mode 100644 index 000000000..005a3a6bd --- /dev/null +++ b/tfhe/src/shortint/parameters/v1_2/hpu.rs @@ -0,0 +1,52 @@ +use crate::core_crypto::prelude::DynamicDistribution; +use crate::shortint::parameters::{CiphertextModulus32, KeySwitch32PBSParameters, StandardDev}; +use crate::shortint::prelude::{ + DecompositionBaseLog, DecompositionLevelCount, GlweDimension, LweDimension, PolynomialSize, +}; +use crate::shortint::{CarryModulus, CiphertextModulus, MaxNoiseLevel, MessageModulus}; + +// Gaussian parameters set +pub const V1_2_HPU_PARAM_MESSAGE_2_CARRY_2_KS32_PBS_GAUSSIAN_2M64: KeySwitch32PBSParameters = + KeySwitch32PBSParameters { + lwe_dimension: LweDimension(804), + glwe_dimension: GlweDimension(1), + polynomial_size: PolynomialSize(2048), + lwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev( + 5.963599673924788e-6, + )), + glwe_noise_distribution: DynamicDistribution::new_gaussian_from_std_dev(StandardDev( + 2.8452674713391114e-15, + )), + pbs_base_log: DecompositionBaseLog(23), + pbs_level: DecompositionLevelCount(1), + ks_base_log: DecompositionBaseLog(2), + ks_level: DecompositionLevelCount(8), + message_modulus: MessageModulus(4), + carry_modulus: CarryModulus(4), + max_noise_level: MaxNoiseLevel::new(5), + log2_p_fail: -64.0, + post_keyswitch_ciphertext_modulus: CiphertextModulus32::new(1 << 21), + ciphertext_modulus: CiphertextModulus::new_native(), + modulus_switch_noise_reduction_params: None, + }; + +// TUniform parameters set +pub const V1_2_HPU_PARAM_MESSAGE_2_CARRY_2_KS32_PBS_TUNIFORM_2M64: KeySwitch32PBSParameters = + KeySwitch32PBSParameters { + lwe_dimension: LweDimension(839), + glwe_dimension: GlweDimension(1), + polynomial_size: PolynomialSize(2048), + lwe_noise_distribution: DynamicDistribution::new_t_uniform(4), + glwe_noise_distribution: DynamicDistribution::new_t_uniform(17), + pbs_base_log: DecompositionBaseLog(23), + pbs_level: DecompositionLevelCount(1), + ks_base_log: DecompositionBaseLog(2), + ks_level: DecompositionLevelCount(7), + message_modulus: MessageModulus(4), + carry_modulus: CarryModulus(4), + max_noise_level: MaxNoiseLevel::new(5), + log2_p_fail: -64.0, + post_keyswitch_ciphertext_modulus: CiphertextModulus32::new(1 << 21), + ciphertext_modulus: CiphertextModulus::new_native(), + modulus_switch_noise_reduction_params: None, + }; diff --git a/tfhe/src/shortint/parameters/v1_2/mod.rs b/tfhe/src/shortint/parameters/v1_2/mod.rs index f3c93ac1b..1f460dc4e 100644 --- a/tfhe/src/shortint/parameters/v1_2/mod.rs +++ b/tfhe/src/shortint/parameters/v1_2/mod.rs @@ -40,6 +40,9 @@ pub use multi_bit::tuniform::p_fail_2_minus_64::ks_pbs::*; pub use multi_bit::tuniform::p_fail_2_minus_64::ks_pbs_gpu::*; pub use noise_squashing::p_fail_2_minus_128::*; +#[cfg(feature = "hpu")] +pub use hpu::*; + use crate::shortint::parameters::{ ClassicPBSParameters, CompactPublicKeyEncryptionParameters, CompressionParameters, MultiBitPBSParameters, NoiseSquashingParameters, ShortintKeySwitchingParameters, @@ -1689,3 +1692,6 @@ pub const VEC_ALL_NOISE_SQUASHING_PARAMETERS: [(&NoiseSquashingParameters, &str) &V1_2_NOISE_SQUASHING_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128, "V1_2_NOISE_SQUASHING_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128", )]; + +#[cfg(feature = "hpu")] +pub mod hpu; diff --git a/tfhe/src/test_user_docs.rs b/tfhe/src/test_user_docs.rs index dc7495172..d53444f32 100644 --- a/tfhe/src/test_user_docs.rs +++ b/tfhe/src/test_user_docs.rs @@ -240,3 +240,17 @@ mod test_gpu_doc { configuration_gpu_acceleration_multi_gpu_device_selection ); } + +#[cfg(feature = "hpu")] +mod test_hpu_doc { + use doc_comment::doctest; + + doctest!( + "../docs/configuration/hpu_acceleration/run_on_hpu.md", + configuration_hpu_acceleration_run_on_hpu + ); + doctest!( + "../docs/configuration/hpu_acceleration/benchmark.md", + configuration_hpu_acceleration_benchmark + ); +} diff --git a/tfhe/tests/hpu.rs b/tfhe/tests/hpu.rs new file mode 100644 index 000000000..dac1f0854 --- /dev/null +++ b/tfhe/tests/hpu.rs @@ -0,0 +1,632 @@ +//! Define a test-harness that handle setup and configuration of Hpu Backend +//! The test harness take a list of testcase and run them +//! A testcase simply bind a IOp to a closure describing it's behavior +//! WARN: Only one Hpu could be use at a time, thus all test must be run sequentially + +#[cfg(feature = "hpu")] +mod hpu_test { + use std::str::FromStr; + + use rand::rngs::StdRng; + use rand::{Rng, RngCore, SeedableRng}; + use tfhe::core_crypto::commons::generators::DeterministicSeeder; + use tfhe::core_crypto::prelude::DefaultRandomGenerator; + + use tfhe::Seed; + pub use tfhe_hpu_backend::prelude::*; + + /// Variable to store initialized HpuDevice and associated client key for fast iteration + static HPU_DEVICE_RNG_CKS: std::sync::OnceLock<( + std::sync::Mutex, + tfhe::integer::ClientKey, + u128, + )> = std::sync::OnceLock::new(); + + // // Instantiate a shared rng for cleartext input generation + // let rng: StdRng = SeedableRng::seed_from_u64((seed & u64::MAX as u128) as u64); + + /// Simple function used to retrieved or generate a seed from environment + fn get_or_init_seed(name: &str) -> u128 { + match std::env::var(name) { + Ok(var) => if let Some(hex) = var.strip_prefix("0x").or_else(|| var.strip_prefix("0X")) + { + u128::from_str_radix(hex, 16) + } else if let Some(bin) = var.strip_prefix("0b").or_else(|| var.strip_prefix("0B")) { + u128::from_str_radix(bin, 2) + } else if let Some(oct) = var.strip_prefix("0o").or_else(|| var.strip_prefix("0O")) { + u128::from_str_radix(oct, 8) + } else { + var.parse::() // default: base 10 + } + .unwrap_or_else(|_| panic!("{name} env variable {var} couldn't be casted in u128")), + _ => { + // Use tread_rng to generate the seed + let lsb = rand::thread_rng().next_u64() as u128; + let msb = rand::thread_rng().next_u64() as u128; + (msb << u64::BITS) | lsb + } + } + } + + fn init_hpu_and_associated_material( + ) -> (std::sync::Mutex, tfhe::integer::ClientKey, u128) { + // Hpu io dump for debug ------------------------------------------------- + #[cfg(feature = "hpu-debug")] + if let Some(dump_path) = std::env::var("HPU_IO_DUMP").ok() { + set_hpu_io_dump(&dump_path); + } + + // Instantiate HpuDevice -------------------------------------------------- + let hpu_device = { + let config_file = ShellString::new( + "${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_config.toml".to_string(), + ); + HpuDevice::from_config(&config_file.expand()) + }; + + // Check if user force a seed for the key generation + let key_seed = get_or_init_seed("HPU_KEY_SEED"); + + // Force key seeder for easily reproduce failure + let mut key_seeder = DeterministicSeeder::::new(Seed(key_seed)); + let shortint_engine = + tfhe::shortint::engine::ShortintEngine::new_from_seeder(&mut key_seeder); + tfhe::shortint::engine::ShortintEngine::with_thread_local_mut(|engine| { + std::mem::replace(engine, shortint_engine) + }); + + // Extract pbs_configuration from Hpu and create Client/Server Key + let cks = tfhe::integer::ClientKey::new( + tfhe::shortint::parameters::KeySwitch32PBSParameters::from(hpu_device.params()), + ); + let sks_compressed = + tfhe::integer::CompressedServerKey::new_radix_compressed_server_key(&cks); + + // Init Hpu device with server key and firmware + tfhe::integer::hpu::init_device(&hpu_device, sks_compressed).expect("Invalid key"); + (std::sync::Mutex::new(hpu_device), cks, key_seed) + } + + // NB: Currently u55c didn't check for workq overflow. + // -> Use default value < queue depth to circumvent this limitation + // NB': This is only for u55c, on V80 user could set HPU_TEST_ITER to whatever value he want + const DEFAULT_TEST_ITER: usize = 32; + + macro_rules! hpu_testbundle { + ($base_name: literal::$integer_width:tt => [$($testcase: literal),+]) => { + ::paste::paste! { + #[test] + pub fn []() { + // Register tracing subscriber that use env-filter + // Discard error ( mainly due to already registered subscriber) + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .compact() + .with_file(false) + .with_line_number(false) + .without_time() + .try_init(); + // Retrieved test iteration from environment ---------------------------- + let hpu_test_iter = match(std::env::var("HPU_TEST_ITER")){ + Ok(var) => usize::from_str(&var).unwrap_or_else(|_| { + panic!("HPU_TEST_ITER env variable {var} couldn't be casted in usize") + }), + _ => DEFAULT_TEST_ITER + }; + + // Retrieved HpuDevice or init --------------------------------------------- + let (hpu_mutex, cks, key_seed)= HPU_DEVICE_RNG_CKS.get_or_init(init_hpu_and_associated_material); + let mut hpu_device = hpu_mutex.lock().expect("Error with HpuDevice Mutex"); + assert!(hpu_device.config().firmware.integer_w.contains(&($integer_width as usize)), "Current Hpu configuration doesn't support {}b integer [has {:?}]", $integer_width, hpu_device.config().firmware.integer_w); + + // Instantiate a Rng for cleartest input generation + // Create a fresh one for each testbundle to be reproducible even if execution order + // of testbundle are not stable + let test_seed = get_or_init_seed("HPU_TEST_SEED"); + // Display used seed value in a reusable manner (i.e. valid bash syntax) + println!("HPU_KEY_SEED={key_seed} #[i.e. 0x{key_seed:x}]"); + println!("HPU_TEST_SEED={test_seed} #[i.e. 0x{test_seed:x}]"); + + let mut rng: StdRng = SeedableRng::seed_from_u64((test_seed & u64::MAX as u128) as u64); + + // Reseed shortint engine for reproducible noise generation. + let mut noise_seeder = DeterministicSeeder::::new(Seed(test_seed)); + let shortint_engine = + tfhe::shortint::engine::ShortintEngine::new_from_seeder(&mut noise_seeder); + tfhe::shortint::engine::ShortintEngine::with_thread_local_mut(|engine| { + std::mem::replace(engine, shortint_engine) + }); + + // Run test-case --------------------------------------------------------- + let mut acc_status = true; + $( + { + let status = [](hpu_test_iter, &mut hpu_device, &mut rng, &cks); + if !status { + println!("Error: in testcase {}", stringify!([])); + } + acc_status &= status + } + )* + + drop(hpu_device); + assert!(acc_status, "At least one testcase failed in the testbundle"); + } + } + }; +} + + macro_rules! hpu_testcase { + ($iop: literal => [$($user_type: ty),+] |$ct:ident, $imm: ident| $behav: expr) => { + ::paste::paste! { + $( + #[cfg(feature = "hpu")] + #[allow(unused)] + pub fn [](iter: usize, device: &mut HpuDevice, rng: &mut StdRng, cks: &tfhe::integer::ClientKey) -> bool { + use tfhe::integer::hpu::ciphertext::HpuRadixCiphertext; + + let iop = hpu_asm::AsmIOpcode::from_str($iop).expect("Invalid AsmIOpcode "); + let proto = if let Some(format) = iop.format() { + format.proto.clone() + } else { + eprintln!("Hpu testcase only work on specified operations. Check test definition"); + return false; + }; + + let width = $user_type::BITS as usize; + let num_block = width / device.params().pbs_params.message_width; + (0..iter).map(|_| { + // Generate inputs ciphertext + let (srcs_clear, srcs_enc): (Vec<_>, Vec<_>) = proto + .src + .iter() + .enumerate() + .map(|(pos, mode)| { + let (bw, block) = match mode { + hpu_asm::iop::VarMode::Native => (width, num_block), + hpu_asm::iop::VarMode::Half => (width / 2, num_block / 2), + hpu_asm::iop::VarMode::Bool => (1, 1), + }; + + let clear = rng.gen_range(0..=$user_type::MAX >> ($user_type::BITS - (bw as u32))); + let fhe = cks.encrypt_radix(clear, block); + let hpu_fhe = HpuRadixCiphertext::from_radix_ciphertext(&fhe, device); + (clear, hpu_fhe) + }) + .unzip(); + + let imms = (0..proto.imm) + .map(|pos| rng.gen_range(0..$user_type::MAX) as u128) + .collect::>(); + + // execute on Hpu + let res_hpu = HpuRadixCiphertext::exec(&proto, iop.opcode(), &srcs_enc, &imms); + let res_fhe = res_hpu + .iter() + .map(|x| x.to_radix_ciphertext()).collect::>(); + let res = res_fhe + .iter() + .map(|x| cks.decrypt_radix(x)) + .collect::>(); + + let exp_res = { + let $ct = &srcs_clear; + let $imm = imms.iter().map(|x| *x as $user_type).collect::>(); + ($behav.iter().map(|x| *x as $user_type).collect::>()) + }; + println!("{:>8} <{:>8x?}> <{:>8x?}> => {:<8x?} [exp {:<8x?}] {{Delta: {:x?} }}", iop, srcs_clear, imms, res, exp_res, std::iter::zip(res.iter(), exp_res.iter()).map(|(x,y)| x ^y).collect::>()); + std::iter::zip(res.iter(), exp_res.iter()).map(|(x,y)| x== y).fold(true, |acc, val| acc & val) + }).fold(true, |acc, val| acc & val) + } + )* + } + }; +} + + // Define testcase implementation for all supported IOp + // Alu IOp with Ct x Imm + hpu_testcase!("ADDS" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_add(imm[0])]); + hpu_testcase!("SUBS" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_sub(imm[0])]); + hpu_testcase!("SSUB" => [u8, u16, u32, u64, u128] + |ct, imm| [imm[0].wrapping_sub(ct[0])]); + hpu_testcase!("MULS" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_mul(imm[0])]); + + // Alu IOp with Ct x Ct + hpu_testcase!("ADD" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_add(ct[1])]); + hpu_testcase!("SUB" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_sub(ct[1])]); + hpu_testcase!("MUL" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0].wrapping_mul(ct[1])]); + + // Bitwise IOp + hpu_testcase!("BW_AND" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] & ct[1]]); + hpu_testcase!("BW_OR" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] | ct[1]]); + hpu_testcase!("BW_XOR" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] ^ ct[1]]); + + // Comparison IOp + hpu_testcase!("CMP_GT" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] > ct[1]]); + hpu_testcase!("CMP_GTE" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] >= ct[1]]); + hpu_testcase!("CMP_LT" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] < ct[1]]); + hpu_testcase!("CMP_LTE" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] <= ct[1]]); + hpu_testcase!("CMP_EQ" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] == ct[1]]); + hpu_testcase!("CMP_NEQ" => [u8, u16, u32, u64, u128] + |ct, imm| [ct[0] != ct[1]]); + + // Ternary IOp + hpu_testcase!("IF_THEN_ZERO" => [u8, u16, u32, u64, u128] + |ct, imm| [if ct[1] != 0 {ct[0]} else { 0}]); + hpu_testcase!("IF_THEN_ELSE" => [u8, u16, u32, u64, u128] + |ct, imm| [if ct[2] != 0 {ct[0]} else { ct[1]}]); + + // ERC 20 found xfer + hpu_testcase!("ERC_20" => [u8, u16, u32, u64, u128] + |ct, imm| { + let from = ct[0]; + let to = ct[1]; + let amount = ct[2]; + // TODO enhance this to prevent overflow + if from >= amount { + vec![from - amount, to.wrapping_add(amount)] + } else { + vec![from, to] + } + }); + + // Define a set of test bundle for various size + // 8bit ciphertext ----------------------------------------- + #[cfg(feature = "hpu")] + hpu_testbundle!("alus"::8 => [ + "adds", + "subs", + "ssub", + "muls" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("alu"::8 => [ + "add", + "sub", + "mul" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("bitwise"::8 => [ + "bw_and", + "bw_or", + "bw_xor" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("cmp"::8 => [ + "cmp_gt", + "cmp_gte", + "cmp_lt", + "cmp_lte", + "cmp_eq", + "cmp_neq" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("ternary"::8 => [ + "if_then_zero", + "if_then_else" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("algo"::8 => [ + "erc_20" + ]); + + // 16bit ciphertext ----------------------------------------- + #[cfg(feature = "hpu")] + hpu_testbundle!("alus"::16 => [ + "adds", + "subs", + "ssub", + "muls" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("alu"::16 => [ + "add", + "sub", + "mul" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("bitwise"::16 => [ + "bw_and", + "bw_or", + "bw_xor" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("cmp"::16 => [ + "cmp_gt", + "cmp_gte", + "cmp_lt", + "cmp_lte", + "cmp_eq", + "cmp_neq" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("ternary"::16 => [ + "if_then_zero", + "if_then_else" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("algo"::16 => [ + "erc_20" + ]); + + // 32bit ciphertext ----------------------------------------- + #[cfg(feature = "hpu")] + hpu_testbundle!("alus"::32 => [ + "adds", + "subs", + "ssub", + "muls" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("alu"::32 => [ + "add", + "sub", + "mul" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("bitwise"::32 => [ + "bw_and", + "bw_or", + "bw_xor" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("cmp"::32 => [ + "cmp_gt", + "cmp_gte", + "cmp_lt", + "cmp_lte", + "cmp_eq", + "cmp_neq" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("ternary"::32 => [ + "if_then_zero", + "if_then_else" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("algo"::32 => [ + "erc_20" + ]); + + // 64bit ciphertext ----------------------------------------- + #[cfg(feature = "hpu")] + hpu_testbundle!("alus"::64 => [ + "adds", + "subs", + "ssub", + "muls" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("alu"::64 => [ + "add", + "sub", + "mul" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("bitwise"::64 => [ + "bw_and", + "bw_or", + "bw_xor" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("cmp"::64 => [ + "cmp_gt", + "cmp_gte", + "cmp_lt", + "cmp_lte", + "cmp_eq", + "cmp_neq" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("ternary"::64 => [ + "if_then_zero", + "if_then_else" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("algo"::64 => [ + "erc_20" + ]); + + // 128bit ciphertext ----------------------------------------- + #[cfg(feature = "hpu")] + hpu_testbundle!("alus"::128 => [ + "adds", + "subs", + "ssub", + "muls" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("alu"::128 => [ + "add", + "sub", + "mul" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("bitwise"::128 => [ + "bw_and", + "bw_or", + "bw_xor" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("cmp"::128 => [ + "cmp_gt", + "cmp_gte", + "cmp_lt", + "cmp_lte", + "cmp_eq", + "cmp_neq" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("ternary"::128 => [ + "if_then_zero", + "if_then_else" + ]); + + #[cfg(feature = "hpu")] + hpu_testbundle!("algo"::128 => [ + "erc_20" + ]); + + /// Simple test dedicated to check entities conversion from/to Cpu + #[cfg(feature = "hpu")] + #[test] + fn hpu_key_loopback() { + use tfhe::core_crypto::prelude::*; + use tfhe::*; + use tfhe_hpu_backend::prelude::*; + + // Retrieved HpuDevice or init --------------------------------------------- + // Used hpu_device backed in static variable to automatically serialize tests + let (hpu_params, cks, key_seed) = { + let (hpu_mutex, cks, key_seed) = + HPU_DEVICE_RNG_CKS.get_or_init(init_hpu_and_associated_material); + let hpu_device = hpu_mutex.lock().expect("Error with HpuDevice Mutex"); + (hpu_device.params().clone(), cks, key_seed) + }; + println!("HPU_KEY_SEED={key_seed} #[i.e. 0x{key_seed:x}]"); + + // Generate Keys --------------------------------------------------------- + let sks_compressed = + tfhe::integer::CompressedServerKey::new_radix_compressed_server_key(cks) + .into_raw_parts(); + + // Unwrap compressed key --------------------------------------------------- + let ap_key = match sks_compressed.compressed_ap_server_key { + tfhe::shortint::atomic_pattern::compressed::CompressedAtomicPatternServerKey::Standard(_) => { + panic!("Hpu not support Standard keys. Required a KeySwitch32 keys") + } + tfhe::shortint::atomic_pattern::compressed::CompressedAtomicPatternServerKey::KeySwitch32(keys) => keys, + }; + + // KSK Loopback conversion and check ------------------------------------- + // Extract and convert ksk + let cpu_ksk_orig = ap_key + .key_switching_key() + .clone() + .decompress_into_lwe_keyswitch_key(); + let hpu_ksk = + HpuLweKeyswitchKeyOwned::create_from(cpu_ksk_orig.as_view(), hpu_params.clone()); + let cpu_ksk_lb = LweKeyswitchKeyOwned::::from(hpu_ksk.as_view()); + + // NB: Some hw modifications such as bit shrinki couldn't be reversed + // cpu_ksk_orig.as_mut().iter_mut().for_each(|coef| { + // let ks_p = hpu_params.ks_params; + // // Apply Hw rounding + // // Extract info bits and rounding if required + // let coef_info = *coef >> (u32::BITS - ks_p.width as u32); + // let coef_rounding = if (ks_p.width as u32) < u32::BITS { + // (*coef >> (u32::BITS - (ks_p.width + 1) as u32)) & 0x1 + // } else { + // 0 + // }; + // *coef = (coef_info + coef_rounding) << (u32::BITS - ks_p.width as u32); + // }); + + let ksk_mismatch: usize = + std::iter::zip(cpu_ksk_orig.as_ref().iter(), cpu_ksk_lb.as_ref().iter()) + .enumerate() + .map(|(i, (x, y))| { + if x != y { + println!("Ksk mismatch @{i}:: {x:x} != {y:x}"); + 1 + } else { + 0 + } + }) + .sum(); + + // BSK Loopback conversion and check ------------------------------------- + // Extract and convert ksk + let cpu_bsk_orig = match ap_key.bootstrapping_key() { + tfhe::shortint::server_key::ShortintCompressedBootstrappingKey::Classic { + bsk: seeded_bsk, + .. + } => seeded_bsk.clone().decompress_into_lwe_bootstrap_key(), + tfhe::shortint::server_key::ShortintCompressedBootstrappingKey::MultiBit { .. } => { + panic!("Hpu currently not support multibit. Required a Classic BSK") + } + }; + let cpu_bsk_ntt = { + // Convert the LweBootstrapKey in Ntt domain + let mut ntt_bsk = NttLweBootstrapKeyOwned::::new( + 0_u64, + cpu_bsk_orig.input_lwe_dimension(), + cpu_bsk_orig.glwe_size(), + cpu_bsk_orig.polynomial_size(), + cpu_bsk_orig.decomposition_base_log(), + cpu_bsk_orig.decomposition_level_count(), + CiphertextModulus::new(u64::from(&hpu_params.ntt_params.prime_modulus) as u128), + ); + + // Conversion to ntt domain + par_convert_standard_lwe_bootstrap_key_to_ntt64( + &cpu_bsk_orig, + &mut ntt_bsk, + NttLweBootstrapKeyOption::Raw, + ); + ntt_bsk + }; + let hpu_bsk = + HpuLweBootstrapKeyOwned::create_from(cpu_bsk_orig.as_view(), hpu_params.clone()); + + let cpu_bsk_lb = NttLweBootstrapKeyOwned::from(hpu_bsk.as_view()); + + let bsk_mismatch: usize = std::iter::zip( + cpu_bsk_ntt.as_view().into_container().iter(), + cpu_bsk_lb.as_view().into_container().iter(), + ) + .enumerate() + .map(|(i, (x, y))| { + if x != y { + println!("@{i}:: {x:x} != {y:x}"); + 1 + } else { + 0 + } + }) + .sum(); + + println!("Ksk loopback with {ksk_mismatch} errors"); + println!("Bsk loopback with {bsk_mismatch} errors"); + + assert_eq!(ksk_mismatch + bsk_mismatch, 0); + } +}