This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

This section is where the HEIR documentation lives.

1 - Getting Started

Getting HEIR

Using a pre-built nightly binary

HEIR releases a nightly binary for Linux x86-64. This is intended for testing compiler passes and not for production use.

wget https://github.com/google/heir/releases/download/nightly/heir-opt
chmod +x heir-opt
./heir-opt --help

Then you can run the examples below, replacing bazel run //tools:heir-opt -- with ./heir-opt. HEIR also publishes heir-translate and heir-lsp in the same way.

Running the nightly binary from a notebook

We publish an ipython extension heir-play that can be used in Jupyter or Colab notebooks.

%pip install heir-play
%load_ext heir_play

This will download the nightly release binaries to the system the notebook server is running on, then:

%%heir_opt --flag1 --flag2

# MLIR code here

Runs heir-opt with the given command line flags on the MLIR code in the cell.

A cell magic is also available for heir-translate as %%heir_translate.

Building From Source

Prerequisites

  • Git
  • A C++ compiler and linker (clang and lld are recommended).
  • Bazel via bazelisk, or version >=5.5
  • See Development for additional prerequisites for active development
Detailed Instructions The first two requirements are frequently pre-installed or can be installed via the system package manager. For example, on Ubuntu, these can be installed with
sudo apt-get update && sudo apt-get install clang lld

You can download the latest Bazelisk release, e.g., for linux-amd64 (see the Bazelisk Release Page for a list of available binaries):

wget -c https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
mv bazelisk-linux-amd64 bazel
chmod +x bazel

You will then likely want to move bazel to a location on your PATH, or add its location to your PATH, e.g.:

mkdir ~/bin
echo 'export PATH=$PATH:~/bin' >> ~/.bashrc
mv bazel ~/bin/bazel

Note that on linux systems, your OS user must not be root as bazel might refuse to work if run as root.

Clone and build the project

You can clone and build HEIR from the terminal as described below. Please see Development for information on IDE configuration if you want to use an IDE to build HEIR.

git clone git@github.com:google/heir.git && cd heir
bazel build @heir//tools:heir-opt

Some passes in this repository require Yosys as a dependency (--yosys-optimizer). If you would like to skip Yosys and ABC compilation to speed up builds, use the following build setting:

bazel build --//:enable_yosys=0 @heir//tools:heir-opt

Optional: Run the tests

bazel test @heir//...

Like above, run the following to skip tests that depend on Yosys:

bazel test --//:enable_yosys=0 --test_tag_filters=-yosys @heir//...

Using HEIR

Run the dot-product example

The dot-product program computes the dot product of two length-8 vectors of 16-bit integers (i16 in MLIR parlance). This example will showcase the OpenFHE backend by manually calling the relevant compiler passes and setting up a C++ harness to call into the HEIR-generated functions.

Note: other backends are similar, but the different backends are in varying stages of development.

The input program is in tests/Examples/openfhe/dot_product_8.mlir. Support for standard input languages like C and C++ are currently experimental at best, but eventually we would use an MLIR-based tool to convert an input language to MLIR like in that file. The program is below:

func.func @dot_product(%arg0: tensor<8xi16> {secret.secret}, %arg1: tensor<8xi16> {secret.secret}) -> i16 {
  %c0 = arith.constant 0 : index
  %c0_si16 = arith.constant 0 : i16
  %0 = affine.for %arg2 = 0 to 8 iter_args(%iter = %c0_si16) -> (i16) {
    %1 = tensor.extract %arg0[%arg2] : tensor<8xi16>
    %2 = tensor.extract %arg1[%arg2] : tensor<8xi16>
    %3 = arith.muli %1, %2 : i16
    %4 = arith.addi %iter, %3 : i16
    affine.yield %4 : i16
  }
  return %0 : i16
}

For an introduction to MLIR syntax, see the official docs or this blog post.

Now we run the heir-opt command to optimize and compile the program.

bazel run //tools:heir-opt -- \
--mlir-to-openfhe-bgv='entry-function=dot_product ciphertext-degree=8' \
$PWD/tests/Examples/openfhe/dot_product_8.mlir > output.mlir

This produces a file in the openfhe exit dialect (part of HEIR).

!Z1005037682689_i64_ = !mod_arith.int<1005037682689 : i64>
!Z1032955396097_i64_ = !mod_arith.int<1032955396097 : i64>
!Z1095233372161_i64_ = !mod_arith.int<1095233372161 : i64>
#polynomial_evaluation_encoding = #lwe.polynomial_evaluation_encoding<cleartext_start = 16, cleartext_bitwidth = 16>
!rns_L0_ = !rns.rns<!Z1095233372161_i64_>
!rns_L1_ = !rns.rns<!Z1095233372161_i64_, !Z1032955396097_i64_>
!rns_L2_ = !rns.rns<!Z1095233372161_i64_, !Z1032955396097_i64_, !Z1005037682689_i64_>
#ring_rns_L0_1_x8_ = #polynomial.ring<coefficientType = !rns_L0_, polynomialModulus = <1 + x**8>>
#ring_rns_L1_1_x8_ = #polynomial.ring<coefficientType = !rns_L1_, polynomialModulus = <1 + x**8>>
#ring_rns_L2_1_x8_ = #polynomial.ring<coefficientType = !rns_L2_, polynomialModulus = <1 + x**8>>
!rlwe_pt_L0_ = !lwe.rlwe_plaintext<encoding = #polynomial_evaluation_encoding, ring = #ring_rns_L0_1_x8_, underlying_type = i16>
!rlwe_pt_L1_ = !lwe.rlwe_plaintext<encoding = #polynomial_evaluation_encoding, ring = #ring_rns_L1_1_x8_, underlying_type = tensor<8xi16>>
!rlwe_pt_L2_ = !lwe.rlwe_plaintext<encoding = #polynomial_evaluation_encoding, ring = #ring_rns_L2_1_x8_, underlying_type = tensor<8xi16>>
#rlwe_params_L0_ = #lwe.rlwe_params<ring = #ring_rns_L0_1_x8_>
#rlwe_params_L1_ = #lwe.rlwe_params<ring = #ring_rns_L1_1_x8_>
#rlwe_params_L2_ = #lwe.rlwe_params<ring = #ring_rns_L2_1_x8_>
#rlwe_params_L2_D3_ = #lwe.rlwe_params<dimension = 3, ring = #ring_rns_L2_1_x8_>
!rlwe_ct_L0_ = !lwe.rlwe_ciphertext<encoding = #polynomial_evaluation_encoding, rlwe_params = #rlwe_params_L0_, underlying_type = i16>
!rlwe_ct_L1_ = !lwe.rlwe_ciphertext<encoding = #polynomial_evaluation_encoding, rlwe_params = #rlwe_params_L1_, underlying_type = tensor<8xi16>>
!rlwe_ct_L1_1 = !lwe.rlwe_ciphertext<encoding = #polynomial_evaluation_encoding, rlwe_params = #rlwe_params_L1_, underlying_type = i16>
!rlwe_ct_L2_ = !lwe.rlwe_ciphertext<encoding = #polynomial_evaluation_encoding, rlwe_params = #rlwe_params_L2_, underlying_type = tensor<8xi16>>
!rlwe_ct_L2_D3_ = !lwe.rlwe_ciphertext<encoding = #polynomial_evaluation_encoding, rlwe_params = #rlwe_params_L2_D3_, underlying_type = tensor<8xi16>>
module {
  func.func @dot_product(%arg0: !openfhe.crypto_context, %arg1: !rlwe_ct_L2_, %arg2: !rlwe_ct_L2_) -> !rlwe_ct_L0_ {
    %cst = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 1]> : tensor<8xi64>
    %0 = openfhe.mul_no_relin %arg0, %arg1, %arg2 : (!openfhe.crypto_context, !rlwe_ct_L2_, !rlwe_ct_L2_) -> !rlwe_ct_L2_D3_
    %1 = openfhe.relin %arg0, %0 : (!openfhe.crypto_context, !rlwe_ct_L2_D3_) -> !rlwe_ct_L2_
    %2 = openfhe.rot %arg0, %1 {index = 4 : index} : (!openfhe.crypto_context, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %3 = openfhe.add %arg0, %1, %2 : (!openfhe.crypto_context, !rlwe_ct_L2_, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %4 = openfhe.rot %arg0, %3 {index = 2 : index} : (!openfhe.crypto_context, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %5 = openfhe.add %arg0, %3, %4 : (!openfhe.crypto_context, !rlwe_ct_L2_, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %6 = openfhe.rot %arg0, %5 {index = 1 : index} : (!openfhe.crypto_context, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %7 = openfhe.add %arg0, %5, %6 : (!openfhe.crypto_context, !rlwe_ct_L2_, !rlwe_ct_L2_) -> !rlwe_ct_L2_
    %8 = openfhe.mod_reduce %arg0, %7 : (!openfhe.crypto_context, !rlwe_ct_L2_) -> !rlwe_ct_L1_
    %9 = openfhe.make_packed_plaintext %arg0, %cst : (!openfhe.crypto_context, tensor<8xi64>) -> !rlwe_pt_L1_
    %10 = openfhe.mul_plain %arg0, %8, %9 : (!openfhe.crypto_context, !rlwe_ct_L1_, !rlwe_pt_L1_) -> !rlwe_ct_L1_
    %11 = openfhe.rot %arg0, %10 {index = 7 : index} : (!openfhe.crypto_context, !rlwe_ct_L1_) -> !rlwe_ct_L1_
    %12 = lwe.reinterpret_underlying_type %11 : !rlwe_ct_L1_ to !rlwe_ct_L1_1
    %13 = openfhe.mod_reduce %arg0, %12 : (!openfhe.crypto_context, !rlwe_ct_L1_1) -> !rlwe_ct_L0_
    return %13 : !rlwe_ct_L0_
  }
  func.func @dot_product__encrypt__arg0(%arg0: !openfhe.crypto_context, %arg1: tensor<8xi16>, %arg2: !openfhe.public_key) -> !rlwe_ct_L2_ {
    ...
  }
  func.func @dot_product__encrypt__arg1(%arg0: !openfhe.crypto_context, %arg1: tensor<8xi16>, %arg2: !openfhe.public_key) -> !rlwe_ct_L2_ {
    ...
  }
  func.func @dot_product__decrypt__result0(%arg0: !openfhe.crypto_context, %arg1: !rlwe_ct_L0_, %arg2: !openfhe.private_key) -> i16 {
    ...
  }
  func.func @dot_product__generate_crypto_context() -> !openfhe.crypto_context {
    ...
  }
  func.func @dot_product__configure_crypto_context(%arg0: !openfhe.crypto_context, %arg1: !openfhe.private_key) -> !openfhe.crypto_context {
    ...
  }
}

Next, we use the heir-translate tool to run code generation for the OpenFHE pke API.

bazel run //tools:heir-translate -- --emit-openfhe-pke-header $PWD/output.mlir > heir_output.h
bazel run //tools:heir-translate -- --emit-openfhe-pke $PWD/output.mlir > heir_output.cpp

The results:

// heir_output.h
#include "src/pke/include/openfhe.h" // from @openfhe

using namespace lbcrypto;
using CiphertextT = ConstCiphertext<DCRTPoly>;
using CCParamsT = CCParams<CryptoContextBGVRNS>;
using CryptoContextT = CryptoContext<DCRTPoly>;
using EvalKeyT = EvalKey<DCRTPoly>;
using PlaintextT = Plaintext;
using PrivateKeyT = PrivateKey<DCRTPoly>;
using PublicKeyT = PublicKey<DCRTPoly>;

CiphertextT dot_product(CryptoContextT v0, CiphertextT v1, CiphertextT v2);
CiphertextT dot_product__encrypt__arg0(CryptoContextT v18, std::vector<int16_t> v19, PublicKeyT v20);
CiphertextT dot_product__encrypt__arg1(CryptoContextT v24, std::vector<int16_t> v25, PublicKeyT v26);
int16_t dot_product__decrypt__result0(CryptoContextT v30, CiphertextT v31, PrivateKeyT v32);
CryptoContextT dot_product__generate_crypto_context();
CryptoContextT dot_product__configure_crypto_context(CryptoContextT v37, PrivateKeyT v38);


// heir_output.cpp
#include "src/pke/include/openfhe.h" // from @openfhe

using namespace lbcrypto;
using CiphertextT = ConstCiphertext<DCRTPoly>;
using CryptoContextT = CryptoContext<DCRTPoly>;
using EvalKeyT = EvalKey<DCRTPoly>;
using PlaintextT = Plaintext;
using PrivateKeyT = PrivateKey<DCRTPoly>;
using PublicKeyT = PublicKey<DCRTPoly>;

CiphertextT dot_product(CryptoContextT v0, CiphertextT v1, CiphertextT v2) {
  std::vector<int64_t> v3 = {0, 0, 0, 0, 0, 0, 0, 1};
  const auto& v4 = v0->EvalMultNoRelin(v1, v2);
  const auto& v5 = v0->Relinearize(v4);
  const auto& v6 = v0->EvalRotate(v5, 4);
  const auto& v7 = v0->EvalAdd(v5, v6);
  const auto& v8 = v0->EvalRotate(v7, 2);
  const auto& v9 = v0->EvalAdd(v7, v8);
  const auto& v10 = v0->EvalRotate(v9, 1);
  const auto& v11 = v0->EvalAdd(v9, v10);
  const auto& v12 = v0->ModReduce(v11);
  auto v3_filled_n = v0->GetCryptoParameters()->GetElementParams()->GetRingDimension() / 2;
  auto v3_filled = v3;
  v3_filled.clear();
  v3_filled.reserve(v3_filled_n);
  for (auto i = 0; i < v3_filled_n; ++i) {
    v3_filled.push_back(v3[i % v3.size()]);
  }
  const auto& v13 = v0->MakePackedPlaintext(v3_filled);
  const auto& v14 = v0->EvalMult(v12, v13);
  const auto& v15 = v0->EvalRotate(v14, 7);
  const auto& v16 = v15;
  const auto& v17 = v0->ModReduce(v16);
  return v17;
}
CiphertextT dot_product__encrypt__arg0(CryptoContextT v24, std::vector<int16_t> v25, PublicKeyT v26) {
  ...
}
CiphertextT dot_product__encrypt__arg1(CryptoContextT v29, std::vector<int16_t> v30, PublicKeyT v31) {
  ...
}
int16_t dot_product__decrypt__result0(CryptoContextT v34, CiphertextT v35, PrivateKeyT v36) {
  ...
}
CryptoContextT dot_product__generate_crypto_context() {
  ...
}
CryptoContextT dot_product__configure_crypto_context(CryptoContextT v37, PrivateKeyT v38) {
  ...
}

At this point we can compile the program as we would a normal OpenFHE program. Note that the above two files just contain the compiled function and encryption/decryption helpers, and does not include any code that provides specific inputs or calls these functions.

Next we’ll create a harness that provides sample inputs, encrypts them, runs the compiled function, and decrypts the result. Once you have the generated header and cpp files, you can do this with any build system. We will use bazel for consistency.

Create a file called BUILD in the same directory as the header and cpp files above, with the following contents:

# A library build target that encapsulates the HEIR-generated code.
cc_library(
    name = "dot_product_codegen",
    srcs = ["heir_output.cpp"],
    hdrs = ["heir_output.h"],
    deps = ["@openfhe//:pke"],
)

# An executable build target that contains your main function and links
# against the above.
cc_binary(
    name = "dot_product_main",
    srcs = ["dot_product_main.cpp"],
    deps = [
        ":dot_product_codegen",
        "@openfhe//:pke",
        "@openfhe//:core",
    ],
)

Where dot_product_main.cpp is a new file containing

#include <cstdint>
#include <vector>

#include "src/pke/include/openfhe.h" // from @openfhe
#include "heir_output.h"

int main(int argc, char *argv[]) {
  CryptoContext<DCRTPoly> cryptoContext = dot_product__generate_crypto_context();

  KeyPair<DCRTPoly> keyPair;
  keyPair = cryptoContext->KeyGen();

  cryptoContext = dot_product__configure_crypto_context(cryptoContext, keyPair.secretKey);

  std::vector<int16_t> arg0 = {1, 2, 3, 4, 5, 6, 7, 8};
  std::vector<int16_t> arg1 = {2, 3, 4, 5, 6, 7, 8, 9};
  int64_t expected = 240;

  auto arg0Encrypted =
      dot_product__encrypt__arg0(cryptoContext, arg0, keyPair.publicKey);
  auto arg1Encrypted =
      dot_product__encrypt__arg1(cryptoContext, arg1, keyPair.publicKey);
  auto outputEncrypted =
      dot_product(cryptoContext, arg0Encrypted, arg1Encrypted);
  auto actual = dot_product__decrypt__result0(cryptoContext, outputEncrypted,
                                              keyPair.secretKey);

  std::cout << "Expected: " << expected << "\n";
  std::cout << "Actual: " << actual << "\n";

  return 0;
}

Then run and show the results:

$ bazel run dot_product_main
Expected: 240
Actual: 240

Optional: Run a custom heir-opt pipeline

HEIR comes with two central binaries, heir-opt for running optimization passes and dialect conversions, and heir-translate for backend code generation. To see the list of available passes in each one, run the binary with --help:

bazel run //tools:heir-opt -- --help
bazel run //tools:heir-translate -- --help

Once you’ve chosen a pass or --pass-pipeline to run, execute it on the desired file. For example, you can run a test file through heir-opt to see its output. Note that when the binary is run via bazel, you must pass absolute paths to input files. You can also access the underlying binary at bazel-bin/tools/heir-opt, provided it has already been built.

bazel run //tools:heir-opt -- \
  --secret-to-cggi -cse \
  $PWD/tests/Dialect/Secret/Conversions/secret_to_cggi/add_one.mlir

To convert an existing lit test to a bazel run command for manual tweaking and introspection (e.g., adding --debug or --mlir-print-ir-after-all to see how he IR changes with each pass), use python scripts/lit_to_bazel.py.

# after pip installing requirements-dev.txt
python scripts/lit_to_bazel.py tests/simd/box_blur_64x64.mlir

Which outputs

bazel run --noallow_analysis_cache_discard //tools:heir-opt -- \
--secretize --wrap-generic --canonicalize --cse --full-loop-unroll \
--insert-rotate --cse --canonicalize --collapse-insertion-chains \
--canonicalize --cse /path/to/heir/tests/simd/box_blur_64x64.mlir

2 - Contributing to HEIR

There are several ways to contribute to HEIR, including:

Ways to contribute

We welcome pull requests, and have tagged issues for newcomers:

For new proposals, please open a GitHub issue or start a discussion for feedback.

Contributing code to HEIR

The following steps should look familiar to typical workflows for pull request contributions. Feel free to consult GitHub Help if you need more information using pull requests. HEIR-specific processes begin at the pull request review stage.

Setup

  1. Fork the HEIR repository by clicking the Fork button on the repository page. This creates a copy of the HEIR repository on your own GitHub account, where you can make changes.

    Setting up git to work with fork and upstream remotes. If you have cloned your fork, you will want to add the HEIR repository as an upstream remote:
    git remote add upstream https://www.github.com/google/heir
    

    Alternatively, if you have cloned the main HEIR repo, you can add your fork as a remote like this:

    git remote rename origin upstream
    git remote add origin https://www.github.com/<USERNAME>/heir
    

    Either way, you will want to create a development branch for your change:

    git checkout -b name-of-change
    

    In the remainder of this document, we will assume origin is your fork, and upstream is the main HEIR repo.

  2. See Development for information on installing developer dependencies, building and running tests, and adding new dialects or passes.

  3. Sign the Contributor License Agreement (CLA). If you are working on HEIR as part of your employment, you might have to instead sign a Corporate CLA. See more here.

Preparing a pull request

  1. Sync your changes against the upstream HEIR repository, i.e., make sure your contributions are (re)based of the most recent upstream/main commit.

  2. Check HEIR’s lint and style checks by running the following from the top of the repository:

     pre-commit run --all
    
  3. Make sure tests are passing with the following:

     bazel test @heir//...
    
  4. Once you are ready with your change, create a commit, e.g.:

    git add change.cpp
    git commit -m "Detailed commit message"
    git push --set-upstream origin name-of-change
    

Pull request review flow

  1. New PR:
  • When a new PR is submitted, it is inspected for quality requirements, such as the CLA requirement, and a sufficient PR description.
  • If the PR passes checks, we assign a reviewer. If not, we request additional changes to ensure the PR passes CI checks.
  1. Review
  • A reviewer will check the PR and potentially request additional changes.
  • If a change is needed, the contributor is requested to make a suggested change. Please make changes with additional commits to your PR, to ensure that the reviewer can easily see the diff.
  • If all looks good, the reviewer will approve the PR.
  • This cycle repeats itself until the PR is approved.
  1. Approved
  • At this stage, you must squash your commits into a single commit.
  • Once the PR is approved, a GitHub workflow will check your PR for multiple commits. You may use the git rebase -i to squash the commits. Pull requests must consist of a single git commit before merging.
  1. Pull Ready
  • Once the PR is squashed into a single git commit, a maintainer will apply the pull ready label.
  • This initiates the internal code migration and presubmits.
  • After the internal process is finished, the commit will be added to main and the PR closed as merged by that commit.

Internal review details

This diagram summarizes the GitHub/Google code synchronization process. This is largely automated by a Google-owned system called Copybara, the configuration for which is Google-internal. This system treats the Google-internal version of HEIR as the source of truth, and applies specified transformation rules to copy internal changes to GitHub and integrate external PRs internally.

Notable aspects:

  • The final merged code may differ slightly from a PR. The changes are mainly to support stricter internal requirements for BUILD files that we cannot reproduce externally due to minor differences between Google’s internal build systems and bazel that we don’t know how to align. Sometimes they will also include additional code quality fixes suggested by internal static analyzers that do not exist outside of Google.
  • Due to the above, signed commits with internal modifications will not maintain valid signatures after merging, which labels the commit with a warning.
  • You will see various actions taken on GitHub that include copybara in the name, such as changes that originate from Google engineers doing various approved migrations (e.g., migrating HEIR to support changes in MLIR or abseil).

A diagram summarizing the copybara flow for HEIR internally to Google

Why bother with Copybara?

tl;dr: Automatic syncing with upstream MLIR and associated code migration.

Until HEIR has a formal governance structure in place, Google engineers—specifically Asra Ali, Shruthi Gorantala, and Jeremy Kun—are the codebase stewards. Because the project is young and the team is small, we want to reduce our workload. One important aspect of that is keeping up to date with the upstream MLIR project and incorporating bug fixes and new features into HEIR. Google also wishes to stay up to date with MLIR and LLVM, and so it has tooling devoted to integrating new MLIR changes into Google’s monorepo every few hours. As part of that rotation, a set of approved internal projects that depend on MLIR (like TensorFlow) are patched to support breaking changes in MLIR. HEIR is one of those approved projects.

As shown in the previous section, the cost of this is that no change can go into HEIR without at least two Googlers approving it, and the project is held to a specific set of code quality standards, namely Google’s. We acknowledge these quirks, and look forward to the day when HEIR is useful enough and important enough that we can revisit this governance structure with the community.

3 - Development

IDE Configuration (VS Code)

While a wide variety of IDEs and editors can be used for HEIR development, we currently only provide support for VSCode.

Setup

For the best experience, we recommend following these steps:

  • Install the MLIR, clangd and Bazel extensions

  • Install and rename Buildifier:

    You can download the latest Buildifier release, e.g., for linux-amd64 (see the Bazelisk Release Page for a list of available binaries):

    wget -c https://github.com/bazelbuild/buildtools/releases/latest/download/buildifier-linux-amd64
    mv buildifier-linux-amd64 buildifier
    chmod +x buildifier
    

    Just as with bazel, you will want to move this somewhere on your PATH, e.g.:

    mkdir ~/bin
    echo 'export PATH=$PATH:~/bin' >> ~/.bashrc
    mv buildifier ~/bin/buildifier
    

    VS Code should automatically detect buildifier. If this is not successful, you can manually set the “Buildifier Executable” setting for the Bazel extension (bazel.buildifierExecutable).

  • Disable the C/C++ (aka ‘cpptools’) extension (either completely, or in the current workspace).

  • Add the following snippet to your VS Code user settings found in .vscode/settings.json to enable autocomplete based on the compile_commands.json file.

      "clangd.arguments": [
        "--compile-commands-dir=${workspaceFolder}/",
        "--completion-style=detailed",
        "--query-driver=**"
      ],
    
  • To generate the compile_commands.json file, run

    bazel run @hedron_compile_commands//:refresh_all
    

    This will need to be regenerated every time you want tooling to see new BUILD file changes.

    If you encounter errors like *.h.inc not found, or syntax errors inside these files, you may need to build those targets and then re-run the refresh_all command above.

  • It might be necesssary to add the path to your buildifier to VSCode, though it should be auto-detected.

    • Open the heir folder in VSCode
    • Go to ‘Settings’ and set it on the ‘Workspace’
    • Search for “Bazel Buildifier Executable”
    • Once you find it, write [home-directory]/bin/buildifier for your specific [home-directory].

Building, Testing, Running and Debugging with VSCode

Building

  1. Open the “Explorer” (File Overview) in the left panel.
  2. Find “Bazel Build Targets” towards the bottom of the “Explorer” panel and click the dropdown button.
  3. Unfold the heir folder
  4. Right-click on “//tools” and click the “Build Package Recursively” option

Testing

  1. Open the “Explorer” (File Overview) in the left panel.
  2. Find “Bazel Build Targets” towards the bottom of the “Explorer” panel and click the dropdown button.
  3. Unfold the heir folder
  4. Right-click on “//test” and click the “Test Package Recursively” option

Running and Debugging

  1. Create a launch.json file in the .vscode folder, changing the "name" and "args" as required:

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Debug Secret->BGV",
                "preLaunchTask": "build",
                "type": "lldb",
                "request": "launch",
                "program": "${workspaceFolder}/bazel-bin/tools/heir-opt",
                "args": [
                    "--secret-to-bgv",
                    "--debug",
                    "${workspaceFolder}/tests/secret_to_bgv/ops.mlir"
                ],
                "relativePathBase": "${workspaceFolder}",
                "sourceMap": {
                    "proc/self/cwd": "${workspaceFolder}",
                    "/proc/self/cwd": "${workspaceFolder}"
                }
            },
        ]
    }
    

    You can add as many different configurations as necessary.

  2. Add Breakpoints to your program as desired.

  3. Open the Run/Debug panel on the left, select the desired configuration and run/debug it.

  • Note that you might have to hit “Enter” to proceed past the Bazel build. It might take several seconds between hitting “Enter” and the debug terminal opening.

Tips for working with Bazel

Avoiding rebuilds

Bazel is notoriously fickle when it comes to deciding whether a full rebuild is necessary, which is bad for HEIR because rebuilding LLVM from scratch takes 15 minutes or more.

The main things that cause a rebuild are:

  • A change to the command-line flags passed to bazel, e.g., -c opt vs -c dbg for optimization level and debug symbols.
  • A change to the .bazelrc that implicitly causes a flag change. Note HEIR has its own project-specific .bazelrc in the root directory.
  • A change to relevant command-line variables, such as PATH, which is avoided by the incompatible_strict_action_env flag. Note activating a python virtualenv triggers a PATH change.

Bazel compilation flags are set by default in the project root’s .bazelrc in such a way as to avoid rebuilds during development as much as possible. This includes setting -c dbg and --incompatible_strict_action_env.

Pointing HEIR to a local clone of llvm-project

Occasionally changes in HEIR will need to be made in tandem with upstream changes in MLIR. In particular, we occasionally find upstream bugs that only occur with HEIR passes, and we are the primary owners/users of the upstream polynomial dialect.

To tell bazel to use a local clone of llvm-project instead of a pinned commit hash, replace bazel/import_llvm.bzl with the following file:

cat > bazel/import_llvm.bzl << EOF
"""Provides the repository macro to import LLVM."""

def import_llvm(name):
    """Imports LLVM."""
    native.new_local_repository(
        name = name,
        # this BUILD file is intentionally empty, because the LLVM project
        # internally contains a set of bazel BUILD files overlaying the project.
        build_file_content = "# empty",
        path = "/path/to/llvm-project",
    )
EOF

The next bazel build will require a full rebuild if the checked-out LLVM commit differs from the pinned commit hash in bazel/import_llvm.bzl.

Note that you cannot reuse the LLVM CMake build artifacts in the bazel build. Based on what you’re trying to do, this may require some extra steps.

  • If you just want to run existing MLIR and HEIR tests against local llvm-project changes, you can run the tests from HEIR using bazel test @llvm-project//mlir/...:all. New lit tests can be added in llvm-project’s existing directories and tested this way without a rebuild.
  • If you add new CMake targets in llvm-project, then to incorporate them into HEIR you need to add new bazel targets in llvm-project/utils/bazel/llvm-project-overlay/mlir/BUILD.bazel. This is required if, for example, a new dialect or pass is added in MLIR upstream.

Send any upstream changes to HEIR-relevant MLIR files to @j2kun (Jeremy Kun) who has LLVM commit access and can also suggest additional MLIR reviewers.

Tips for building dependencies / useful external libraries

MLIR

Instructions for building MLIR can be found on the Getting started page of the MLIR website. The instructions there seem to work as written (tested on Ubuntu 22.04). However, the command shown in Unix-like compile/testing: may require a large amount of RAM. If building on a system with 16GB of RAM or less, and if you don’t plan to target GPUs, you may want to replace the line

   -DLLVM_TARGETS_TO_BUILD="Native;NVPTX;AMDGPU" \

with

   -DLLVM_TARGETS_TO_BUILD="Native" \

OpenFHE

A simple way to build OpenFHE is to follow the instructions in the openfhe-configurator repository. This allows to build the library with or without support for the Intel HEXL library. First, clone the repository and configure it using:

git clone https://github.com/openfheorg/openfhe-configurator.git
cd openfhe-configurator
scripts/configure.sh

You will be asked whether to stage a vanilla OpenFHE build or add support for HEXL. You can then build the library using

./scripts/build-openfhe-development.sh

The build may fail on systems with less than 32GB or RAM due to parallel compilation. You can disable it by editing ./scripts/build-openfhe-development.sh and replacing

make -j || abort "Build of openfhe-development failed."

with

make || abort "Build of openfhe-development failed."

Compilation will be significantly slower but should then take less than 8GB of memory.

Creating a New Pass

The scripts/templates folder contains Python scripts to create boilerplate for new conversion or (dialect-specific) transform passes. These should be used when the tablegen files containing existing pass definitions in the expected filepaths are not already present. Otherwise, you should modify the existing tablegen files directly.

Conversion Pass

To create a new conversion pass, run a command similar to the following:

python scripts/templates/templates.py new_conversion_pass \
--source_dialect_name=CGGI \
--source_dialect_namespace=cggi \
--source_dialect_mnemonic=cggi \
--target_dialect_name=TfheRust \
--target_dialect_namespace=tfhe_rust \
--target_dialect_mnemonic=tfhe_rust

In order to build the resulting code, you must fix the labeled FIXMEs in the type converter and the op conversion patterns.

Transform Passes

To create a transform or rewrite pass that operates on a dialect, run a command similar to the following:

python scripts/templates/templates.py new_dialect_transform \
--pass_name=ForgetSecrets \
--pass_flag=forget-secrets \
--dialect_name=Secret \
--dialect_namespace=secret \
--force=false

If the transform does not operate from and to a specific dialect, use

python scripts/templates/templates.py new_transform \
--pass_name=ForgetSecrets \
--pass_flag=forget-secrets \
--force=false

Pre-Commit

We use pre-commit to manage a series of git pre-commit hooks for the project; for example, each time you commit code, the hooks will make sure that your C++ is formatted properly. If your code isn’t, the hook will format it, so when you try to commit the second time you’ll get past the hook.

All hooks are defined in .pre-commit-config.yaml. To install these hooks, run

pip install -r requirements-dev.txt

Then install the hooks to run automatically on git commit:

pre-commit install

To run them manually, run

pre-commit run --all-files

5 - Design

This section contains documentation on the high level design of the project. Readers are intended to have basic familiarity with MLIR and FHE.

5.1 - Data-oblivious Transformations

A data-oblivious program is one that decouples data input from program execution. Such programs exhibit control-flow and memory access patterns that are independent of their input(s). This programming model, when applied to encrypted data, is necessary for expressing FHE programs. There are 3 major transformations applied to convert a conventional program into a data-oblivious program:

(1) If-Transformation

If-operations conditioned on inputs create data-dependent control-flow in programs. scf.if operations should at least define a ’then’ region (true path) and always terminate with scf.yield even when scf.if doesn’t produce a result. To convert a data-dependent scf.if operation to an equivalent set of data-oblivious operations in MLIR, we hoist all safely speculatable operations in the scf.if operation and convert the scf.yield operation to an arith.select operation. The following code snippet demonstrates an application of this transformation.

// Before applying If-transformation
func.func @my_function(%input : i1 {secret.secret}) -> () {
  ...
  // Violation: %input is used as a condition causing a data-dependent branch
  %result =`%input -> (i16) {
        %a = arith.muli %b, %c : i16
        scf.yield %a : i16
      } else {
        scf.yield %b : i16
      }
  ...
}

// After applying If-transformation
func.func @my_function(%input : i16 {secret.secret}) -> (){
  ...
  %a = arith.muli %b, %c : i16
  %result = arith.select %input, %a, %b : i16
  ...
}

We implement a ConvertIfToSelect pass that transforms operations with secret-input conditions and with only Pure operations (i.e., operations that have no memory side effect and are speculatable) in their body. This transformation cannot be applied to operations when side effects are present in only one of the two regions. Although possible, we currently do not support transformations for operations where both regions have operations with matching side effects. When side effects are present, the pass fails.

(2) Loop-Transformation

Loop statements with input-dependent conditions (bounds) and number of iterations introduce data-dependent branches that violate data-obliviousness. To convert such loops into a data-oblivious version, we replace input-dependent conditionals (bounds) with static input-independent parameters (e.g. defining a constant upper bound), and early-exits with update operations where the value returned from the loop is selectively updated using conditional predication. In MLIR, loops are expressed using either affine.for, scf.for or scf.while operations.

[!NOTE] Early exiting from loops is not supported in scf and affine, so early exits are not supported in this pipeline. TODO(#922): support early exits

  • affine.for: This operation lends itself well to expressing data oblivious programs because it requires constant loop bounds, eliminating input-dependent limits.
 %sum_0 = arith.constant 0.0 : f32
 // The for-loop's bound is a fixed constant
 %sum = affine.for %i = 0 to 10 step 2
 iter_args(%sum_iter = %sum_0) -> (f32) {
   %t = affine.load %buffer[%i] : memref<1024xf32>
   %sum_next = arith.addf %sum_iter, %input : f32
   affine.yield %sum_next : f32
 }
 ...
  • scf.for: In contrast to affine.for, scf.for does allow input-dependent conditionals which does not adhere to data-obliviousness constraints. A solution to this could be to either have the programmer or the compiler specify an input-independent upper bound so we can transform the loop to use this upper bound and also carefully update values returned from the for-loop using conditional predication. Our current solution to this is for the programmer to add the lower bound and worst case upper bound in the static affine loop’s attributes list.
func.func @my_function(%value: i32 {secret.secret}, %inputIndex: index {secret.secret}) -> i32 {
  ...
  // Violation: for-loop uses %inputIndex as upper bound which causes a secret-dependent control-flow
  %result = scf.for %iv = %begin to %inputIndex step %step_value iter_args(%arg1 = %value) -> i32 {
    %output = arith.muli %arg1, %arg1 : i32
    scf.yield %output : i32
  }{lower = 0, upper = 32}
  ...
}

// After applying Loop-Transformation
func.func @my_function(%value: i32 {secret.secret}, %inputIndex: index {secret.secret}) -> i32 {
  ...
  // Build for-loop using lower and upper values from the `attributes` list
  %result = affine.for %iv = 0 to  step 32 iter_args(%arg1 = %value) -> i32 {
    %output = arith.muli %arg1, %agr1 : i32
    %cond = arith.cmpi eq, %iv, %inputIndex : index
    %newOutput = arith.select %cond, %output, %arg1
    scf.yield %newOutput : i32
    }
  ...
}
  • scf.while: This operation represents a generic while/do-while loop that keeps iterating as long as a condition is met. An input-dependent while condition introduces a data-dependent control flow that violates data-oblivious constraints. For this transformation, the programmer needs to add the max_iter attribute that describes the maximum number of iterations the loop runs which we then use the value to build our static affine.for loop.
// Before applying Loop-Transformation
func.func @my_function(%input: i16 {secret.secret}){
  %zero = arith.constant 0 : i16
  %result = scf.while (%arg1 = %input) : (i16) -> i16 {
    %cond = arith.cmpi slt, %arg1, %zero : i16
    // Violation: scf.while uses %cond whose value depends on %input
    scf.condition(%cond) %arg1 : i16
  } do {
  ^bb0(%arg2: i16):
    %mul = arith.muli %arg2, %arg2: i16
    scf.yield %mul
  } attributes {max_iter = 16 : i64}
  ...
  return
}

// After applying Loop-Transformation
func.func @my_function(%input: i16 {secret.secret}){
  %zero = arith.constant 0 : i16
  %begin = arith.constant 1 : index
  ...
  // Replace while-loop with a for-loop with a constant bound %MAX_ITER
  %result = affine.for %iv = %0 to %16 step %step_value iter_args(%iter_arg = %input) -> i16 {
    %cond = arith.cmpi slt, %iter_arg, %zero : i16
    %mul = arith.muli %iter_arg, %iter_arg : i16
    %output = arith.select %cond, %mul, %iter_arg
    scf.yield %output
  }{max_iter = 16 : i64}
  ...
  return
}

(3) Access-Transformation

Input-dependent memory access cause data-dependent memory footprints. A naive data-oblivious solution to this maybe doing read-write operations over the entire data structure while only performing the desired save/update operation for the index of interest. For simplicity, we only look at load/store operations for tensors as they are well supported structures in high-level MLIR likely emitted by most frontends. We drafted the following non-SIMD approach for this transformation and defer SIMD optimizations to the heir-simd-vectorizer pass:

// Before applying Access Transformation
func.func @my_function(%input: tensor<16xi32> {secret.secret}, %inputIndex: index {secret.secret}) {
  ...
  %c_10 = arith.constant 10 : i32
  // Violation: tensor.extract loads value at %inputIndex
  %extractedValue = tensor.extract %input[%inputIndex] : tensor<16xi32>
  %newValue = arith.addi %extractedValue, %c_10 : i32
  // Violation: tensor.insert stores value at %inputIndex
  %inserted = tensor.insert %newValue into %input[%inputIndex] : tensor<16xi32>
  ...
}

// After applying Non-SIMD Access Transformation
func.func @my_function(%input: tensor<16xi32> {secret.secret}, %inputIndex: index {secret.secret}) {
  ...
  %c_10 = arith.constant 10 : i32
  %i_0 = arith.constant 0 : index
  %dummyValue = arith.constant 0 : i32

  %extractedValue = affine.for %i=0 to 16 iter_args(%arg= %dummyValue) -> (i32) {
    // 1. Check if %i matches %inputIndex
    // 2. Extract value at %i
    // 3. If %i matches %inputIndex, select %value extracted in (2), else select %dummyValue
    // 4. Yield selected value
    %cond = arith.cmpi eq, %i, %inputIndex : index
    %value = tensor.extract %input[%i] : tensor<16xi32>
    %selected = arith.select %cond, %value, %dummyValue : i32
    affine.yield %selected : i32
  }

  %newValue = arith.addi %extractedValue, %c_10 : i32

  %inserted = affine.for %i=0 to 16 iter_args(%inputArg = %input) -> tensor<16xi32> {
    // 1. Check if %i matches the %inputIndex
    // 2. Insert %newValue and produce %newTensor
    // 3. If %i matches %inputIndex, select %newTensor, else select input tensor
    // 4. Yield final tensor
    %cond = arith.cmpi eq, %i, %inputIndex : index
    %newTensor = tensor.insert %value into %inputArg[%i] : tensor<16xi32>
    %finalTensor= arith.select %cond, %newTensor, %inputArg : tensor<16xi32>
    affine.yield %finalTensor : tensor<16xi32>
  }
  ...
}

More notes on these transformations

These 3 transformations have a cascading behavior where transformations can be applied progressively to achieve a data-oblivious program. The order of the transformations goes as follows:

  • Access-Transformation (change data-dependent tensor accesses (reads-writes) to use affine.for and scf.if operations) -> Loop-Transformation (change data-dependent loops to use constant bounds and condition the loop’s yield results with scf.if operation) -> If-Transformation (substitute data-dependent conditionals with arith.select operation).
  • Besides that, when we apply non-SIMD Access-Transformation on multiple data-dependent tensor read-write operations over the same tensor, we can benefit from upstream affine transformations over the resulting multiple affine loops produced by the Access-Transformation to fuse these loops.

5.2 - Secret

The secret dialect contains types and operations to represent generic computations on secret data. It is intended to be a high-level entry point for the HEIR compiler, agnostic of any particular FHE scheme.

Most prior FHE compiler projects design their IR around a specific FHE scheme, and provide dedicated IR types for the secret analogues of existing data types, and/or dedicated operations on secret data types. For example, the Concrete compiler has !FHE.eint<32> for an encrypted 32-bit integer, and add_eint and similar ops. HECO has !fhe.secret<T> that models a generic secret type, but similarly defines fhe.add and fhe.multiply, and other projects are similar.

The problem with this approach is that it is difficult to incorporate the apply upstream canonicalization and optimization passes to these ops. For example, the arith dialect in MLIR has canonicalization patterns that must be replicated to apply to FHE analogues. One of the goals of HEIR is to reuse as much upstream infrastructure as possible, and so this led us to design the secret dialect to have both generic types and generic computations. Thus, the secret dialect has two main parts: a secret<T> type that wraps any other MLIR type T, and a secret.generic op that lifts any computation on cleartext to the “corresponding” computation on secret data types.

Overview with BGV-style lowering pipeline

Here is an example of a program that uses secret to lift a dot product computation:

func.func @dot_product(
    %arg0: !secret.secret<tensor<8xi16>>,
    %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c0_i16 = arith.constant 0 : i16
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %1 = affine.for %arg4 = 0 to 8 iter_args(%arg5 = %c0_i16) -> (i16) {
      %extracted = tensor.extract %arg2[%arg4] : tensor<8xi16>
      %extracted_0 = tensor.extract %arg3[%arg4] : tensor<8xi16>
      %2 = arith.muli %extracted, %extracted_0 : i16
      %3 = arith.addi %arg5, %2 : i16
      affine.yield %3 : i16
    }
    secret.yield %1 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

The operands to the generic op are the secret data types, and the op contains a single region, whose block arguments are the corresponding cleartext data values. Then the region is free to perform any computation, and the values passed to secret.yield are lifted back to secret types. Note that secret.generic is not isolated from its enclosing scope, so one may refer to cleartext SSA values without adding them as generic operands and block arguments.

Clearly secret.generic does not actually do anything. It is not decrypting data. It is merely describing the operation that one wishes to apply to the secret data in more familiar terms. It is a structural operation, primarily used to demarcate which operations involve secret operands and have secret results, and group them for later optimization. The benefit of this is that one can write optimization passes on types and ops that are not aware of secret, and they will naturally match on the bodies of generic ops.

For example, here is what the above dot product computation looks like after applying the -cse -canonicalize -heir-simd-vectorizer passes, the implementations of which do not depend on secret or generic.

func.func @dot_product(
    %arg0: !secret.secret<tensor<8xi16>>,
    %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %1 = arith.muli %arg2, %arg3 : tensor<8xi16>
    %2 = tensor_ext.rotate %1, %c4 : tensor<8xi16>, index
    %3 = arith.addi %1, %2 : tensor<8xi16>
    %4 = tensor_ext.rotate %3, %c2 : tensor<8xi16>, index
    %5 = arith.addi %3, %4 : tensor<8xi16>
    %6 = tensor_ext.rotate %5, %c1 : tensor<8xi16>, index
    %7 = arith.addi %5, %6 : tensor<8xi16>
    %extracted = tensor.extract %7[%c7] : tensor<8xi16>
    secret.yield %extracted : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

The canonicalization patterns for secret.generic apply a variety of simplifications, such as:

  • Removing any unused or non-secret arguments and return values.
  • Hoisting operations in the body of a generic that only depend on cleartext values to the enclosing scope.
  • Removing any generic ops that use no secrets at all.

These can be used together with the secret-distribute-generic pass to split an IR that contains a large generic op into generic ops that contain a single op, which can then be lowered to a particular FHE scheme dialect with dedicated ops. This makes lowering easier because it gives direct access to the secret version of each type that is used as input to an individual op.

As an example, a single-op secret might look like this (taken from the larger example below. Note the use of a cleartext from the enclosing scope, and the proximity of the secret type to the op to be lowered.

  %c2 = arith.constant 2 : index
  %3 = secret.generic ins(%2 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c2 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>

For a larger example, applying --secret-distribute-generic --canonicalize to the IR above:

func.func @dot_product(%arg0: !secret.secret<tensor<8xi16>>, %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.muli %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %1 = secret.generic ins(%0 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c4 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %2 = secret.generic ins(%0, %1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %3 = secret.generic ins(%2 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c2 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %4 = secret.generic ins(%2, %3 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %5 = secret.generic ins(%4 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c1 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %6 = secret.generic ins(%4, %5 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %7 = secret.generic ins(%6 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %extracted = tensor.extract %arg2[%c7] : tensor<8xi16>
    secret.yield %extracted : i16
  } -> !secret.secret<i16>
  return %7 : !secret.secret<i16>
}

And then lowering it to bgv with --secret-to-bgv="poly-mod-degree=8" (the pass option matches the tensor size, but it is an unrealistic FHE polynomial degree used here just for demonstration purposes). Note type annotations on ops are omitted for brevity.

#encoding = #lwe.polynomial_evaluation_encoding<cleartext_start = 16, cleartext_bitwidth = 16>
#params = #lwe.rlwe_params<ring = <cmod=463187969, ideal=#_polynomial.polynomial<1 + x**8>>>
!ty1 = !lwe.rlwe_ciphertext<encoding=#encoding, rlwe_params=#params, underlying_type=tensor<8xi16>>
!ty2 = !lwe.rlwe_ciphertext<encoding=#encoding, rlwe_params=#params, underlying_type=i16>

func.func @dot_product(%arg0: !ty1, %arg1: !ty1) -> !ty2 {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = bgv.mul %arg0, %arg1
  %1 = bgv.relinearize %0 {from_basis = array<i32: 0, 1, 2>, to_basis = array<i32: 0, 1>}
  %2 = bgv.rotate %1, %c4
  %3 = bgv.add %1, %2
  %4 = bgv.rotate %3, %c2
  %5 = bgv.add %3, %4
  %6 = bgv.rotate %5, %c1
  %7 = bgv.add %5, %6
  %8 = bgv.extract %7, %c7
  return %8
}

Differences for CGGI-style pipeline

The tosa-to-boolean-tfhe and related pipelines add a few additional steps. The main goal here is to apply a hardware circuit optimizer to blocks of standard MLIR code (inside secret.generic ops) which converts the computation to an optimized boolean circuit with a desired set of gates. Only then is -secret-distribute-generic applied to split the ops up and lower them to the cggi dialect. In particular, because passing an IR through the circuit optimizer requires unrolling all loops, one useful thing you might want to do is to optimize only the body of a for loop nest.

To accomplish this, we have two additional mechanisms. One is the pass option ops-to-distribute for -secret-distribute-generic, which allows the user to specify a list of ops that generic should be split across, and all others left alone. Specifying affine.for here will pass generic through the affine.for loop, but leave its body intact. This can also be used with the -unroll-factor option to the -yosys-optimizer pass to partially unroll a loop nest and pass the partially-unrolled body through the circuit optimizer.

The other mechanism is the secret.separator op, which is a purely structural op that demarcates the boundary of a subset of a block that should be jointly optimized in the circuit optimizer.

For example, the following tosa ops lower to multiple linalg instructions, and hence multiple for loops, that we want to pass to a circuit optimizer as a unit. The secret.separator ops surrounding the op are preserved through the lowering.

func.func @main(%arg0: tensor<1x1xi8> {secret.secret}) -> tensor<1x16xi32> {
  secret.separator
  %4 = "tosa.const"() {value = dense<[0, 0, -5438, -5515, -1352, -1500, -4152, -84, 3396, 0, 1981, -5581, 0, -6964, 3407, -7217]> : tensor<16xi32>} : () -> tensor<16xi32>
  %5 = "tosa.const"() {value = dense<[[-9], [-54], [57], [71], [104], [115], [98], [99], [64], [-26], [127], [25], [-82], [68], [95], [86]]> : tensor<16x1xi8>} : () -> tensor<16x1xi8>
  %6 = "tosa.fully_connected"(%arg0, %5, %4) {quantization_info = #tosa.conv_quant<input_zp = -128, weight_zp = 0>} : (tensor<1x1xi8>, tensor<16x1xi8>, tensor<16xi32>) -> tensor<1x16xi32>
  secret.separator
  return %6 : tensor<1x16xi32>
}

After running --tosa-to-boolean-tfhe and dumping the IR after the linalg ops are lowered to loops, we can see the secret.separator ops enclose the lowered ops, with the exception of some pure ops that are speculatively executed.

func.func @main(%arg0: memref<1x1xi8, strided<[?, ?], offset: ?>> {secret.secret}) -> memref<1x16xi32> {
  %c-128_i32 = arith.constant -128 : i32
  %0 = memref.get_global @__constant_16xi32 : memref<16xi32>
  %1 = memref.get_global @__constant_16x1xi8 : memref<16x1xi8>
  secret.separator
  %alloc = memref.alloc() {alignment = 64 : i64} : memref<1x16xi8>
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      %2 = affine.load %1[%arg2, %arg1] : memref<16x1xi8>
      affine.store %2, %alloc[%arg1, %arg2] : memref<1x16xi8>
    }
  }
  %alloc_0 = memref.alloc() {alignment = 64 : i64} : memref<1x16xi32>
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      %2 = affine.load %0[%arg2] : memref<16xi32>
      affine.store %2, %alloc_0[%arg1, %arg2] : memref<1x16xi32>
    }
  }
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      affine.for %arg3 = 0 to 1 {
        %2 = affine.load %arg0[%arg1, %arg3] : memref<1x1xi8, strided<[?, ?], offset: ?>>
        %3 = affine.load %alloc[%arg3, %arg2] : memref<1x16xi8>
        %4 = affine.load %alloc_0[%arg1, %arg2] : memref<1x16xi32>
        %5 = arith.extsi %2 : i8 to i32
        %6 = arith.subi %5, %c-128_i32 : i32
        %7 = arith.extsi %3 : i8 to i32
        %8 = arith.muli %6, %7 : i32
        %9 = arith.addi %4, %8 : i32
        affine.store %9, %alloc_0[%arg1, %arg2] : memref<1x16xi32>
      }
    }
  }
  secret.separator
  memref.dealloc %alloc : memref<1x16xi8>
  return %alloc_0 : memref<1x16xi32>
}

We decided to use the separator op over a few alternatives:

  • Grouping by secret.generic: these tosa ops must be bufferized, but secret types cannot participate in bufferization (see the Limitations section).
  • Grouping by basic blocks: secret.generic is a single-block op with a yield terminator, and grouping by blocks would require us to change this.
  • Grouping by regions: SSA values generated by a region are not visible to the enclosing scope, so we would need to have the region-bearing op return values, which is tedious to organize.
  • Attaching attributes to ops that should be grouped together: this would not be preserved by upstream lowerings and optimization passes.

generic operands

secret.generic takes any SSA values as legal operands. They may be secret types or non-secret. Canonicalizing secret.generic removes non-secret operands and leaves them to be referenced via the enclosing scope (secret.generic is not IsolatedFromAbove).

This may be unintuitive, as one might expect that only secret types are valid arguments to secret.generic, and that a verifier might assert non-secret args are not present.

However, we allow non-secret operands because it provides a convenient scope encapsulation mechanism, which is useful for the --yosys-optimizer pass that runs a circuit optimizer on individual secret.generic ops and needs to have access to all SSA values used as inputs. The following passes are related to this functionality:

  • secret-capture-generic-ambient-scope
  • secret-generic-absorb-constants
  • secret-extract-generic-body

Due to the canonicalization rules for secret.generic, anyone using these passes as an IR organization mechanism must be sure not to canonicalize before accomplishing the intended task.

Limitations

Bufferization

Secret types cannot participate in bufferization passes. In particular, -one-shot-bufferize hard-codes the notion of tensor and memref types, and so it cannot currently operate on secret<tensor<...>> or secret<memref<...>> types, which prevents us from implementing a bufferization interface for secret.generic. This was part of the motivation to introduce secret.separator, because tosa ops like a fully connected neural network layer lower to multiple linalg ops, and these ops need to be bufferized before they can be lowered further. However, we want to keep the lowered ops grouped together for circuit optimization (e.g., fusing transposes and constant weights into the optimized layer), but because of this limitation, we can’t simply wrap the tosa ops in a secret.generic (bufferization would fail).

5.3 - SIMD Optimizations

HEIR includes a SIMD (Single Instruction, Multiple Data) optimizer which is designed to exploit the restricted SIMD parallelism most (Ring-LWE-based) FHE schemes support (also commonly known as “packing” or “batching”). Specifically, HEIR incorporates the “automated batching” optimizations (among many other things) from the HECO compiler. The following will assume basic familiarity with the FHE SIMD paradigm and the high-level goals of the optimization, and we refer to the associated HECO paper, slides, talk and additional resources on the Usenix'23 website for an introduction to the topic. This documentation will mostly focus on describing how the optimization is realized in HEIR (which differs somewhat from the original implementation) and how the optimization is intended to be used in an overall end-to-end compilation pipeline.

Representing FHE SIMD Operations

Following the design principle of maintaining programs in standard MLIR dialects as long as possible (cf. the design rationale behind the Secret Dialect), HEIR uses the MLIR tensor dialect and ElementwiseMappable operations from the MLIR arith dialect to represent HE SIMD operations.

We do introduce the HEIR-specific tensor_ext.rotate operation, which represents a cyclical left-rotation of a tensor. Note that, as the current SIMD vectorizer only supports one-dimensional tensors, the semantics of this operation on multi-dimensional tensors are not (currently) defined.

For example, the common “rotate-and-reduce” pattern which results in each element containing the sum/product/etc of the original vector can be expressed as:

%tensor = tensor.from_elements %i1, %i2, %i3, %i4, %i5, %i6, %i7, %i8 : tensor<8xi16>
%0 = tensor_ext.rotate %tensor, %c4 : tensor<8xi16>, index
%1 = arith.addi %tensor, %0 : tensor<8xi16>
%2 = tensor_ext.rotate %1, %c2 : tensor<8xi16>, index
%3 = arith.addi %1, %2 : tensor<8xi16>
%4 = tensor_ext.rotate %3, %c1 : tensor<8xi16>, index
%5 = arith.addi %3, %4 : tensor<8xi16>

The %cN and %iN, which are defined as %cN = arith.constant N : index and %iN = arith.constant N : i16, respectively, have been omitted for readability.

Intended Usage

The -heir-simd-vectorizer pipeline transforms a program consisting of loops and index-based accesses into tensors (e.g., tensor.extract and tensor.insert) into one consisting of SIMD operations (including rotations) on entire tensors. While its implementation does not depend on any FHE-specific details or even the Secret dialect, this transformation is likely only useful when lowering a high-level program to an arithmetic-circuit-based FHE scheme (e.g., B/FV, BGV, or CKKS). The -mlir-to-openfhe-bgv pipeline demonstrates the intended flow: augmenting a high-level program with secret annotations, then applying the SIMD optimization (and any other high-level optimizations) before lowering to BGV operations and then exiting to OpenFHE.

Warning The current SIMD vectorizer pipeline supports only one-dimensional tensors. As a workaround, one could reshape all multi-dimensional tensors into one-dimensional tensors, but MLIR/HEIR currently do not provide a pass to automate this process.

Since the optimization is based on heuristics, the resulting program might not be optimal or could even be worse than a trivial realization that does not use ciphertext packing. However, well-structured programs generally lower to reasonable batched solutions, even if they do not achieve optimal batching layouts. For common operations such as matrix-vector or matrix-matrix multiplications, state-of-the-art approaches require advanced packing schemes that might map elements into the ciphertext vector in non-trivial ways (e.g., diagonal-major and/or replicated). The current SIMD vectorizer will never change the arrangement of elements inside an input tensor and therefore cannot produce the optimal approaches for these operations.

Note, that the SIMD batching optimization is different from, and significantly more complex than, the Straight Line Vectorizer (-straight-line-vectorize pass), which simply groups ElementwiseMappable operations that agree in operation name and operand/result types into vectorized/tensorized versions.

Implementation

Below, we give a brief overview over the implementation, with the goal of both improving maintainability/extensibility of the SIMD vectorizer and allowing advanced users to better understand why a certain program is transformed in the way it is.

Components

The -heir-simd-vectorizer pipeline uses a combination of standard MLIR passes (-canonicalize, -cse, -sccp) and custom HEIR passes. Some of these (-apply-folders, -full-loop-unroll) might have applications outside the SIMD optimization, while others (-insert-rotate, -collapse-insertion-chains and -rotate-and-reduce) are very specific to the FHE SIMD optimization. In addition, the passes make use of the RotationAnalysis and TargetSlotAnalysis analyses.

High-Level Flow

  • Loop Unrolling (-full-loop-unroll): The implementation currently begins by unrolling all loops in the program to simplify the later passes. See #589 for a discussion on how this could be avoided.

  • Canonicalization (-apply-folders -canonicalize): As the rotation-specific passes are very strict about the structure of the IR they operate on, we must first simplify away things such as tensors of constant values. For performance reasons (c.f. comments in the heirSIMDVectorizerPipelineBuilder function in heir-opt.cpp), this must be done by first applying folds before applying the full canonicalization.

  • Main SIMD Rewrite (-insert-rotate -cse -canonicalize -cse): This pass rewrites arithmetic operations over tensor.extract-ed operands into SIMD operations over the entire tensor, rotating the (full-tensor) operands so that the correct elements interact. For example, it will rewrite the following snippet (which computes t2[4] = t0[3] + t1[5])

    %0 = tensor.extract %t0[%c3] : tensor<32xi16>
    %1 = tensor.extract %t1[%c5] : tensor<32xi16>
    %2 = arith.addi %0, %1 : i16
    %3 = tensor.insert %2 into %t2[%c4] : tensor<32xi16>
    

    to

    %0 = tensor_ext.rotate %t0, %c31 : tensor<32xi16>, index
    %1 = tensor_ext.rotate %t1, %c1 : tensor<32xi16>, index
    %2 = arith.addi %0, %1 : tensor<32xi16>
    

    i.e., rotating t0 down by one (31 = -1 (mod 32)) and t1 up by one to bring the elements at index 3 and 5, respectively, to the “target” index 4. The pass uses the TargetSlotAnalysis to identify the appropriate target index (or ciphertext “slot” in FHE-speak). See Insert Rotate Pass below for more details. This pass is roughly equivalent to the -batching pass in the original HECO implementation.

    Doing this rewrite by itself does not represent an optimization, but if we consider what happens to the corresponding code for other indices (e.g., t2[5] = t0[4] + t1[6]), we see that the pass transforms expressions with the same relative index offsets into the exact same set of rotations/SIMD operations, so the following Common Subexpression Elimination (CSE) will remove redundant computations. We apply CSE twice, once directly (which creates new opportunities for canonicalization and folding) and then again after that canonicalization. See TensorExt Canonicalization for a description of the rotation-specific canonocalization patterns).

  • Cleanup of Redundant Insert/Extract (-collapse-insertion-chains -sccp -canonicalize -cse): Because the -insert-rotate pass maintains the consistency of the IR, it emits a tensor.extract operation after the SIMD operation and uses that to replace the original operation (which is valid, as both produce the desired scalar result). As a consequence, the generated code for the snippet above is actually trailed by a (redundant) extract/insert:

    %extracted = tensor.extract %2[%c4] : tensor<32xi16>
    %inserted = tensor.insert %extracted into %t2[%c4] : tensor<32xi16>
    

    In real code, this might generate a long series of such extraction/insertion operations, all extracting from the same (due to CSE) tensor and inserting into the same output tensor. Therefore, the -collapse-insertion-chains pass searches for such chains over entire tensors and collapses them. It supports not just chains where the indices match perfectly, but any chain where the relative offset is consistent across the tensor, issuing a rotation to realize the offset (if the offset is zero, the canonicalization will remove the redundant rotation). Note, that in HECO, insertion/extraction is handled differently, as HECO features a combine operation modelling not just simple insertions (combine(%t0#j, %t1)) but also more complex operations over slices of tensors (combine(%t0#[i,j], %t1)). As a result, the equivalent pass in HECO (-combine-simplify) instead joins different combine operations, and a later fold removes combines that replace the entire target tensor. See issue #512 for a discussion on why the combine operation is a more powerful framework and what would be necessary to port it to HEIR.

  • Applying Rotate-and-Reduce Patterns (-rotate-and-reduce -sccp -canonicalize -cse): The rotate and reduce pattern (see Representing FHE SIMD Operations for an example) is an important aspect of accelerating SIMD-style operations in FHE, but it does not follow automatically from the batching rewrites applied so far. As a result, the -rotate-and-reduce pass needs to search for sequences of arithmetic operations that correspond to the full folding of a tensor, i.e., patterns such as t[0]+(t[1]+(t[2]+t[3]+(...))), which currently uses a backwards search through the IR, but could be achieved more efficiently through a data flow analysis (c.f. issue #532). In HECO, rotate-and-reduce is handled differently, by identifying sequences of compatible operations prior to batching and rewriting them to “n-ary” operations. However, this approach requires non-standard arithmetic operations and is therefore not suitable for use in HEIR. However, there is likely still an opportunity to make the patterns in HEIR more robust/general (e.g., support constant scalar operands in the fold, or support non-full-tensor folds). See issue #522 for ideas on how to make the HEIR pattern more robust/more general.

Insert Rotate Pass

TODO(#721): Write a detailed description of the rotation insertion pass and the associated target slot analysis.

TensorExt Canonicalization

The TensorExt (tensor_ext) Dialect includes a series of canonicalization rules that are essential to making automatically generated rotation code efficient:

  • Rotation by zero: rotate %t, 0 folds away to %t

  • Cyclical wraparound: rotate %t, k for $k > t.size$ can be simplified to rotate %t, (k mod t.size)

  • Sequential rotation: %0 = rotate %t, k followed by %1 = rotate %0, l is simplified to rotate %t (k+l)

  • Extraction: %0 = rotate %t, k followed by %1 = tensor.extract %0[l] is simplified to tensor.extract %t[k+l]

  • Binary Arithmetic Ops: where both operands to a binary arith operation are rotations by the same amount, the rotation can be performed only once, on the result. For Example,

    %0 = rotate %t1, k
    %1 = rotate %t2, k
    %2 = arith.add %0, %1
    

    can be simplified to

    %0 = arith.add %t1, %t2
    %1 = rotate %0, k
    
  • Sandwiched Binary Arithmetic Ops: If a rotation follows a binary arith operation which has rotation as its operands, the post-arith operation can be moved forward. For example,

    %0 = rotate %t1, x
    %1 = rotate %t2, y
    %2 = arith.add %0, %1
    %3 = rotate %2, z
    

    can be simplified to

    %0 = rotate %t1, x + z
    %1 = rotate %t2, y + z
    %2 = arith.add %0, %1
    
  • Single-Use Arithmetic Ops: Finally, there is a pair of rules that do not eliminate rotations, but move rotations up in the IR, which can help in exposing further canonicalization and/or CSE opportunities. These only apply to arith operations with a single use, as they might otherwise increase the total number of rotations. For example,

    %0 = rotate %t1, k
    %2 = arith.add %0, %t2
    %1 = rotate %2, l
    

    can be equivalently rewritten as

    %0 = rotate %t1, (k+l)
    %1 = rotate %t2, l
    %2 = arith.add %0, %1
    

    and a similar pattern exists for situations where the rotation is the rhs operand of the arithmetic operation.

Note that the index computations in the patterns above (e.g., k+l, k mod t.size are realized via emitting arith operations. However, for constant/compile-time-known indices, these will be subsequently constant-folded away by the canonicalization pass.

5.4 - Optimizing relinearization

This document outlines the integer linear program model used in the optimize-relinearization pass.

Background

In vector/arithmetic FHE, RLWE ciphertexts often have the form $\mathbf{c} = (c_0, c_1)$, where the details of how $c_0$ and $c_1$ are computed depend on the specific scheme. However, in most of these schemes, the process of decryption can be thought of as taking a dot product between the vector $\mathbf{c}$ and a vector $(1, s)$ containing the secret key $s$ (followed by rounding).

In such schemes, the homomorphic multiplication of two ciphertexts $\mathbf{c} = (c_0, c_1)$ and $\mathbf{d} = (d_0, d_1)$ produces a ciphertext $\mathbf{f} = (f_0, f_1, f_2)$. This triple can be decrypted by taking a dot product with $(1, s, s^2)$.

With this in mind, each RLWE ciphertext $\mathbf{c}$ has an associated key basis, which is the vector $\mathbf{s_c}$ whose dot product with $\mathbf{c}$ decrypts it.

Usually a larger key basis is undesirable. For one, operations in a higher key basis are more expensive and have higher rates of noise growth. Repeated multiplications exponentially increase the length of the key basis. So to avoid this, an operation called relinearization was designed that converts a ciphertext from a given key basis back to $(1, s)$. Doing this requires a set of relinearization keys to be provided by the client and stored by the server.

In general, key bases can be arbitrary. Rotation of an RLWE ciphertext by a shift of $k$, for example, first applies the automorphism $x \mapsto x^k$. This converts the key basis from $(1, s)$ to $(1, s^k)$, and more generally maps $(1, s, s^2, \dots, s^d) \mapsto (1, s^k, s^{2k}, \dots, s^{kd})$. Most FHE implementations post-compose this automorphism with a key switching operation to return to the linear basis $(1, s)$. Similarly, multiplication can be defined for two key bases $(1, s^n)$ and $(1, s^m)$ (with $n < m$) to produce a key basis $(1, s^n, s^m, s^{n+m})$. By a combination of multiplications and rotations (without ever relinearizing or key switching), ciphertexts with a variety of strange key bases can be produced.

Most FHE implementations do not permit wild key bases because each key switch and relinearization operation (for each choice of key basis) requires additional secret key material to be stored by the server. Instead, they often enforce that rotation has key-switching built in, and multiplication relinearizes by default.

That said, many FHE implementations do allow for the relinearization operation to be deferred. A useful such situation is when a series of independent multiplications are performed, and the results are added together. Addition can operate in any key basis (though all inputs must have the same key basis), and so the relinearization op that follows each multiplication can be deferred until after the additions are complete, at which point there is only one relinearization to perform. This technique is usually called lazy relinearization. It has the benefit of avoiding expensive relinearization operations, as well as reducing noise growth, as relinearization adds noise to the ciphertext, which can further reduce the need for bootstrapping.

In much of the literature, lazy relinearization is applied manually. See for example Blatt-Gusev-Polyakov-Rohloff-Vaikuntanathan 2019 and Lee-Lee-Kim-Kim-No-Kang 2020. In some compiler projects, such as the EVA compiler relinearization is applied automatically via a heuristic, either “eagerly” (immediately after each multiplication op) or “lazily,” deferred as late as possible.

The optimize-relinearization pass

In HEIR, relinearization placement is implemented via a mixed-integer linear program (ILP). It is intended to be more general than a lazy relinearization heuristic, and certain parameter settings of the ILP reproduce lazy relinearization.

The optimize-relinearization pass starts by deleting all relinearization operations from the IR, solves the ILP, and then inserts relinearization ops according to the solution. This implies that the input IR to the ILP has no relinearization ops in it already.

Model specification

The ILP model fits into a family of models that is sometimes called “state-dynamics” models, in that it has “state” variables that track a quantity that flows through a system, as well as “decision” variables that control decisions to change the state at particular points. A brief overview of state dynamics models can be found here

In this ILP, the “state” value is the degree of the key basis. I.e., rather than track the entire key basis, we assume the key basis always has the form $(1, s, s^2, \dots, s^k)$ and track the value $k$. The index tracking state is SSA value, and the decision variables are whether to relinearize.

Variables

Define the following variables:

  • For each operation $o$, $R_o \in { 0, 1 }$ defines the decision to relinearize the result of operation $o$. Relinearization is applied if and only if $R_o = 1$.
  • For each SSA value $v$, $\textup{KB}_v$ is a continuous variable representing the degree of the key basis of $v$. For example, if the key basis of a ciphertext is $(1, s)$, then $\textup{KB}_v = 1$. If $v$ is the result of an operation $o$, $\textup{KB}_v$ is the key basis of the result of $o$ after relinearization has been optionally applied to it, depending on the value of the decision variable $R_o$.
  • For each SSA value $v$ that is an operation result, $\textup{KB}^{br}_v$ is a continuous variable whose value represents the key basis degree of $v$ before relinearization is applied (br = “before relin”). These SSA values are mainly for after the model is solved and relinearization operations need to be inserted into the IR. Here, type conflicts require us to reconstruct the key basis degree, and saving the values allows us to avoid recomputing the values.

Each of the key-basis variables is bounded from above by a parameter MAX_KEY_BASIS_DEGREE that can be used to impose hard limits on the key basis size, which may be required if generating code for a backend that does not support operations over generalized key bases.

Objective

The objective is to minimize the number of relinearization operations, i.e., $\min \sum_o R_o$.

TODO(#1018): update docs when objective is generalized.

Constraints

The simple constraints are as follows:

  • Initial key basis degree: For each block argument, $\textup{KB}_v$ is fixed to equal the dimension parameter on the RLWE ciphertext type.
  • Operand agreement: For each operation with operand SSA values $v_1, \dots, v_k$, $\textup{KB}_{v_1} = \dots = \textup{KB}_{v_k}$, i.e., all key basis inputs must match.
  • Special linearized ops: bgv.rotate and func.return require linearized inputs, i.e., $\textup{KB}_{v_i} = 1$ for all inputs $v_i$ to these operations.
  • Before relinearization key basis: for each operation $o$ with operands $v_1, \dots, v_k$, constrain $\textup{KB}^{br}_{\textup{result}(o)} = f(\textup{KB}_{v_1}, \dots, \textup{KB}_{v_k})$, where $f$ is a statically known linear function. For multiplication $f$ it addition, and for all other ops it is the projection onto any input, since multiplication is the only op that increases the degree, and all operands are constrained to have equal degree.

The remaining constraints control the dynamics of how the key basis degree changes as relinearizations are inserted.

They can be thought of as implementing this (non-linear) constraint for each operation $o$:

\[ \textup{KB}_{\textup{result}(o)} = \begin{cases} \textup{KB}^{br}_{\textup{result(o)}} & \text{ if } R_o = 0 \ 1 & \text{ if } R_o = 1 \end{cases} \]

Note that $\textup{KB}^{br}_{\textup{result}(o)}$ is constrained by one of the simple constraints to be a linear expression containing key basis variables for the operands of $o$. The conditional above cannot be implemented directly in an ILP. Instead, one can implement it via four constraints that effectively linearize (in the sense of making non-linear constraints linear) the multiplexer formula

\[ \textup{KB}_{\textup{result}(o)} = (1 - R_o) \cdot \textup{KB}^{br}_{\textup{result}(o)} + R_o \cdot 1 \]

(Note the above is not linear because in includes the product of two variables.) The four constraints are:

\[ \begin{aligned} \textup{KB}_\textup{result}(o) &\geq \textup{ R}_o \\ \textup{KB}\_\textup{result}(o) &\leq 1 + C(1 – \textup{R}_o) \\ \textup{KB}_\textup{result}(o) &\geq \textup{KB}^{br}_{\textup{result}(o)} – C \textup{ R}_o \\ \textup{KB}_\textup{result}(o) &\leq \textup{KB}^{br}_{\textup{result}(o)} + C \textup{ R}_o \\ \end{aligned} \]

Here $C$ is a constant that can be set to any value larger than MAX_KEY_BASIS_DEGREE. We set it to 100.

Setting $R_o = 0$ makes constraints 1 and 2 trivially satisfied, while constraints 3 and 4 enforce the equality $\textup{KB}_{\textup{result}(o)} = \textup{KB}^{br}_{\textup{result}(o)}$. Likewise, setting $R_o = 1$ makes constraints 3 and 4 trivially satisfied, while constraints 1 and 2 enforce the equality $\textup{KB}_{\textup{result}(o)} = 1$.

Notes

  • ILP performance scales roughly with the number of integer variables. The formulation above only requires the decision variable to be integer, and the initialization and constraints effectively force the key basis variables to be integer. As a result, the solve time of the above ILP should scale with the number of ciphertext-handling ops in the program.

6 - Pipelines

heir-opt

--heir-simd-vectorizer

Run scheme-agnostic passes to convert FHE programs that operate on scalar types to equivalent programs that operate on vectors.

This pass is intended to process FHE programs that are known to be good for SIMD, but a specific FHE scheme has not yet been chosen. It expects to handle arith ops operating on tensor types (with or without secret.generic).

The pass unrolls all loops, then applies a series of passes that convert scalar operations on tensor elements to SIMD operations on full tensors. This uses the FHE computational model common to BGV, BFV, and CKKS, in which data is packed in polynomial ciphertexts, interpreted as vectors of individual data elements, and arithmetic can be applied across entire ciphertexts, with some limited support for rotations via automorphisms of the underlying ring.

Along the way, this pipeline applies heuristic optimizations to minimize the number of rotations needed, relying on the implicit cost model that rotations are generally expensive. The specific set of passes can be found in tools/heir-opt.cpp where the pipeline is defined.

--heir-tosa-to-arith

Lowers a TOSA MLIR model to func, arith, and memref.

Lowers from TOSA through linalg and affine, and converts all tensors to memrefs. Fully unrolls all loops, and forwards stores to subsequent loads whenever possible. The output is suitable as an input to heir-translate --emit-verilog. Retains affine.load and affine.store ops that cannot be removed (e.g., reading from the input and writing to the output, or loading from a memref with a variable index).

The pass pipeline assumes that the input is a valid TOSA MLIR model with stripped quantized types. The iree-import-tflite tool can lower a TFLite FlatBuffer to textual MLIR with --output-format=mlir-ir. See hello_world.tosa.mlir for an example.

--yosys-optimizer

Uses Yosys to booleanize and optimize MLIR functions.

This pass pipeline requires inputs to be in standard MLIR (arith, affine, func, memref). The pass imports the model to Yosys and runs passes to booleanize the circuit and then uses ABC to perform optimizations. We use standard LUT 3 cells. THe output of this pass includes arith constants and comb.truth_table ops.

The pass requires that the environment variable HEIR_ABC_BINARY contains the location of the ABC binary and that HEIR_YOSYS_SCRIPTS_DIR contains the location of the Yosys’ techlib files that are needed to execute the path.

This pass can be disabled by defining HEIR_NO_YOSYS; this will avoid Yosys library and ABC binary compilation, and avoid registration of this pass.

--tosa-to-boolean-tfhe

This is an experimental pipeline for end-to-end private inference.

Converts a TOSA MLIR model to tfhe_rust dialect defined by HEIR. It converts a tosa model to optimized boolean circuit using Yosys ABC optimizations. The resultant optimized boolean circuit in comb dialect is then converted to cggi and then to tfhe_rust exit dialect. This pipeline can be used with heir-translate –emit-tfhe-rust to generate code for tfhe-rs FHE library.

The pass requires that the environment variable HEIR_ABC_BINARY contains the location of the ABC binary and that HEIR_YOSYS_SCRIPTS_DIR contains the location of the Yosys’ techlib files that are needed to execute the path.

heir-translate

--emit-tfhe-rust

Code generation for the tfhe-rs FHE library. The library is based on the CGGI cryptosystem, and so this pass is most useful when paired with lowerings from the cggi dialect.

The version of tfhe-rs supported is defined in the end to end tfhe_rust tests.

Example input:

!sks = !tfhe_rust.server_key
!lut = !tfhe_rust.lookup_table
!eui3 = !tfhe_rust.eui3

func.func @test_apply_lookup_table(%sks : !sks, %lut: !lut, %input : !eui3) -> !eui3 {
  %v1 = tfhe_rust.apply_lookup_table %sks, %input, %lut : (!sks, !eui3, !lut) -> !eui3
  %v2 = tfhe_rust.add %sks, %input, %v1 : (!sks, !eui3, !eui3) -> !eui3
  %c1 = arith.constant 1 : i8
  %v3 = tfhe_rust.scalar_left_shift %sks, %v2, %c1 : (!sks, !eui3, i8) -> !eui3
  %v4 = tfhe_rust.apply_lookup_table %sks, %v3, %lut : (!sks, !eui3, !lut) -> !eui3
  return %v4 : !eui3
}

Example output:

use tfhe::shortint::prelude::*;

pub fn test_apply_lookup_table(
  v9: &ServerKey,
  v10: &LookupTableOwned,
  v11: &Ciphertext,
) -> Ciphertext {
  let v4 = v9.apply_lookup_table(&v11, &v10);
  let v5 = v9.unchecked_add(&v11, &v4);
  let v6 = 1;
  let v7 = v9.scalar_left_shift(&v5, v6);
  let v8 = v9.apply_lookup_table(&v7, &v10);
  v8
}

Note, the chosen variable names are arbitrary, and the resulting program still must be integrated with a larger Rust program.

--emit-verilog

Code generation for verilog from arith and memref. Expects a single top level func.func op as the entry point, which is converted to the output verilog module.

Example input:

module {
  func.func @main(%arg0: i8) -> (i8) {
    %c0 = arith.constant 0 : i32
    %c1 = arith.constant 1 : i32
    %c2 = arith.constant 2 : i32
    %c3 = arith.constant 3 : i32
    %0 = arith.extsi %arg0 : i8 to i32
    %1 = arith.subi %0, %c1 : i32
    %2 = arith.muli %1, %c2 : i32
    %3 = arith.addi %2, %c3 : i32
    %4 = arith.cmpi sge, %2, %c0 : i32
    %5 = arith.select %4, %c1, %c2 : i32
    %6 = arith.shrsi %3, %c1 : i32
    %7 = arith.shrui %3, %c1 : i32
    %out = arith.trunci %6 : i32 to i8
    return %out : i8
  }
}

Output:

module main(
  input wire signed [7:0] arg1,
  output wire signed [7:0] _out_
);
  wire signed [31:0] v2;
  wire signed [31:0] v3;
  wire signed [31:0] v4;
  wire signed [31:0] v5;
  wire signed [31:0] v6;
  wire signed [31:0] v7;
  wire signed [31:0] v8;
  wire signed [31:0] v9;
  wire v10;
  wire signed [31:0] v11;
  wire signed [31:0] v12;
  wire signed [31:0] v13;
  wire signed [7:0] v14;

  assign v2 = 0;
  assign v3 = 1;
  assign v4 = 2;
  assign v5 = 3;
  assign v6 = {{24{arg1[7]}}, arg1};
  assign v7 = v6 - v3;
  assign v8 = v7 * v4;
  assign v9 = v8 + v5;
  assign v10 = v8 >= v2;
  assign v11 = v10 ? v3 : v4;
  assign v12 = v9 >>> v3;
  assign v13 = v9 >> v3;
  assign v14 = v12[7:0];
  assign _out_ = v14;
endmodule

--emit-metadata

Prints a json object describing the function signatures. Used for code generation after --emit-verilog.

Example input:

module {
  func.func @main(%arg0: memref<80xi8>) -> memref<1x3x2x1xi8> {
    %alloc_0 = memref.alloc() {alignment = 64 : i64} : memref<1x3x2x1xi8>
    return %alloc_0 : memref<1x3x2x1xi8>
  }
}

Example output:

{
  "functions": [
    {
      "name": "main",
      "params": [
        {
          "index": 0,
          "type": {
            "memref": {
              "element_type": {
                "integer": {
                  "is_signed": false,
                  "width": 8
                }
              },
              "shape": [80]
            }
          }
        }
      ],
      "return_types": [{
        "memref": {
          "element_type": {
            "integer": {
              "is_signed": false,
              "width": 8
            }
          },
          "shape": [1, 3, 2, 1]
        }
      }]
    }
  ]
}

7 - Dialects

This section contains the reference documentation for all of the dialects defined in HEIR.

7.1 - BGV

‘bgv’ Dialect

The BGV dialect defines the types and operations of the BGV cryptosystem.

BGV ops

bgv.add (heir::bgv::AddOp)

Addition operation between ciphertexts.

Syntax:

operation ::= `bgv.add` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.add_plain (heir::bgv::AddPlainOp)

Addition operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.add_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

bgv.extract (heir::bgv::ExtractOp)

Extract the i-th element of a ciphertext.

Syntax:

operation ::= `bgv.extract` operands attr-dict `:`  functional-type(operands, results)

While this operation is costly to compute in FHE, we represent it so we can implement efficient lowerings and folders.

This op can be implemented as a plaintext multiplication with a one-hot vector and a rotate into the zero-th index.

An extraction op’s input ciphertext type is asserted to have an underlying_type corresponding to a ranked tensor type, and this op’s return type is inferred to have the underlying_type corresponding to the element type of that tensor type.

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultRings

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA ciphertext type
offsetsignless integer or index

Results:

ResultDescription
outputA ciphertext type

bgv.modulus_switch (heir::bgv::ModulusSwitchOp)

Lower the modulus level of the ciphertext.

Syntax:

operation ::= `bgv.modulus_switch` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Attributes:

AttributeMLIR TypeDescription
to_ring::mlir::heir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.mul (heir::bgv::MulOp)

Multiplication operation between ciphertexts.

Syntax:

operation ::= `bgv.mul` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, InferTypeOpAdaptor, SameOperandsAndResultRings, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.mul_plain (heir::bgv::MulPlainOp)

Multiplication operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.mul_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

bgv.negate (heir::bgv::NegateOp)

Negate the coefficients of the ciphertext.

Syntax:

operation ::= `bgv.negate` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Involution, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.relinearize (heir::bgv::RelinearizeOp)

Relinearize the ciphertext.

Syntax:

operation ::= `bgv.relinearize` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

This op takes integer array attributes from_basis and to_basis that are used to indicate the key basis from which and to which the ciphertext is encrypted against. A ciphertext is canonically encrypted against key basis (1, s). After a multiplication, its size will increase and the basis will be (1, s, s^2). The array that represents the key basis is constructed by listing the powers of s at each position of the array. For example, (1, s, s^2) corresponds to [0, 1, 2], while (1, s^2) corresponds to [0, 2].

Traits: InferTypeOpAdaptor, SameOperandsAndResultRings

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
from_basis::mlir::DenseI32ArrayAttri32 dense array attribute
to_basis::mlir::DenseI32ArrayAttri32 dense array attribute

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.rotate (heir::bgv::RotateOp)

Rotate the coefficients of the ciphertext using a Galois automorphism.

Syntax:

operation ::= `bgv.rotate` operands attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
offset::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.sub (heir::bgv::SubOp)

Subtraction operation between ciphertexts.

Syntax:

operation ::= `bgv.sub` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

bgv.sub_plain (heir::bgv::SubPlainOp)

Subtraction operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.sub_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

7.2 - CGGI

‘cggi’ Dialect

A dialect for types and operations in the CGGI cryptosystem

CGGI attributes

CGGIBoolGatesAttr

An attribute containing an array of strings to store bool gates

Syntax:

#cggi.cggi_bool_gates<
  ::llvm::ArrayRef<::mlir::heir::cggi::CGGIBoolGateEnumAttr>   # gates
>
This attributes stores a list of integer identifiers for Boolean gates.

Uses following mapping: AND_GATE = 0; NAND_GATE = 1; OR_GATE = 2; NOR_GATE = 3; XOR_GATE = 4; XNOR_GATE = 5; NOT_GATE = 6;

Parameters:

ParameterC++ typeDescription
gates::llvm::ArrayRef<::mlir::heir::cggi::CGGIBoolGateEnumAttr>

CGGIParamsAttr

Syntax:

#cggi.cggi_params<
  ::mlir::heir::lwe::RLWEParamsAttr,   # rlweParams
  unsigned,   # bsk_noise_variance
  unsigned,   # bsk_gadget_base_log
  unsigned,   # bsk_gadget_num_levels
  unsigned,   # ksk_noise_variance
  unsigned,   # ksk_gadget_base_log
  unsigned   # ksk_gadget_num_levels
>

Parameters:

ParameterC++ typeDescription
rlweParams::mlir::heir::lwe::RLWEParamsAttr
bsk_noise_varianceunsigned
bsk_gadget_base_logunsigned
bsk_gadget_num_levelsunsigned
ksk_noise_varianceunsigned
ksk_gadget_base_logunsigned
ksk_gadget_num_levelsunsigned

CGGI ops

cggi.and (heir::cggi::AndOp)

Logical AND of two ciphertexts.

Syntax:

operation ::= `cggi.and` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut2 (heir::cggi::Lut2Op)

A lookup table on two inputs.

Syntax:

operation ::= `cggi.lut2` operands attr-dict `:` qualified(type($output))

An op representing a lookup table applied to some number n of ciphertexts encrypting boolean input bits.

Over cleartext bits a, b, c, using n = 3 for example, the operation computed by this function can be interpreted as

  truth_table >> {c, b, a}

where {c, b, a} is the unsigned 3-bit integer with bits c, b, a from most significant bit to least-significant bit. The input are combined into a single ciphertext input to the lookup table using products with plaintexts and sums.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
bciphertext-like
aciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut3 (heir::cggi::Lut3Op)

A lookup table on three inputs.

Syntax:

operation ::= `cggi.lut3` operands attr-dict `:` qualified(type($output))

An op representing a lookup table applied to some number n of ciphertexts encrypting boolean input bits.

Over cleartext bits a, b, c, using n = 3 for example, the operation computed by this function can be interpreted as

  truth_table >> {c, b, a}

where {c, b, a} is the unsigned 3-bit integer with bits c, b, a from most significant bit to least-significant bit. The input are combined into a single ciphertext input to the lookup table using products with plaintexts and sums.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
cciphertext-like
bciphertext-like
aciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut_lincomb (heir::cggi::LutLinCombOp)

A variadic-input lookup table with inputs prepared via linear combination.

Syntax:

operation ::= `cggi.lut_lincomb` operands attr-dict `:` type($output)

An op representing a lookup table applied to an arbitrary number of input ciphertexts, which are combined according to a static linear combination attached to the op.

The user must ensure the chosen linear combination does not bleed error bits into the message space according to the underlying ciphertext’s encoding attributes. E.g., a bit_field_encoding with 3 cleartext bits cannot be multiplied by 16.

Example:

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
#params = #lwe.lwe_params<cmod=7917, dimension=4>
!ciphertext = !lwe.lwe_ciphertext<encoding = #encoding, lwe_params = #params>

%4 = cggi.lut_lincomb %c0, %c1, %c2, %c3 {coefficients = array<i32: 1, 2, 3, 2>, lookup_table = 68 : index} : !ciphertext

Represents applying the lut

  68 >> (1 * c0 + 2 * c1 + 3 * c2 + 2 * c3)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
coefficients::mlir::DenseI32ArrayAttri32 dense array attribute
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of any type

Results:

ResultDescription
outputany type

cggi.multi_lut_lincomb (heir::cggi::MultiLutLinCombOp)

A multi-output version of lut_lincomb with one LUT per output.

Syntax:

operation ::= `cggi.multi_lut_lincomb` operands attr-dict `:` functional-type($inputs, $outputs)

An op representing multiple lookup tables applied to a shared input, which is prepared via a static linear combination. This is equivalent to cggi.lut_lincomb, but where the linear combination is given to multiple lookup tables, each producing a separate output.

This can be achieved by a special implementation of blind rotate in the CGGI scheme. See AutoHoG.

Example:

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
#params = #lwe.lwe_params<cmod=7917, dimension=4>
!ciphertext = !lwe.lwe_ciphertext<encoding = #encoding, lwe_params = #params>

%4 = cggi.multi_lut_lincomb %c0, %c1, %c2, %c3 {
    coefficients = array<i32: 1, 2, 3, 2>,
    lookup_tables = array<index: 68, 70, 4, 8>
} : (!ciphertext, !ciphertext, !ciphertext, !ciphertext) -> (!ciphertext, !ciphertext, !ciphertext, !ciphertext)

Represents applying the following LUTs. Performance-wise, this is comparable to applying a single LUT to a linear combination.

x = (1 * c0 + 2 * c1 + 3 * c2 + 2 * c3)
return (
  (68 >> x) & 1,
  (70 >> x) & 1,
  (4 >> x) & 1,
  (8 >> x) & 1
)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
coefficients::mlir::DenseI32ArrayAttri32 dense array attribute
lookup_tables::mlir::DenseI32ArrayAttri32 dense array attribute

Operands:

OperandDescription
inputsvariadic of A type for LWE ciphertexts

Results:

ResultDescription
outputsvariadic of A type for LWE ciphertexts

cggi.nand (heir::cggi::NandOp)

Logical NAND of two ciphertexts.

Syntax:

operation ::= `cggi.nand` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.nor (heir::cggi::NorOp)

Logical NOR of two ciphertexts.

Syntax:

operation ::= `cggi.nor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.not (heir::cggi::NotOp)

Logical NOT of two ciphertexts

Syntax:

operation ::= `cggi.not` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Elementwise, Involution, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.or (heir::cggi::OrOp)

Logical OR of two ciphertexts.

Syntax:

operation ::= `cggi.or` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.packed_lut3 (heir::cggi::PackedLut3Op)

Syntax:

operation ::= `cggi.packed_lut3` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_tables::mlir::ArrayAttrArray of integers

Operands:

OperandDescription
aciphertext-like
bciphertext-like
cciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.packed_gates (heir::cggi::PackedOp)

Syntax:

operation ::= `cggi.packed_gates` operands attr-dict `:` functional-type(operands, results)

Operation to where different Boolean gates are executed pairwise between elements of two ciphertext arrays.

For example,

%0 = cggi.packed_gates %a, %b {gates = #cggi.cggi_gate<"and", "xor">} : tensor<2x!lwe.lwe_ciphertext>

applies an “and” gate to the first elements of %a and %b and an xor gate to the second elements.

Mapping is defined in the BooleanGates.td file.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
gates::mlir::heir::cggi::CGGIBoolGatesAttrAn attribute containing an array of strings to store bool gates

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.programmable_bootstrap (heir::cggi::ProgrammableBootstrapOp)

Programmable Bootstrap with a given lookup table.

Syntax:

operation ::= `cggi.programmable_bootstrap` operands attr-dict `:` qualified(type($output))

An op representing a programmable bootstrap applied to an LWE ciphertext.

This operation evaluates a univariate function homomorphically on the ciphertext by selecting the correct value from a lookup table. The bit size of the lookup table integer attribute should be equal to the plaintext space size. For example, if there ciphertext can hold 3 plaintext message bits, then the lookup table must be represented at most by an integer with 8 bits.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.xnor (heir::cggi::XNorOp)

Logical XNOR of two ciphertexts.

Syntax:

operation ::= `cggi.xnor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.xor (heir::cggi::XorOp)

Logical XOR of two ciphertexts.

Syntax:

operation ::= `cggi.xor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

CGGI additional definitions

AffineMapAttr

An Attribute containing an AffineMap object

Syntax:

affine-map-attribute ::= `affine_map` `<` affine-map `>`

Examples:

affine_map<(d0) -> (d0)>
affine_map<(d0, d1, d2) -> (d0, d1)>

Parameters:

ParameterC++ typeDescription
valueAffineMap

ArrayAttr

A collection of other Attribute values

Syntax:

array-attribute ::= `[` (attribute-value (`,` attribute-value)*)? `]`

An array attribute is an attribute that represents a collection of attribute values.

Examples:

[]
[10, i32]
[affine_map<(d0, d1, d2) -> (d0, d1)>, i32, "string attribute"]

Parameters:

ParameterC++ typeDescription
value::llvm::ArrayRef<Attribute>

DenseArrayAttr

A dense array of integer or floating point elements.

A dense array attribute is an attribute that represents a dense array of primitive element types. Contrary to DenseIntOrFPElementsAttr this is a flat unidimensional array which does not have a storage optimization for splat. This allows to expose the raw array through a C++ API as ArrayRef<T> for compatible types. The element type must be bool or an integer or float whose bitwidth is a multiple of 8. Bool elements are stored as bytes.

This is the base class attribute. Access to C++ types is intended to be managed through the subclasses DenseI8ArrayAttr, DenseI16ArrayAttr, DenseI32ArrayAttr, DenseI64ArrayAttr, DenseF32ArrayAttr, and DenseF64ArrayAttr.

Syntax:

dense-array-attribute ::= `array` `<` (integer-type | float-type)
                                      (`:` tensor-literal)? `>`

Examples:

array<i8>
array<i32: 10, 42>
array<f64: 42., 12.>

When a specific subclass is used as argument of an operation, the declarative assembly will omit the type and print directly:

[1, 2, 3]

Parameters:

ParameterC++ typeDescription
elementTypeType
sizeint64_t
rawData::llvm::ArrayRef<char>64-bit aligned storage for dense array elements

DenseIntOrFPElementsAttr

An Attribute containing a dense multi-dimensional array of integer or floating-point values

Syntax:

tensor-literal ::= integer-literal | float-literal | bool-literal | [] | [tensor-literal (, tensor-literal)* ]
dense-intorfloat-elements-attribute ::= `dense` `<` tensor-literal `>` `:`
                                        ( tensor-type | vector-type )

A dense int-or-float elements attribute is an elements attribute containing a densely packed vector or tensor of integer or floating-point values. The element type of this attribute is required to be either an IntegerType or a FloatType.

Examples:

// A splat tensor of integer values.
dense<10> : tensor<2xi32>
// A tensor of 2 float32 elements.
dense<[10.0, 11.0]> : tensor<2xf32>

Parameters:

ParameterC++ typeDescription
typeShapedType
rawDataArrayRef<char>

DenseResourceElementsAttr

An Attribute containing a dense multi-dimensional array backed by a resource

Syntax:

dense-resource-elements-attribute ::=
  `dense_resource` `<` resource-handle `>` `:` shaped-type

A dense resource elements attribute is an elements attribute backed by a handle to a builtin dialect resource containing a densely packed array of values. This class provides the low-level attribute, which should only be interacted with in very generic terms, actual access to the underlying resource data is intended to be managed through one of the subclasses, such as; DenseBoolResourceElementsAttr, DenseUI64ResourceElementsAttr, DenseI32ResourceElementsAttr, DenseF32ResourceElementsAttr, DenseF64ResourceElementsAttr, etc.

Examples:

"example.user_op"() {attr = dense_resource<blob1> : tensor<3xi64> } : () -> ()

{-#
dialect_resources: {
    builtin: {
      blob1: "0x08000000010000000000000002000000000000000300000000000000"
    }
  }
#-}

Parameters:

ParameterC++ typeDescription
typeShapedType
rawHandleDenseResourceElementsHandle

DenseStringElementsAttr

An Attribute containing a dense multi-dimensional array of strings

Syntax:

dense-string-elements-attribute ::= `dense` `<` attribute-value `>` `:`
                                    ( tensor-type | vector-type )

A dense string elements attribute is an elements attribute containing a densely packed vector or tensor of string values. There are no restrictions placed on the element type of this attribute, enabling the use of dialect specific string types.

Examples:

// A splat tensor of strings.
dense<"example"> : tensor<2x!foo.string>
// A tensor of 2 string elements.
dense<["example1", "example2"]> : tensor<2x!foo.string>

Parameters:

ParameterC++ typeDescription
typeShapedType
valueArrayRef<StringRef>

DictionaryAttr

An dictionary of named Attribute values

Syntax:

dictionary-attribute ::= `{` (attribute-entry (`,` attribute-entry)*)? `}`

A dictionary attribute is an attribute that represents a sorted collection of named attribute values. The elements are sorted by name, and each name must be unique within the collection.

Examples:

{}
{attr_name = "string attribute"}
{int_attr = 10, "string attr name" = "string attribute"}

Parameters:

ParameterC++ typeDescription
value::llvm::ArrayRef<NamedAttribute>

FloatAttr

An Attribute containing a floating-point value

Syntax:

float-attribute ::= (float-literal (`:` float-type)?)
                  | (hexadecimal-literal `:` float-type)

A float attribute is a literal attribute that represents a floating point value of the specified float type. It can be represented in the hexadecimal form where the hexadecimal value is interpreted as bits of the underlying binary representation. This form is useful for representing infinity and NaN floating point values. To avoid confusion with integer attributes, hexadecimal literals must be followed by a float type to define a float attribute.

Examples:

42.0         // float attribute defaults to f64 type
42.0 : f32   // float attribute of f32 type
0x7C00 : f16 // positive infinity
0x7CFF : f16 // NaN (one of possible values)
42 : f32     // Error: expected integer type

Parameters:

ParameterC++ typeDescription
type::mlir::Type
value::llvm::APFloat

IntegerAttr

An Attribute containing a integer value

Syntax:

integer-attribute ::= (integer-literal ( `:` (index-type | integer-type) )?)
                      | `true` | `false`

An integer attribute is a literal attribute that represents an integral value of the specified integer or index type. i1 integer attributes are treated as boolean attributes, and use a unique assembly format of either true or false depending on the value. The default type for non-boolean integer attributes, if a type is not specified, is signless 64-bit integer.

Examples:

10 : i32
10    // : i64 is implied here.
true  // A bool, i.e. i1, value.
false // A bool, i.e. i1, value.

Parameters:

ParameterC++ typeDescription
type::mlir::Type
valueAPInt

IntegerSetAttr

An Attribute containing an IntegerSet object

Syntax:

integer-set-attribute ::= `affine_set` `<` integer-set `>`

Examples:

affine_set<(d0) : (d0 - 2 >= 0)>

Parameters:

ParameterC++ typeDescription
valueIntegerSet

OpaqueAttr

An opaque representation of another Attribute

Syntax:

opaque-attribute ::= dialect-namespace `<` attr-data `>`

Opaque attributes represent attributes of non-registered dialects. These are attribute represented in their raw string form, and can only usefully be tested for attribute equality.

Examples:

#dialect<"opaque attribute data">

Parameters:

ParameterC++ typeDescription
dialectNamespaceStringAttr
attrData::llvm::StringRef
type::mlir::Type

SparseElementsAttr

An opaque representation of a multi-dimensional array

Syntax:

sparse-elements-attribute ::= `sparse` `<` attribute-value `,`
                              attribute-value `>` `:`
                              ( tensor-type | vector-type )

A sparse elements attribute is an elements attribute that represents a sparse vector or tensor object. This is where very few of the elements are non-zero.

The attribute uses COO (coordinate list) encoding to represent the sparse elements of the elements attribute. The indices are stored via a 2-D tensor of 64-bit integer elements with shape [N, ndims], which specifies the indices of the elements in the sparse tensor that contains non-zero values. The element values are stored via a 1-D tensor with shape [N], that supplies the corresponding values for the indices.

Example:

sparse<[[0, 0], [1, 2]], [1, 5]> : tensor<3x4xi32>

// This represents the following tensor:
///  [[1, 0, 0, 0],
///   [0, 0, 5, 0],
///   [0, 0, 0, 0]]

Parameters:

ParameterC++ typeDescription
typeShapedType
indicesDenseIntElementsAttr
valuesDenseElementsAttr

StringAttr

An Attribute containing a string

Syntax:

string-attribute ::= string-literal (`:` type)?

A string attribute is an attribute that represents a string literal value.

Examples:

"An important string"
"string with a type" : !dialect.string

Parameters:

ParameterC++ typeDescription
value::llvm::StringRef
type::mlir::Type

SymbolRefAttr

An Attribute containing a symbolic reference to an Operation

Syntax:

symbol-ref-attribute ::= symbol-ref-id (`::` symbol-ref-id)*

A symbol reference attribute is a literal attribute that represents a named reference to an operation that is nested within an operation with the OpTrait::SymbolTable trait. As such, this reference is given meaning by the nearest parent operation containing the OpTrait::SymbolTable trait. It may optionally contain a set of nested references that further resolve to a symbol nested within a different symbol table.

Rationale: Identifying accesses to global data is critical to enabling efficient multi-threaded compilation. Restricting global data access to occur through symbols and limiting the places that can legally hold a symbol reference simplifies reasoning about these data accesses.

See Symbols And SymbolTables for more information.

Examples:

@flat_reference
@parent_reference::@nested_reference

Parameters:

ParameterC++ typeDescription
rootReferenceStringAttr
nestedReferences::llvm::ArrayRef<FlatSymbolRefAttr>

TypeAttr

An Attribute containing a Type

Syntax:

type-attribute ::= type

A type attribute is an attribute that represents a type object.

Examples:

i32
!dialect.type

Parameters:

ParameterC++ typeDescription
valueType

UnitAttr

An Attribute value of unit type

Syntax:

unit-attribute ::= `unit`

A unit attribute is an attribute that represents a value of unit type. The unit type allows only one value forming a singleton set. This attribute value is used to represent attributes that only have meaning from their existence.

One example of such an attribute could be the swift.self attribute. This attribute indicates that a function parameter is the self/context parameter. It could be represented as a boolean attribute(true or false), but a value of false doesn’t really bring any value. The parameter either is the self/context or it isn’t.

Examples:

// A unit attribute defined with the `unit` value specifier.
func.func @verbose_form() attributes {dialectName.unitAttr = unit}

// A unit attribute in an attribute dictionary can also be defined without
// the value specifier.
func.func @simple_form() attributes {dialectName.unitAttr}

StridedLayoutAttr

An Attribute representing a strided layout of a shaped type

Syntax:

strided-layout-attribute ::= `strided` `<` `[` stride-list `]`
                             (`,` `offset` `:` dimension)? `>`
stride-list ::= /*empty*/
              | dimension (`,` dimension)*
dimension ::= decimal-literal | `?`

A strided layout attribute captures layout information of the memref type in the canonical form. Specifically, it contains a list of strides, one for each dimension. A stride is the number of elements in the linear storage one must step over to reflect an increment in the given dimension. For example, a MxN row-major contiguous shaped type would have the strides [N, 1]. The layout attribute also contains the offset from the base pointer of the shaped type to the first effectively accessed element, expressed in terms of the number of contiguously stored elements.

Strides must be positive and the offset must be non-negative. Both the strides and the offset may be dynamic, i.e. their value may not be known at compile time. This is expressed as a ? in the assembly syntax and as ShapedType::kDynamic in the code. Stride and offset values must satisfy the constraints above at runtime, the behavior is undefined otherwise.

See [Dialects/Builtin.md#memreftype](MemRef type) for more information.

Parameters:

ParameterC++ typeDescription
offsetint64_t
strides::llvm::ArrayRef<int64_t>array of strides (64-bit integer)

7.3 - CKKS

‘ckks’ Dialect

The CKKS dialect defines the types and operations of the CKKS cryptosystem.

CKKS ops

ckks.add (heir::ckks::AddOp)

Addition operation between ciphertexts.

Syntax:

operation ::= `ckks.add` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.add_plain (heir::ckks::AddPlainOp)

Addition operation between ciphertext-plaintext.

Syntax:

operation ::= `ckks.add_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

ckks.extract (heir::ckks::ExtractOp)

Extract the i-th element of a ciphertext.

Syntax:

operation ::= `ckks.extract` operands attr-dict `:`  functional-type(operands, results)

While this operation is costly to compute in FHE, we represent it so we can implement efficient lowerings and folders.

This op can be implemented as a plaintext multiplication with a one-hot vector and a rotate into the zero-th index.

An extraction op’s input ciphertext type is asserted to have an underlying_type corresponding to a ranked tensor type, and this op’s return type is inferred to have the underlying_type corresponding to the element type of that tensor type.

Traits: SameOperandsAndResultRings

Operands:

OperandDescription
inputA ciphertext type
offsetsignless integer or index

Results:

ResultDescription
outputA ciphertext type

ckks.mul (heir::ckks::MulOp)

Multiplication operation between ciphertexts.

Syntax:

operation ::= `ckks.mul` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, InferTypeOpAdaptor, SameOperandsAndResultRings, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.mul_plain (heir::ckks::MulPlainOp)

Multiplication operation between ciphertext-plaintext.

Syntax:

operation ::= `ckks.mul_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

ckks.negate (heir::ckks::NegateOp)

Negate the coefficients of the ciphertext.

Syntax:

operation ::= `ckks.negate` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Involution, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.relinearize (heir::ckks::RelinearizeOp)

Relinearize the ciphertext.

Syntax:

operation ::= `ckks.relinearize` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

This op takes integer array attributes from_basis and to_basis that are used to indicate the key basis from which and to which the ciphertext is encrypted against. A ciphertext is canonically encrypted against key basis (1, s). After a multiplication, its size will increase and the basis will be (1, s, s^2). The array that represents the key basis is constructed by listing the powers of s at each position of the array. For example, (1, s, s^2) corresponds to [0, 1, 2], while (1, s^2) corresponds to [0, 2].

Traits: AlwaysSpeculatableImplTrait, InferTypeOpAdaptor, SameOperandsAndResultRings

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
from_basis::mlir::DenseI32ArrayAttri32 dense array attribute
to_basis::mlir::DenseI32ArrayAttri32 dense array attribute

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.rescale (heir::ckks::RescaleOp)

Rescales the ciphertext, which is the CKKS version of modulus switching in BGV/BFV.

Syntax:

operation ::= `ckks.rescale` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
to_ring::mlir::heir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.rotate (heir::ckks::RotateOp)

Rotate the coefficients of the ciphertext using a Galois automorphism.

Syntax:

operation ::= `ckks.rotate` operands attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
offset::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.sub (heir::ckks::SubOp)

Subtraction operation between ciphertexts.

Syntax:

operation ::= `ckks.sub` operands attr-dict `:` qualified(type($output))

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

ckks.sub_plain (heir::ckks::SubPlainOp)

Subtraction operation between ciphertext-plaintext.

Syntax:

operation ::= `ckks.sub_plain` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextInputA ciphertext type
plaintextInputA plaintext type

Results:

ResultDescription
outputA ciphertext type

7.4 - Comb

‘comb’ Dialect

Types and operations for comb dialect

This dialect defines the comb dialect, which is intended to be a generic representation of combinational logic outside of a particular use-case.

Operations

comb.add (heir::comb::AddOp)

Syntax:

operation ::= `comb.add` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.and (heir::comb::AndOp)

Syntax:

operation ::= `comb.and` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.concat (heir::comb::ConcatOp)

Concatenate a variadic list of operands together.

Syntax:

operation ::= `comb.concat` $inputs attr-dict `:` qualified(type($inputs))

See the comb rationale document for details on operand ordering.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.extract (heir::comb::ExtractOp)

Extract a range of bits into a smaller value, lowBit specifies the lowest bit included.

Syntax:

operation ::= `comb.extract` $input `from` $lowBit attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lowBit::mlir::IntegerAttr32-bit signless integer attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.icmp (heir::comb::ICmpOp)

Compare two integer values

Syntax:

operation ::= `comb.icmp` (`bin` $twoState^)? $predicate $lhs `,` $rhs attr-dict `:` qualified(type($lhs))

This operation compares two integers using a predicate. If the predicate is true, returns 1, otherwise returns 0. This operation always returns a one bit wide result.

    %r = comb.icmp eq %a, %b : i4

Traits: AlwaysSpeculatableImplTrait, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
predicate::mlir::heir::comb::ICmpPredicateAttrhw.icmp comparison predicate
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
lhssignless integer
rhssignless integer

Results:

ResultDescription
result1-bit signless integer

comb.inv (heir::comb::InvOp)

Syntax:

operation ::= `comb.inv` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.mul (heir::comb::MulOp)

Syntax:

operation ::= `comb.mul` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.mux (heir::comb::MuxOp)

Return one or the other operand depending on a selector bit

Syntax:

operation ::= `comb.mux` (`bin` $twoState^)? $cond `,` $trueValue `,` $falseValue  attr-dict `:` qualified(type($result))
  %0 = mux %pred, %tvalue, %fvalue : i4

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
cond1-bit signless integer
trueValueany type
falseValueany type

Results:

ResultDescription
resultany type

comb.nand (heir::comb::NandOp)

Syntax:

operation ::= `comb.nand` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.nor (heir::comb::NorOp)

Syntax:

operation ::= `comb.nor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.or (heir::comb::OrOp)

Syntax:

operation ::= `comb.or` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.parity (heir::comb::ParityOp)

Syntax:

operation ::= `comb.parity` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
result1-bit signless integer

comb.replicate (heir::comb::ReplicateOp)

Concatenate the operand a constant number of times

Syntax:

operation ::= `comb.replicate` $input attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.truth_table (heir::comb::TruthTableOp)

Return a true/false based on a lookup table

Syntax:

operation ::= `comb.truth_table` $inputs `->` $lookupTable attr-dict
  %a = ... : i1
  %b = ... : i1
  %0 = comb.truth_table %a, %b -> 6 : ui4

This operation assumes that the lookup table is described as an integer of 2^n bits to fully specify the table. Inputs are sorted MSB -> LSB from left to right and the offset into lookupTable is computed from them. The integer containing the truth table value’s LSB is the output for the input “all false”, and the MSB is the output for the input “all true”.

No difference from array_get into an array of constants except for xprop behavior. If one of the inputs is unknown, but said input doesn’t make a difference in the output (based on the lookup table) the result should not be ‘x’ – it should be the well-known result.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookupTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of 1-bit signless integer

Results:

ResultDescription
result1-bit signless integer

comb.xnor (heir::comb::XNorOp)

Syntax:

operation ::= `comb.xnor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.xor (heir::comb::XorOp)

Syntax:

operation ::= `comb.xor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

Enums

ICmpPredicate

hw.icmp comparison predicate

Cases:

SymbolValueString
eq0eq
ne1ne
slt2slt
sle3sle
sgt4sgt
sge5sge
ult6ult
ule7ule
ugt8ugt
uge9uge
ceq10ceq
cne11cne
weq12weq
wne13wne

Comb types

Comb ops

comb.add (heir::comb::AddOp)

Syntax:

operation ::= `comb.add` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.and (heir::comb::AndOp)

Syntax:

operation ::= `comb.and` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.concat (heir::comb::ConcatOp)

Concatenate a variadic list of operands together.

Syntax:

operation ::= `comb.concat` $inputs attr-dict `:` qualified(type($inputs))

See the comb rationale document for details on operand ordering.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.extract (heir::comb::ExtractOp)

Extract a range of bits into a smaller value, lowBit specifies the lowest bit included.

Syntax:

operation ::= `comb.extract` $input `from` $lowBit attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lowBit::mlir::IntegerAttr32-bit signless integer attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.icmp (heir::comb::ICmpOp)

Compare two integer values

Syntax:

operation ::= `comb.icmp` (`bin` $twoState^)? $predicate $lhs `,` $rhs attr-dict `:` qualified(type($lhs))

This operation compares two integers using a predicate. If the predicate is true, returns 1, otherwise returns 0. This operation always returns a one bit wide result.

    %r = comb.icmp eq %a, %b : i4

Traits: AlwaysSpeculatableImplTrait, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
predicate::mlir::heir::comb::ICmpPredicateAttrhw.icmp comparison predicate
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
lhssignless integer
rhssignless integer

Results:

ResultDescription
result1-bit signless integer

comb.inv (heir::comb::InvOp)

Syntax:

operation ::= `comb.inv` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.mul (heir::comb::MulOp)

Syntax:

operation ::= `comb.mul` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.mux (heir::comb::MuxOp)

Return one or the other operand depending on a selector bit

Syntax:

operation ::= `comb.mux` (`bin` $twoState^)? $cond `,` $trueValue `,` $falseValue  attr-dict `:` qualified(type($result))
  %0 = mux %pred, %tvalue, %fvalue : i4

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
cond1-bit signless integer
trueValueany type
falseValueany type

Results:

ResultDescription
resultany type

comb.nand (heir::comb::NandOp)

Syntax:

operation ::= `comb.nand` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.nor (heir::comb::NorOp)

Syntax:

operation ::= `comb.nor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.or (heir::comb::OrOp)

Syntax:

operation ::= `comb.or` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.parity (heir::comb::ParityOp)

Syntax:

operation ::= `comb.parity` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
result1-bit signless integer

comb.replicate (heir::comb::ReplicateOp)

Concatenate the operand a constant number of times

Syntax:

operation ::= `comb.replicate` $input attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.truth_table (heir::comb::TruthTableOp)

Return a true/false based on a lookup table

Syntax:

operation ::= `comb.truth_table` $inputs `->` $lookupTable attr-dict
  %a = ... : i1
  %b = ... : i1
  %0 = comb.truth_table %a, %b -> 6 : ui4

This operation assumes that the lookup table is described as an integer of 2^n bits to fully specify the table. Inputs are sorted MSB -> LSB from left to right and the offset into lookupTable is computed from them. The integer containing the truth table value’s LSB is the output for the input “all false”, and the MSB is the output for the input “all true”.

No difference from array_get into an array of constants except for xprop behavior. If one of the inputs is unknown, but said input doesn’t make a difference in the output (based on the lookup table) the result should not be ‘x’ – it should be the well-known result.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookupTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of 1-bit signless integer

Results:

ResultDescription
result1-bit signless integer

comb.xnor (heir::comb::XNorOp)

Syntax:

operation ::= `comb.xnor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.xor (heir::comb::XorOp)

Syntax:

operation ::= `comb.xor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

7.5 - Jaxite

‘jaxite’ Dialect

The jaxite dialect is an exit dialect for generating py code against the jaxite library API, using the jaxite parameters and encoding scheme.

See https://github.com/google/jaxite

Jaxite types

ParamsType

The jaxite security params required to perform homomorphic operations.

Syntax: !jaxite.params

PmapLut3TupleType

A tuple of pmap_lut3 args.

Syntax: !jaxite.pmap_lut3_tuple

ServerKeySetType

The jaxite server key set required to perform homomorphic operations.

Syntax: !jaxite.server_key_set

Jaxite ops

jaxite.constant (heir::jaxite::ConstantOp)

Syntax:

operation ::= `jaxite.constant` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
value1-bit signless integer
paramsThe jaxite security params required to perform homomorphic operations.

Results:

ResultDescription
outputA type for LWE ciphertexts

jaxite.lut3_args (heir::jaxite::Lut3ArgsOp)

Syntax:

operation ::= `jaxite.lut3_args` operands attr-dict `:` functional-type(operands, results)

The operation computed by this function is generating tuples for pmap_lut3.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
aA type for LWE ciphertexts
bA type for LWE ciphertexts
cA type for LWE ciphertexts
truth_table8-bit signless integer

Results:

ResultDescription
outputA tuple of pmap_lut3 args.

jaxite.lut3 (heir::jaxite::Lut3Op)

Syntax:

operation ::= `jaxite.lut3` operands attr-dict `:` functional-type(operands, results)

The operation computed by this function can be interpreted as

truth_table » {c, b, a}

where {c, b, a} is the unsigned 3-bit integer with bits c, b, a from most significant bit to least-significant bit.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
aA type for LWE ciphertexts
bA type for LWE ciphertexts
cA type for LWE ciphertexts
truth_table8-bit signless integer
serverKeySetThe jaxite server key set required to perform homomorphic operations.
paramsThe jaxite security params required to perform homomorphic operations.

Results:

ResultDescription
outputA type for LWE ciphertexts

jaxite.pmap_lut3 (heir::jaxite::PmapLut3Op)

Syntax:

operation ::= `jaxite.pmap_lut3` operands attr-dict `:` functional-type(operands, results)

The operation computed by this function is a bacthed Lut3Op.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lut3_argspmap_lut3_tuples
serverKeySetThe jaxite server key set required to perform homomorphic operations.
paramsThe jaxite security params required to perform homomorphic operations.

Results:

ResultDescription
outputciphertext-like

7.6 - Lattigo

Lattigo attributes

BGVParametersLiteralAttr

Literal parameters for Lattigo BGV

Syntax:

#lattigo.bgv.parameters_literal<
  int,   # logN
  DenseI64ArrayAttr,   # Q
  DenseI64ArrayAttr,   # P
  DenseI32ArrayAttr,   # logQ
  DenseI32ArrayAttr,   # logP
  int64_t   # plaintextModulus
>

This attribute represents the literal parameters for Lattigo BGV.

This is in accordance with https://pkg.go.dev/github.com/tuneinsight/lattigo/v6@v6.1.0/schemes/bgv#ParametersLiteral where some field are not present in the current implementation.

Users must set the polynomial degree (LogN) and the coefficient modulus, by either setting the Q and P fields to the desired moduli chain, or by setting the LogQ and LogP fields to the desired moduli sizes.

Note that for Lattigo, Q/P requires []uint64, where this attribute only provides int64. We assume user should not select moduli so large to consider the signedness issue.

Users must also specify the coefficient modulus in plaintext-space (T). This modulus must be an NTT-friendly prime in the plaintext space: it must be equal to 1 modulo 2n where n is the plaintext ring degree (i.e., the plaintext space has n slots).

Parameters:

ParameterC++ typeDescription
logNint
QDenseI64ArrayAttr
PDenseI64ArrayAttr
logQDenseI32ArrayAttr
logPDenseI32ArrayAttr
plaintextModulusint64_t

Lattigo types

BGVEncoderType

Syntax: !lattigo.bgv.encoder

This type represents the encoder for the BGV encryption scheme.

BGVEvaluatorType

Syntax: !lattigo.bgv.evaluator

This type represents the evaluator for the BGV encryption scheme.

BGVParameterType

Syntax: !lattigo.bgv.parameter

This type represents the parameters for the BGV encryption scheme.

RLWECiphertextType

Syntax: !lattigo.rlwe.ciphertext

This type represents the ciphertext for the RLWE encryption scheme.

RLWEDecryptorType

Syntax: !lattigo.rlwe.decryptor

This type represents the decryptor for the RLWE encryption scheme.

RLWEEncryptorType

Syntax: !lattigo.rlwe.encryptor

This type represents the encryptor for the RLWE encryption scheme.

RLWEEvaluationKeySetType

Syntax: !lattigo.rlwe.evaluation_key_set

This type represents the evaluation key set for the RLWE encryption scheme.

RLWEGaloisKeyType

Syntax:

!lattigo.rlwe.galois_key<
  ::mlir::IntegerAttr   # galoisElement
>

This type represents the Galois key for the RLWE encryption scheme.

galoisElement: Enabling the automorphism X -> X^{galoisElement}.

Parameters:

ParameterC++ typeDescription
galoisElement::mlir::IntegerAttrAn Attribute containing a integer value

RLWEKeyGeneratorType

Syntax: !lattigo.rlwe.key_generator

This type represents the key generator for the RLWE encryption scheme.

RLWEPlaintextType

Syntax: !lattigo.rlwe.plaintext

This type represents the plaintext for the RLWE encryption scheme.

RLWEPublicKeyType

Syntax: !lattigo.rlwe.public_key

This type represents the public key for the RLWE encryption scheme.

RLWERelinearizationKeyType

Syntax: !lattigo.rlwe.relinearization_key

This type represents the relinearization key for the RLWE encryption scheme.

RLWESecretKeyType

Syntax: !lattigo.rlwe.secret_key

This type represents the secret key for the RLWE encryption scheme.

Lattigo ops

lattigo.bgv.add (heir::lattigo::BGVAddOp)

Add two ciphertexts in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.add` operands attr-dict `:` functional-type(operands, results)

This operation adds two ciphertext values in the Lattigo BGV dialect.

Operands:

OperandDescription
evaluator
lhs
rhs

Results:

ResultDescription
output

lattigo.bgv.decode (heir::lattigo::BGVDecodeOp)

Decode a plaintext value in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.decode` operands attr-dict `:` functional-type(operands, results)

This operation decodes a plaintext value using the specified encoder in the Lattigo BGV dialect.

Operands:

OperandDescription
encoder
plaintext
valueany type

Results:

ResultDescription
decodedany type

lattigo.bgv.encode (heir::lattigo::BGVEncodeOp)

Encode a plaintext value in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.encode` operands attr-dict `:` functional-type(operands, results)

This operation encodes a plaintext value using the specified encoder in the Lattigo BGV dialect.

Operands:

OperandDescription
encoder
valueany type
plaintext

Results:

ResultDescription
encoded

lattigo.bgv.mul (heir::lattigo::BGVMulOp)

Multiply two ciphertexts in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.mul` operands attr-dict `:` functional-type(operands, results)

This operation multiplies two ciphertext values in the Lattigo BGV dialect.

Operands:

OperandDescription
evaluator
lhs
rhs

Results:

ResultDescription
output

lattigo.bgv.new_encoder (heir::lattigo::BGVNewEncoderOp)

Create a new encoder in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.new_encoder` operands attr-dict `:` functional-type(operands, results)

This operation creates a new encoder for encoding plaintext values in the Lattigo BGV dialect.

Operands:

OperandDescription
params

Results:

ResultDescription
encoder

lattigo.bgv.new_evaluator (heir::lattigo::BGVNewEvaluatorOp)

Create a new evaluator in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.new_evaluator` operands attr-dict `:` functional-type(operands, results)

This operation creates a new evaluator for performing operations on ciphertexts in the Lattigo BGV dialect.

By default, the evaluator is created with the provided parameters and could execute operations which does not relying on evaluation keys.

To support operations that require evaluation keys, the optional evaluation key set should be provided.

Operands:

OperandDescription
params
evaluationKeySet

Results:

ResultDescription
evaluator

lattigo.bgv.new_parameters_from_literal (heir::lattigo::BGVNewParametersFromLiteralOp)

Create new BGV parameters from a literal in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.new_parameters_from_literal` operands attr-dict `:` functional-type(operands, results)

This operation creates new BGV parameters from a given literal value in the Lattigo BGV dialect.

Attributes:

AttributeMLIR TypeDescription
paramsLiteral::mlir::heir::lattigo::BGVParametersLiteralAttrLiteral parameters for Lattigo BGV

Results:

ResultDescription
params

lattigo.bgv.new_plaintext (heir::lattigo::BGVNewPlaintextOp)

Create a new plaintext in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.new_plaintext` operands attr-dict `:` functional-type(operands, results)

This operation creates a new plaintext value in the Lattigo BGV dialect.

Operands:

OperandDescription
params

Results:

ResultDescription
plaintext

lattigo.bgv.relinearize (heir::lattigo::BGVRelinearizeOp)

Relinearize a ciphertext in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.relinearize` operands attr-dict `:` functional-type(operands, results)

This operation relinearizes a ciphertext value in the Lattigo BGV dialect.

Operands:

OperandDescription
evaluator
input

Results:

ResultDescription
output

lattigo.bgv.rescale (heir::lattigo::BGVRescaleOp)

Rescale a ciphertext in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.rescale` operands attr-dict `:` functional-type(operands, results)

This operation rescales a ciphertext value in the Lattigo BGV dialect.

Operands:

OperandDescription
evaluator
input

Results:

ResultDescription
output

lattigo.bgv.rotate_columns (heir::lattigo::BGVRotateColumnsOp)

Rotate columns of a ciphertext in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.rotate_columns` operands attr-dict `:` functional-type(operands, results)

This operation rotates the columns of a ciphertext value in the Lattigo BGV dialect.

Lattigo exposes the SIMD slot of BGV as a N/2 x 2 matrix, where N/2 is the column.

offset is valid in (-N/2, N/2).

Attributes:

AttributeMLIR TypeDescription
offset::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
evaluator
input

Results:

ResultDescription
output

lattigo.bgv.rotate_rows (heir::lattigo::BGVRotateRowsOp)

Rotate rows of a ciphertext in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.rotate_rows` operands attr-dict `:` functional-type(operands, results)

This operation swap the rows of a ciphertext value in the Lattigo BGV dialect.

Lattigo exposes the SIMD slot of BGV as a N/2 x 2 matrix, where 2 is the row.

Operands:

OperandDescription
evaluator
input

Results:

ResultDescription
output

lattigo.bgv.sub (heir::lattigo::BGVSubOp)

Subtract two ciphertexts in the Lattigo BGV dialect

Syntax:

operation ::= `lattigo.bgv.sub` operands attr-dict `:` functional-type(operands, results)

This operation subtracts one ciphertext value from another in the Lattigo BGV dialect.

Operands:

OperandDescription
evaluator
lhs
rhs

Results:

ResultDescription
output

lattigo.rlwe.decrypt (heir::lattigo::RLWEDecryptOp)

Decrypts a ciphertext using RLWE

Syntax:

operation ::= `lattigo.rlwe.decrypt` operands attr-dict `:` functional-type(operands, results)

This operation decrypts a ciphertext using RLWE

Operands:

OperandDescription
decryptor
ciphertext

Results:

ResultDescription
plaintext

lattigo.rlwe.encrypt (heir::lattigo::RLWEEncryptOp)

Encrypts a plaintext using RLWE

Syntax:

operation ::= `lattigo.rlwe.encrypt` operands attr-dict `:` functional-type(operands, results)

This operation encrypts a plaintext using RLWE

Operands:

OperandDescription
encryptor
plaintext

Results:

ResultDescription
ciphertext

lattigo.rlwe.gen_galois_key (heir::lattigo::RLWEGenGaloisKeyOp)

Generates a new RLWE Galois key

Syntax:

operation ::= `lattigo.rlwe.gen_galois_key` operands attr-dict `:` functional-type(operands, results)

This operation generates a new RLWE Galois key

galoisElement: Enabling the automorphism X -> X^{galoisElement}.

Attributes:

AttributeMLIR TypeDescription
galoisElement::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
keyGenerator
secretKey

Results:

ResultDescription
galoisKey

lattigo.rlwe.gen_key_pair (heir::lattigo::RLWEGenKeyPairOp)

Generates a new RLWE key pair

Syntax:

operation ::= `lattigo.rlwe.gen_key_pair` operands attr-dict `:` functional-type(operands, results)

This operation generates a new RLWE key pair

Operands:

OperandDescription
keyGenerator

Results:

ResultDescription
secretKey
publicKey

lattigo.rlwe.gen_relinearization_key (heir::lattigo::RLWEGenRelinearizationKeyOp)

Generates a new RLWE relinearization key

Syntax:

operation ::= `lattigo.rlwe.gen_relinearization_key` operands attr-dict `:` functional-type(operands, results)

This operation generates a new RLWE relinearization key

Operands:

OperandDescription
keyGenerator
secretKey

Results:

ResultDescription
relinearizationKey

lattigo.rlwe.new_decryptor (heir::lattigo::RLWENewDecryptorOp)

Creates a new RLWE decryptor

Syntax:

operation ::= `lattigo.rlwe.new_decryptor` operands attr-dict `:` functional-type(operands, results)

This operation creates a new RLWE decryptor

Operands:

OperandDescription
params
secretKey

Results:

ResultDescription
decryptor

lattigo.rlwe.new_encryptor (heir::lattigo::RLWENewEncryptorOp)

Creates a new RLWE encryptor

Syntax:

operation ::= `lattigo.rlwe.new_encryptor` operands attr-dict `:` functional-type(operands, results)

This operation creates a new RLWE encryptor

Operands:

OperandDescription
params
publicKey

Results:

ResultDescription
encryptor

lattigo.rlwe.new_evaluation_key_set (heir::lattigo::RLWENewEvaluationKeySetOp)

Generates a new RLWE evaluation key set

Syntax:

operation ::= `lattigo.rlwe.new_evaluation_key_set` operands attr-dict `:` functional-type(operands, results)

This operation generates a new RLWE evaluation key set

Operands:

OperandDescription
relinearizationKey
galoisKeysvariadic of

Results:

ResultDescription
evaluationKeySet

lattigo.rlwe.new_key_generator (heir::lattigo::RLWENewKeyGeneratorOp)

Generates a new RLWE key generator

Syntax:

operation ::= `lattigo.rlwe.new_key_generator` operands attr-dict `:` functional-type(operands, results)

This operation generates a new RLWE key generator

Operands:

OperandDescription
params

Results:

ResultDescription
keyGenerator

7.7 - LWE

LWE attributes

ApplicationDataAttr

Syntax:

#lwe.application_data<
  mlir::Type,   # message_type
  Attribute   # overflow
>

An attribute describing the semantics of the underlying application data.

The messageType parameter is used to describe the type and bits of the original application data, e.g. i1, i32, f32. This type is later mapped into the plaintext space of an FHE scheme by embedding, scaling, or other techniques.

This attribute also contains information about the overflow semantics of the data in the application. By default, we assume that the application program was written so that the overflow is not expected and the overflow attribute can can be no_overflow. For LWE-based CGGI ciphertexts, the overflow attribute will usually be preserve_overflow, since messages will overflow into padding bits.

Parameters:

ParameterC++ typeDescription
message_typemlir::Type
overflowAttribute

BitFieldEncodingAttr

An attribute describing encoded LWE plaintexts using bit fields.

Syntax:

#lwe.bit_field_encoding<
  unsigned,   # cleartext_start
  unsigned   # cleartext_bitwidth
>

A bit field encoding of an integer describes which contiguous region of bits a small integer occupies within a larger integer.

The data describing the encoding consists of the starting bit positions of the cleartext bit field and its width, where the LSB is bit 0 and the MSB is bit bit_width-1. So the above example would have starting bit 30 and width 3. The bits not specified for the message have semantics defined by the scheme or lowering.

Note that this encoding does not specify the underlying bit width of the plaintext space. This is left for lowerings to decide.

The presence of this attribute as the encoding attribute of a tensor indicates that the tensor is an LWE ciphertext.

Example (CGGI):

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
!plaintext = !lwe.lwe_plaintext<encoding = #encoding>

%0 = arith.constant 4 : i3
%1 = lwe.encode %0 { encoding = #encoding }: i3 to !plaintext

The above represents an LWE plaintext encoding the 3-bit cleartext 4 as an LWE ciphertext in a 32-bit integer, with a single bit of padding at the MSB. This corresponds to the following, where 0 denotes a 0 bit, b denotes a bit of the cleartext, n denotes a bit reserved for noise, and | is a visual aid to show where the bit fields begin and end.

   0|bbb|nn...n
MSB^          ^LSB

Example (BGV):

Note: BGV uses the RLWE encodings, but they have the same bit-field encoding attributes as here. So this example serves mainly to show how this attribute can be used to specify storing bits in the LSB of a plaintext.

#encoding = #lwe.bit_field_encoding<cleartext_start=4, cleartext_bitwidth=4>
!plaintext = !lwe.lwe_plaintext<encoding = #encoding>

%0 = arith.constant 9 : i4
%1 = lwe.encode %0 { encoding = #encoding }: i4 to !plaintext

The above represents an LWE plaintext encoding a 4-bit cleartext as an LWE ciphertext in the least-significant bits of a larger integer. This corresponds to the following.

   nn...n|bbbb
MSB^         ^LSB

Parameters:

ParameterC++ typeDescription
cleartext_startunsigned
cleartext_bitwidthunsigned

CiphertextSpaceAttr

Syntax:

#lwe.ciphertext_space<
  ::mlir::heir::polynomial::RingAttr,   # ring
  ::mlir::heir::lwe::LweEncryptionType,   # encryption_type
  unsigned   # size
>

An attribute describing the ciphertext space and the transformation from plaintext space to ciphertext space of an FHE scheme.

The ciphertext space information includes the ring attribute, describing the space that the ciphertext elements belong to. The ring attribute contains a coefficient type attribute that describes the semantics of the coefficient. For example, a ring modulo $1 + x^1024$ with coefficients modulo $q = 298374$ will be described as

!ideal = !polynomial.int_polynomial<1 + x**1024>
!cmod = !mod_arith.mod_arith<modulus=298374 : i64>
#ring = #polynomial.ring<coefficientType = !cmod, modulus = !ideal>
#ciphertext_space = #lwe.ciphertext_space<ring = #ring, encryption_type = #encryption_type>

Ciphertexts using an RNS representation for $q$ will use an RNS type in their ring’s coefficient type attribute.

// TODO(#1085): Validate syntax of polynomial ring after coefficientType changes.

!ideal = !polynomial.int_polynomial<1 + x**1024>
!limb1 = !mod_arith.mod_arith<modulus=2251799814045697 : i64>
!limb2 = !mod_arith.mod_arith<modulus=65537 : i64>
#rns_mod = !rns.rns<!limb1, !limb2>
#ring = #polynomial.ring<coefficientType = #rns_mod, modulus = #ideal>
#ciphertext_space = #lwe.ciphertext_space<ring = #ring, encryption_type = #encryption_type>

Scalar LWE ciphertexts (like those used in CGGI) use an ideal polynomial of degree 1, $x$. CGGI ciphertexts will typically use a power of two modulus and may use a native integer type for its coefficient modulus.

!ideal = !polynomial.int_polynomial<1 + x**1024>
#ring = #polynomial.ring<coefficientType = i32, modulus = #ideal>
#ciphertext_space = #lwe.ciphertext_space<ring = #ring, encryption_type = #encryption_type>

The ciphertext encoding info is used to describe the way the plaintext data is encoded into the ciphertext (in the MSB, LSB, or mixed).

The size parameter is used to describe the number of polynomials comprising the ciphertext. This is typically 2 for RLWE ciphertexts that are made up of an $(a, b)$ pair and greater than 2 for LWE instances. For example, after an RLWE multiplication of two size 2 ciphertexts, the ciphertext’s size will be 3.

Parameters:

ParameterC++ typeDescription
ring::mlir::heir::polynomial::RingAttr
encryption_type::mlir::heir::lwe::LweEncryptionType
sizeunsigned

CoefficientEncodingAttr

An encoding of cleartexts directly as coefficients.

Syntax:

#lwe.coefficient_encoding<
  unsigned   # scaling_factor
>

A coefficient encoding of a list of integers asserts that the coefficients of the polynomials contain the integers, with the same semantics as constant_coefficient_encoding for per-coefficient encodings.

A scaling_factor is optionally applied on the scalar when converting from a rounded floating point to an integer.

Example:

#coeff_encoding = #lwe.coefficient_encoding<scaling_factor=10000>

Parameters:

ParameterC++ typeDescription
scaling_factorunsigned

ConstantCoefficientEncodingAttr

An encoding of a scalar in the constant coefficient

Syntax:

#lwe.constant_coefficient_encoding<
  unsigned   # scaling_factor
>

An encoding of a single scalar into the constant coefficient of the plaintext.

All other coefficients of the plaintext are set to be zero. This encoding is used to encode scalar LWE ciphertexts where the plaintext space is viewed as a polynomial ring modulo x.

The scalar is first multiplied by the scaling_factor and then rounded to the nearest integer before encoding into the plaintext coefficient.

Example:

#coeff_encoding = #lwe.constant_coefficient_encoding<scaling_factor=10000>

Parameters:

ParameterC++ typeDescription
scaling_factorunsigned

FullCRTPackingEncodingAttr

An encoding of cleartexts via CRT slots.

Syntax:

#lwe.full_crt_packing_encoding<
  unsigned   # scaling_factor
>

This encoding maps a list of integers via the Chinese Remainder Theorem (CRT) into the plaintext space.

Given a ring with irreducible ideal polynomial f(x) and coefficient modulus q, f(x) can be decomposed modulo q into a direct product of lower-degree polynomials. This allows full SIMD-style homomorphic operations across the slots formed from each factor.

This attribute can only be used in the context of on full CRT packing, where the polynomial f(x) splits completely (into linear factors) and the number of slots equals the degree of f(x). This happens when q is prime and q = 1 mod n.

A scaling_factor is optionally applied on the scalar when converting from a rounded floating point to an integer.

Example:

#coeff_encoding = #lwe.full_crt_packing_encoding<scaling_factor=10000>

Parameters:

ParameterC++ typeDescription
scaling_factorunsigned

InverseCanonicalEncodingAttr

An encoding of cleartexts via the inverse canonical embedding.

Syntax:

#lwe.inverse_canonical_encoding<
  unsigned   # scaling_factor
>

Let $n$ be the degree of the polynomials in the plaintext space. An “inverse_canonical_encoding” of a list of real or complex values $v_1, \dots, v_{n/2}$ is (almost) the inverse of the following decoding map.

Define a map $\tau_N$ that maps a polynomial $p \in \mathbb{Z}[x] / (x^N + 1) \to \mathbb{C}^{N/2}$ by evaluating it at the following $N/2$ points, where $\omega = e^{2 \pi i / 2N}$ is the primitive $2N$th root of unity:

[ \omega, \omega^3, \omega^5, \dots, \omega^{N-1} ]

Then the complete decoding operation is $\textup{Decode}(p) = (1/\Delta)\tau_N(p)$, where $\Delta$ is a scaling parameter and $\tau_N$ is the truncated canonical embedding above. The encoding operation is the inverse of the decoding operation, with some caveats explained below.

The map $\tau_N$ is derived from the so-called canonical embedding $\tau$, though in the standard canonical embedding, we evaluate at all odd powers of the root of unity, $\omega, \omega^3, \dots, \omega^{2N-1}$. For polynomials in the slightly larger space $\mathbb{R}[x] / (x^N + 1)$, the image of the canonical embedding is the subspace $H \subset \mathbb{C}^N$ defined by tuples $(z_1, \dots, z_N)$ such that $\overline{z_i} = \overline{z_{N-i+1}}$. Note that this property holds because polynomial evaluation commutes with complex conjugates, and the second half of the roots of unity evaluate are complex conjugates of the first half. The converse, that any such tuple with complex conjugate symmetry has an inverse under $\tau$ with all real coefficients, makes $\tau$ is a bijection onto $H$. $\tau$ and its inverse are explicitly computable as discrete Fourier Transforms.

Because of the symmetry in canonical embedding for real polynomials, inputs to this encoding can be represented as a list of $N/2$ complex points, with the extra symmetric structure left implicit. $\tau_N$ and its inverse can also be explicitly computed without need to expand the vectors to length $N$.

The rounding step is required to invert the decoding because, while cleartexts must be (implicitly) in the subspace $H$, they need not be the output of $\tau_N$ for an integer polynomial. The rounding step ensures we can use integer polynomial plaintexts for the FHE operations. There are multiple rounding mechanisms, and this attribute does not specify which is used, because in theory two ciphertexts that have used different roundings are still compatible, though they may have different noise growth patterns.

The scaling parameter $\Delta$ is specified by the scaling_factor, which are applied coefficient-wise using the same semantics as the constant_coefficient_encoding.

A typical flow for the CKKS scheme using this encoding would be to apply an inverse FFT operation to invert the canonical embedding to be a polynomial with real coefficients, then encrypt scale the resulting polynomial’s coefficients according to the scaling parameters, then round to get integer coefficients.

Example:

#canonical_encoding = #lwe.inverse_canonical_encoding<scaling_factor=10000>

Parameters:

ParameterC++ typeDescription
scaling_factorunsigned

KeyAttr

Syntax:

#lwe.key<
  int   # slot_index
>

An attribute describing the key with which the message is currently encrypted.

The key attribute describes the key with which the message is currently encrypted and decryption can be performed. For example, if the decryption of a ciphertext $c = (c_0(x), c_1(x))$ is performed by computing the inner product $(c_0(x), c_1(x)) \cdot (1, s(x))$ then the key is $(1, s(x))$.

The slot_index describes the key after using a Galois automorphism to rotate the plaintext slots by slot_index. This will correspond to an action $\phi_k: x \rightarrow x^k$ for some k that depends on the structure of the Galois group for the chosen scheme parameters. The corresponding key will have a new basis $(1, s(x^(k)))$.

Parameters:

ParameterC++ typeDescription
slot_indexint

LWEParamsAttr

Syntax:

#lwe.lwe_params<
  IntegerAttr,   # cmod
  unsigned   # dimension
>

Parameters:

ParameterC++ typeDescription
cmodIntegerAttr
dimensionunsigned

ModulusChainAttr

Syntax:

#lwe.modulus_chain<
  ::llvm::ArrayRef<mlir::IntegerAttr>,   # elements
  int   # current
>

An attribute describing the elements of the modulus chain of an RLWE scheme.

Parameters:

ParameterC++ typeDescription
elements::llvm::ArrayRef<mlir::IntegerAttr>
currentint

NoOverflowAttr

An attribute informing that application data never overflows.

Syntax: #lwe.no_overflow

This attribute informs lowerings that a program is written so that the message data will never overflow beyond the message type.

// FIXME: Have a separate WraparoundOverflow, which lowers the same as NoOverflow?

PlaintextSpaceAttr

Syntax:

#lwe.plaintext_space<
  ::mlir::heir::polynomial::RingAttr,   # ring
  Attribute   # encoding
>

An attribute describing the plaintext space and the transformation from application data to plaintext space of an FHE scheme.

The plaintext space information is the ring structure, which contains the plaintext modulus $t$, which may be a power of two in the case of CGGI ciphertexts, or a prime power for RLWE. LWE ciphertexts use the ideal polynomial of degree 1 $x$. The plaintext modulus used in LWE-based CGGI plaintexts describes the full message space $\mathbb{Z}_p$ including the padding bits. The application data info attribute describes the space $\mathbb{Z}_p’$ where $p’ < p$ that the underlying message belongs to.

For RLWE schemes, this will include the type of encoding of application data integers to a plaintext space Z_p[X]/X^N + 1. This may be a constant coefficient encoding, CRT-based packing for SIMD semantics, or other slot packing. When using full CRT packing, the ring must split into linear factors. The CKKS scheme will also include attributes describing the complex encoding, including the scaling factor, which will change after multiplication and rescaling.

Parameters:

ParameterC++ typeDescription
ring::mlir::heir::polynomial::RingAttr
encodingAttributeAn encoding of a scalar in the constant coefficient or An encoding of cleartexts directly as coefficients. or An encoding of cleartexts via the inverse canonical embedding. or An encoding of cleartexts via CRT slots.

PreserveOverflowAttr

An attribute informing that application data overflows in the message type.

Syntax: #lwe.preserve_overflow

This attribute informs lowerings that a program is written so that the message data may overflow beyond the message type.

RLWEParamsAttr

Syntax:

#lwe.rlwe_params<
  unsigned,   # dimension
  ::mlir::heir::polynomial::RingAttr   # ring
>

An attribute describing classical RLWE parameters:

  • dimension: the number of polynomials used in an RLWE sample, analogous to LWEParams.dimension.
  • ring: the polynomial ring to use.

Parameters:

ParameterC++ typeDescription
dimensionunsigned
ring::mlir::heir::polynomial::RingAttr

UnspecifiedBitFieldEncodingAttr

An attribute describing unspecified bit field encodings.

Syntax:

#lwe.unspecified_bit_field_encoding<
  unsigned   # cleartext_bitwidth
>

See LWE_BitFieldEncoding for a description of bit field encodings.

This attribute describes an unspecified bit field encoding; this is where the starting bit position of the cleartext bit field is unspecified, but its width is fixed. A noise growth analysis should be performed to determine the optimal amount of bits needed for noise and padding to specify the bit field encodings starting bit position.

Example:

#lwe_encoding = #lwe.unspecified_bit_field_encoding<cleartext_bitwidth=3>
%lwe_ciphertext = arith.constant <[1,2,3,4]> : tensor<4xi32, #lwe_encoding>

Parameters:

ParameterC++ typeDescription
cleartext_bitwidthunsigned

LWE types

LWECiphertextType

A type for LWE ciphertexts

Syntax:

!lwe.lwe_ciphertext<
  ::mlir::Attribute,   # encoding
  LWEParamsAttr   # lwe_params
>

A type for LWE ciphertexts.

This type keeps track of the plaintext integer encoding for the LWE Ciphertext to ensure proper decoding after decryption. It also keeps track of the ring where the LWE ciphertext is defined, which provides information on the ciphertext shape and the ring operations used in LWE operations.

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
lwe_paramsLWEParamsAttr

LWEPlaintextType

A type for LWE plaintexts

Syntax:

!lwe.lwe_plaintext<
  ::mlir::Attribute   # encoding
>

A type for LWE plaintexts.

This type keeps track of the plaintext integer encoding for the LWE plaintext before it is encrypted.

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute

NewLWECiphertextType

A ciphertext type

Syntax:

!lwe.new_lwe_ciphertext<
  ApplicationDataAttr,   # application_data
  PlaintextSpaceAttr,   # plaintext_space
  CiphertextSpaceAttr,   # ciphertext_space
  KeyAttr,   # key
  ModulusChainAttr   # modulus_chain
>

An LWE ciphertext will always contain the application data, plaintext space, ciphertext space, and key information.

A modulus chain is optionally specified for parameter choices in RLWE schemes that use more than one of modulus. When no modulus chain is specified, the ciphertext modulus is always the ciphertext ring’s coefficient modulus.

Parameters:

ParameterC++ typeDescription
application_dataApplicationDataAttr
plaintext_spacePlaintextSpaceAttr
ciphertext_spaceCiphertextSpaceAttr
keyKeyAttr
modulus_chainModulusChainAttr

NewLWEPlaintextType

A plaintext type

Syntax:

!lwe.new_lwe_plaintext<
  ApplicationDataAttr,   # application_data
  PlaintextSpaceAttr   # plaintext_space
>

Parameters:

ParameterC++ typeDescription
application_dataApplicationDataAttr
plaintext_spacePlaintextSpaceAttr

NewLWEPublicKeyType

A public key for LWE

Syntax:

!lwe.new_lwe_public_key<
  KeyAttr,   # key
  ::mlir::heir::polynomial::RingAttr   # ring
>

Parameters:

ParameterC++ typeDescription
keyKeyAttr
ring::mlir::heir::polynomial::RingAttr

NewLWESecretKeyType

A secret key for LWE

Syntax:

!lwe.new_lwe_secret_key<
  KeyAttr,   # key
  ::mlir::heir::polynomial::RingAttr   # ring
>

Parameters:

ParameterC++ typeDescription
keyKeyAttr
ring::mlir::heir::polynomial::RingAttr

RLWECiphertextType

A type for RLWE ciphertexts

Syntax:

!lwe.rlwe_ciphertext<
  ::mlir::Attribute,   # encoding
  RLWEParamsAttr,   # rlwe_params
  Type   # underlying_type
>

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
rlwe_paramsRLWEParamsAttr
underlying_typeType

RLWEPlaintextType

A type for RLWE plaintexts

Syntax:

!lwe.rlwe_plaintext<
  ::mlir::Attribute,   # encoding
  ::mlir::heir::polynomial::RingAttr,   # ring
  Type   # underlying_type
>

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
ring::mlir::heir::polynomial::RingAttr
underlying_typeType

RLWEPublicKeyType

A public key for RLWE

Syntax:

!lwe.rlwe_public_key<
  RLWEParamsAttr   # rlwe_params
>

Parameters:

ParameterC++ typeDescription
rlwe_paramsRLWEParamsAttr

RLWESecretKeyType

A secret key for RLWE

Syntax:

!lwe.rlwe_secret_key<
  RLWEParamsAttr   # rlwe_params
>

Parameters:

ParameterC++ typeDescription
rlwe_paramsRLWEParamsAttr

LWE ops

lwe.add (heir::lwe::AddOp)

Add two LWE ciphertexts

Syntax:

operation ::= `lwe.add` operands attr-dict `:` type($output)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA type for LWE ciphertexts
rhsA type for LWE ciphertexts

Results:

ResultDescription
outputA type for LWE ciphertexts

lwe.encode (heir::lwe::EncodeOp)

Encode an integer to yield an LWE plaintext

Syntax:

operation ::= `lwe.encode` $input attr-dict `:` qualified(type($input)) `to` qualified(type($output))

Encode an integer to yield an LWE plaintext.

This op uses a an encoding attribute to encode the bits of the integer into an LWE plaintext value that can then be encrypted.

Examples:

%Y = lwe.encode %value {encoding = #enc}: i1 to !lwe.lwe_plaintext<encoding = #enc>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn attribute describing encoded LWE plaintexts using bit fields. or An attribute describing unspecified bit field encodings.

Operands:

OperandDescription
inputsignless-integer-like or floating-point-like

Results:

ResultDescription
outputA type for LWE plaintexts

lwe.mul_scalar (heir::lwe::MulScalarOp)

Multiply an LWE ciphertext by a scalar

Syntax:

operation ::= `lwe.mul_scalar` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextciphertext-like
scalarinteger

Results:

ResultDescription
outputciphertext-like

lwe.radd (heir::lwe::RAddOp)

Add two RLWE ciphertexts

Syntax:

operation ::= `lwe.radd` operands attr-dict `:` type($output)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

lwe.rlwe_decode (heir::lwe::RLWEDecodeOp)

Decode an RLWE plaintext to an underlying type

Syntax:

operation ::= `lwe.rlwe_decode` $input attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn encoding of a scalar in the constant coefficient or An encoding of cleartexts directly as coefficients. or An encoding of cleartexts via the inverse canonical embedding. or An encoding of cleartexts via CRT slots.
ring::mlir::heir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputA plaintext type

Results:

ResultDescription
outputsignless-integer-like or floating-point-like

lwe.rlwe_decrypt (heir::lwe::RLWEDecryptOp)

Decrypt an RLWE ciphertext to a RLWE plaintext

Syntax:

operation ::= `lwe.rlwe_decrypt` operands attr-dict `:`  functional-type(operands, results)

Decrypt an RLWE ciphertext to yield a RLWE plaintext

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA ciphertext type
secret_keyA secret key for LWE

Results:

ResultDescription
outputA plaintext type

lwe.rlwe_encode (heir::lwe::RLWEEncodeOp)

Encode an integer to yield an RLWE plaintext

Syntax:

operation ::= `lwe.rlwe_encode` $input attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Encode an integer to yield an RLWE plaintext.

This op uses a an encoding attribute to encode the bits of the integer into an RLWE plaintext value that can then be encrypted. CKKS cleartext inputs may be floating points, and a scaling factor described by the encoding will be applied.

Examples:

%Y = lwe.rlwe_encode %value {encoding = #enc, ring = #ring}: i1 to !lwe.rlwe_plaintext<encoding = #enc, ring = #ring>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn encoding of a scalar in the constant coefficient or An encoding of cleartexts directly as coefficients. or An encoding of cleartexts via the inverse canonical embedding. or An encoding of cleartexts via CRT slots.
ring::mlir::heir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputsignless-integer-like or floating-point-like

Results:

ResultDescription
outputA plaintext type

lwe.rlwe_encrypt (heir::lwe::RLWEEncryptOp)

Encrypt an RLWE plaintext to a RLWE ciphertext

Syntax:

operation ::= `lwe.rlwe_encrypt` operands attr-dict `:`  functional-type(operands, results)

Encrypt an RLWE plaintext to yield a RLWE ciphertext.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA plaintext type
keyA secret key for LWE or A public key for LWE

Results:

ResultDescription
outputA ciphertext type

lwe.rmul (heir::lwe::RMulOp)

Multiplies two RLWE ciphertexts

Syntax:

operation ::= `lwe.rmul` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, InferTypeOpAdaptor, SameTypeOperands, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

lwe.rnegate (heir::lwe::RNegateOp)

Negate a RLWE ciphertexts

Syntax:

operation ::= `lwe.rnegate` operands attr-dict `:` type($output)

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputnew-lwe-ciphertext-like

Results:

ResultDescription
outputnew-lwe-ciphertext-like

lwe.rsub (heir::lwe::RSubOp)

Subtract two RLWE ciphertexts

Syntax:

operation ::= `lwe.rsub` operands attr-dict `:` type($output)

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

lwe.trivial_encrypt (heir::lwe::TrivialEncryptOp)

Create a trivial encryption of a plaintext.

Syntax:

operation ::= `lwe.trivial_encrypt` operands attr-dict `:`  qualified(type(operands)) `to` qualified(type(results))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
params::mlir::heir::lwe::LWEParamsAttr

Operands:

OperandDescription
inputA type for LWE plaintexts

Results:

ResultDescription
outputA type for LWE ciphertexts

lwe.reinterpret_underlying_type (heir::lwe::ReinterpretUnderlyingTypeOp)

A placeholder cast from one ciphertext type to another

Syntax:

operation ::= `lwe.reinterpret_underlying_type` $input attr-dict `:` qualified(type($input)) `to` qualified(type($output))

The cast op is thus used to translate underlying_type between ciphertexts in particular situations , such as when lowering to an API that does not keep track of types for you.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA ciphertext type

Results:

ResultDescription
outputA ciphertext type

7.8 - Mgmt

Mgmt attributes

MgmtAttr

Container attribute for all mgmt parameter

Syntax:

#mgmt.mgmt<
  int,   # level
  int   # dimension
>

This attribute is used to store all mgmt parameters.

The attribute is a struct with the following fields:

  • level : the level of the ciphertext, from L to 0
  • dimension : the dimension of the ciphertext, defaults to 2

Internally, this attribute is used by secret-to- for determining the level and dimension of the ciphertext.

It should be populated by –secret-with-mgmt- before going through the secret-to- pass.

Example:

#mgmt = #mgmt.mgmt<level = 1> // dimension defaults to 2
#mgmt1 = #mgmt.mgmt<level = 1, dimension = 3>
%0 = secret.generic ins(%arg0, %arg1 : !secret.secret<i16>) attrs = {mgmt.mgmt = #mgmt} {
   ...
} -> !secret.secret<i16>

Parameters:

ParameterC++ typeDescription
levelint
dimensionint

Mgmt ops

mgmt.modreduce (heir::mgmt::ModReduceOp)

Modulus switch the input ciphertext down by one limb (RNS assumed)

Syntax:

operation ::= `mgmt.modreduce` operands attr-dict `:` type($output)

This is scheme-agonistic operation that implies modulus switching/rescaling by one limb.

Input ciphertext is assumed to be in RNS form when further lowered.

When further lowered, it could be lowered to bgv.modulus_switch or ckks.rescale depending on the scheme.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputany type

Results:

ResultDescription
outputany type

mgmt.relinearize (heir::mgmt::RelinearizeOp)

Relinearize the input ciphertext to be linear

Syntax:

operation ::= `mgmt.relinearize` operands attr-dict `:` type($output)

This is scheme-agonistic operation that implies relinearization of the input ciphertext to be linear (i.e. returns to dimension 2).

This is used solely by multiplication. For rotation, currently HEIR assumes relinearization is done internally and does not have a separate scheme-specific operation for it.

This accepts a ciphertext with dimension > 2 and returns a ciphertext with dimension 2. Note that the semantic includes the relinearization of higher dimension input like input with dimension 4 or higher, which when materialized should require multiple relinearization keys.

When further lowered, it could be lowered to bgv.relinearize or ckks.relinearize depending on the scheme.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputany type

Results:

ResultDescription
outputany type

7.9 - ModArith

ModArith attributes

ModArithAttr

a typed mod_arith attribute

Syntax:

#mod_arith.int<
  ::mlir::heir::mod_arith::ModArithType,   # type
  mlir::IntegerAttr   # value
>

Example:

#attr = 123:i32
#attr_verbose = #mod_arith.int<123:i32>

Parameters:

ParameterC++ typeDescription
type::mlir::heir::mod_arith::ModArithType
valuemlir::IntegerAttr

ModArith types

ModArithType

Integer type with modular arithmetic

Syntax:

!mod_arith.int<
  ::mlir::IntegerAttr   # modulus
>

mod_arith.int<p> represents an element of the ring of integers modulo $p$. The modulus attribute is the ring modulus, and mod_arith operations lower to arith operations that produce results in the range [0, modulus), often called the canonical representative.

modulus is specified with an integer type suffix, for example, mod_arith.int<65537 : i32>. This corresponds to the storage type for the modulus, and is i64 by default.

It is required that the underlying integer type should be larger than twice the modulus (have one extra bit of storage space) to avoid signedness issues. For example, when modulus == 2 ** 32 - 1, the underlying type for the modulus should be at least i33, though i64 is a natural choice.

Passes may allow intermediate values that do not always produce a canonical representative in [0, modulus). For example, if the machine storage type is i64, but the modulus fits within an i32, a lowering could allow intermediate arithmetic values to grow to as large as an i64 before reducing them. However, all passes must ensure that values used outside the local scope (e.g., function return values or arguments to calls to linked functions) are appropriately reduced to the canonical representative. modulus is the modulus the arithmetic working with.

Examples:

!Zp1 = !mod_arith.int<7> // implicitly being i64
!Zp2 = !mod_arith.int<65537 : i32>
!Zp3 = !mod_arith.int<536903681 : i64>

Parameters:

ParameterC++ typeDescription
modulus::mlir::IntegerAttr

ModArith ops

mod_arith.add (heir::mod_arith::AddOp)

Modular addition operation

Syntax:

operation ::= `mod_arith.add` operands attr-dict `:` type($output)

Computes modular addition.

Unless otherwise specified, the operation assumes both inputs are canonical representatives and guarantees the output being canonical representative.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsmod_arith-like
rhsmod_arith-like

Results:

ResultDescription
outputmod_arith-like

mod_arith.barrett_reduce (heir::mod_arith::BarrettReduceOp)

Compute the first step of the Barrett reduction.

Syntax:

operation ::= `mod_arith.barrett_reduce` operands attr-dict `:` qualified(type($input))

Let $q$ denote a statically known modulus and $b = 4^{w}$, where $w$ is the smallest bit-width that contains the range $[0, q)$. The Barrett reduce operation computes barret_reduce x = x - floor(x * floor(b / q) / b) * q.

Given $0 <= x < q^2$, then this will compute $(x \mod q)$ or $(x \mod q) + q$.

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
inputsignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

mod_arith.constant (heir::mod_arith::ConstantOp)

Define a constant value via an attribute.

Example:

%0 = mod_arith.constant 123 : !mod_arith.int<65537:i32>

Traits: AlwaysSpeculatableImplTrait, InferTypeOpAdaptor

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
value::mlir::heir::mod_arith::ModArithAttra typed mod_arith attribute

Results:

ResultDescription
outputInteger type with modular arithmetic

mod_arith.encapsulate (heir::mod_arith::EncapsulateOp)

Encapsulate an integer into a mod_arith type

Syntax:

operation ::= `mod_arith.encapsulate` operands attr-dict `:` type($input) `->` type($output)

mod_arith.encapsulate converts the integer to be of mod_arith type.

Examples:

mod_arith.encapsulate %c0 : i32 -> mod_arith.int<65537 : i32>
mod_arith.encapsulate %c1 : i64 -> mod_arith.int<65537>

Traits: AlwaysSpeculatableImplTrait, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsignless-integer-like

Results:

ResultDescription
outputmod_arith-like

mod_arith.extract (heir::mod_arith::ExtractOp)

Extract the integer stored inside mod_arith type

Syntax:

operation ::= `mod_arith.extract` operands attr-dict `:` type($input) `->` type($output)

mod_arith.extract extracts the integer inside the mod_arith type.

It is required that the bitwidth of the output integer type is the same as that of the storage type of the input mod_arith type.

Examples:

%m0 = mod_arith.encapsulate %c0 : i32 -> mod_arith.int<65537 : i32>
%m1 = mod_arith.encapsulate %c1 : i64 -> mod_arith.int<65537>
%c2 = mod_arith.extract %m0 : mod_arith.int<65537 : i32> -> i32
%c3 = mod_arith.extract %m1 : mod_arith.int<65537> -> i64

Traits: AlwaysSpeculatableImplTrait, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputmod_arith-like

Results:

ResultDescription
outputsignless-integer-like

mod_arith.mac (heir::mod_arith::MacOp)

Modular multiplication-and-accumulation operation

Syntax:

operation ::= `mod_arith.mac` operands attr-dict `:` type($output)

mod_arith.mac x, y, z computes $(x * y) + z$

Unless otherwise specified, the operation assumes all inputs are canonical representatives and guarantees the output being canonical representative.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsmod_arith-like
rhsmod_arith-like
accmod_arith-like

Results:

ResultDescription
outputmod_arith-like

mod_arith.mod_switch (heir::mod_arith::ModSwitchOp)

Change the modulus of a mod_arith

Syntax:

operation ::= `mod_arith.mod_switch` $input attr-dict `:` type($input) `to` type($output)

“mod_switch” operation to change the modulus of a mod_arith type to a bigger space.

Examples:

`mod_arith.mod_switch %c0 : mod_arith.int<65537 : i32> to mod_arith.int<65539 : i32>`

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultShape

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputInteger type with modular arithmetic

Results:

ResultDescription
outputInteger type with modular arithmetic

mod_arith.mul (heir::mod_arith::MulOp)

Modular multiplication operation

Syntax:

operation ::= `mod_arith.mul` operands attr-dict `:` type($output)

Computes modular multiplication.

Unless otherwise specified, the operation assumes both inputs are canonical representatives and guarantees the output being canonical representative.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsmod_arith-like
rhsmod_arith-like

Results:

ResultDescription
outputmod_arith-like

mod_arith.reduce (heir::mod_arith::ReduceOp)

Reduce the mod arith type to its canonical representative

Syntax:

operation ::= `mod_arith.reduce` operands attr-dict `:` type($output)

mod_arith.reduce x produces $y$, the canonical representative in $[0, q)$ such that $x \equiv y \mod q$.

Examples:

%c0 = arith.constant 65538 : i32
%m0 = mod_arith.encapsulate %c0 : i32 -> mod_arith.int<65537 : i32>
// mod_arith.extract %m0 produces 65538
%m1 = mod_arith.reduce %m0 : mod_arith.int<65537: i32>
// mod_arith.extract %m1 produces 1

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputmod_arith-like

Results:

ResultDescription
outputmod_arith-like

mod_arith.subifge (heir::mod_arith::SubIfGEOp)

Compute (x >= y) ? x - y : x.

Syntax:

operation ::= `mod_arith.subifge` operands attr-dict `:` qualified(type($output))

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

mod_arith.sub (heir::mod_arith::SubOp)

Modular subtraction operation

Syntax:

operation ::= `mod_arith.sub` operands attr-dict `:` type($output)

Computes modular subtraction.

Unless otherwise specified, the operation assumes both inputs are canonical representatives and guarantees the output being canonical representative.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsmod_arith-like
rhsmod_arith-like

Results:

ResultDescription
outputmod_arith-like

7.10 - Openfhe

‘openfhe’ Dialect

The openfhe dialect is an exit dialect for generating c++ code against the OpenFHE library API.

See https://github.com/openfheorg/openfhe-development

Openfhe types

CCParamsType

The CCParams required to create CryptoContext.

Syntax: !openfhe.cc_params

CryptoContextType

The CryptoContext required to perform homomorphic operations in OpenFHE.

Syntax: !openfhe.crypto_context

EvalKeyType

The evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Syntax: !openfhe.eval_key

PrivateKeyType

The private key required to decrypt a ciphertext in OpenFHE.

Syntax: !openfhe.private_key

PublicKeyType

The public key required to encrypt plaintext in OpenFHE.

Syntax: !openfhe.public_key

Openfhe ops

openfhe.add (heir::openfhe::AddOp)

OpenFHE add operation of two ciphertexts.

Syntax:

operation ::= `openfhe.add` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.add_plain (heir::openfhe::AddPlainOp)

OpenFHE add operation of a ciphertext and a plaintext.

Syntax:

operation ::= `openfhe.add_plain` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
plaintextA plaintext type

Results:

ResultDescription
outputA ciphertext type

openfhe.automorph (heir::openfhe::AutomorphOp)

Syntax:

operation ::= `openfhe.automorph` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
evalKeyThe evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Results:

ResultDescription
outputA ciphertext type

openfhe.bootstrap (heir::openfhe::BootstrapOp)

OpenFHE bootstrap operation of a ciphertext. (For CKKS)

Syntax:

operation ::= `openfhe.bootstrap` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.decrypt (heir::openfhe::DecryptOp)

Syntax:

operation ::= `openfhe.decrypt` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

Results:

ResultDescription
plaintextA plaintext type

openfhe.encrypt (heir::openfhe::EncryptOp)

Syntax:

operation ::= `openfhe.encrypt` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
plaintextA plaintext type
publicKeyThe public key required to encrypt plaintext in OpenFHE.

Results:

ResultDescription
ciphertextA ciphertext type

openfhe.gen_bootstrapkey (heir::openfhe::GenBootstrapKeyOp)

Syntax:

operation ::= `openfhe.gen_bootstrapkey` operands attr-dict `:` functional-type(operands, results)

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

openfhe.gen_context (heir::openfhe::GenContextOp)

Syntax:

operation ::= `openfhe.gen_context` operands attr-dict `:` functional-type(operands, results)

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
supportFHE::mlir::BoolAttrbool attribute

Operands:

OperandDescription
paramsThe CCParams required to create CryptoContext.

Results:

ResultDescription
contextThe CryptoContext required to perform homomorphic operations in OpenFHE.

openfhe.gen_mulkey (heir::openfhe::GenMulKeyOp)

Syntax:

operation ::= `openfhe.gen_mulkey` operands attr-dict `:` functional-type(operands, results)

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

openfhe.gen_params (heir::openfhe::GenParamsOp)

Syntax:

operation ::= `openfhe.gen_params` operands attr-dict `:` functional-type(operands, results)

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
mulDepth::mlir::IntegerAttr64-bit signless integer attribute
plainMod::mlir::IntegerAttr64-bit signless integer attribute
insecure::mlir::BoolAttrbool attribute

Results:

ResultDescription
paramsThe CCParams required to create CryptoContext.

openfhe.gen_rotkey (heir::openfhe::GenRotKeyOp)

Syntax:

operation ::= `openfhe.gen_rotkey` operands attr-dict `:` functional-type(operands, results)

Attributes:

AttributeMLIR TypeDescription
indices::mlir::DenseI64ArrayAttri64 dense array attribute

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

openfhe.key_switch (heir::openfhe::KeySwitchOp)

Syntax:

operation ::= `openfhe.key_switch` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
evalKeyThe evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Results:

ResultDescription
outputA ciphertext type

openfhe.level_reduce (heir::openfhe::LevelReduceOp)

OpenFHE level_reduce operation of a ciphertext.

Syntax:

operation ::= `openfhe.level_reduce` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.make_ckks_packed_plaintext (heir::openfhe::MakeCKKSPackedPlaintextOp)

Syntax:

operation ::= `openfhe.make_ckks_packed_plaintext` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
valueranked tensor of floating-point or integer values

Results:

ResultDescription
plaintextA plaintext type

openfhe.make_packed_plaintext (heir::openfhe::MakePackedPlaintextOp)

Syntax:

operation ::= `openfhe.make_packed_plaintext` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
valueranked tensor of integer values

Results:

ResultDescription
plaintextA plaintext type

openfhe.mod_reduce (heir::openfhe::ModReduceOp)

OpenFHE mod_reduce operation of a ciphertext. (used only for BGV/CKKS)

Syntax:

operation ::= `openfhe.mod_reduce` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.mul_const (heir::openfhe::MulConstOp)

OpenFHE mul operation of a ciphertext and a constant.

Syntax:

operation ::= `openfhe.mul_const` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
constant64-bit signless integer

Results:

ResultDescription
outputA ciphertext type

openfhe.mul_no_relin (heir::openfhe::MulNoRelinOp)

OpenFHE mul operation of two ciphertexts without relinearization.

Syntax:

operation ::= `openfhe.mul_no_relin` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.mul (heir::openfhe::MulOp)

OpenFHE mul operation of two ciphertexts with relinearization.

Syntax:

operation ::= `openfhe.mul` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.mul_plain (heir::openfhe::MulPlainOp)

OpenFHE mul operation of a ciphertext and a plaintext.

Syntax:

operation ::= `openfhe.mul_plain` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type
plaintextA plaintext type

Results:

ResultDescription
outputA ciphertext type

openfhe.negate (heir::openfhe::NegateOp)

OpenFHE negate operation of a ciphertext.

Syntax:

operation ::= `openfhe.negate` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.relin (heir::openfhe::RelinOp)

OpenFHE relinearize operation of a ciphertext.

Syntax:

operation ::= `openfhe.relin` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.rot (heir::openfhe::RotOp)

Syntax:

operation ::= `openfhe.rot` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
index::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.setup_bootstrap (heir::openfhe::SetupBootstrapOp)

Syntax:

operation ::= `openfhe.setup_bootstrap` operands attr-dict `:` functional-type(operands, results)

Attributes:

AttributeMLIR TypeDescription
levelBudgetEncode::mlir::IntegerAttrAn Attribute containing a integer value
levelBudgetDecode::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.

openfhe.square (heir::openfhe::SquareOp)

OpenFHE square operation of a ciphertext.

Syntax:

operation ::= `openfhe.square` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA ciphertext type

Results:

ResultDescription
outputA ciphertext type

openfhe.sub (heir::openfhe::SubOp)

OpenFHE sub operation of two ciphertexts.

Syntax:

operation ::= `openfhe.sub` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA ciphertext type
rhsA ciphertext type

Results:

ResultDescription
outputA ciphertext type

7.11 - Polynomial

Polynomial attributes

FloatPolynomialAttr

an attribute containing a single-variable polynomial with double precision floating point coefficients

A polynomial attribute represents a single-variable polynomial with double precision floating point coefficients.

The polynomial must be expressed as a list of monomial terms, with addition or subtraction between them. The choice of variable name is arbitrary, but must be consistent across all the monomials used to define a single attribute. The order of monomial terms is arbitrary, each monomial degree must occur at most once.

Example:

#poly = #polynomial.float_polynomial<0.5 x**7 + 1.5>

Parameters:

ParameterC++ typeDescription
polynomialFloatPolynomial

IntPolynomialAttr

an attribute containing a single-variable polynomial with integer coefficients

A polynomial attribute represents a single-variable polynomial with integer coefficients, which is used to define the modulus of a RingAttr, as well as to define constants and perform constant folding for polynomial ops.

The polynomial must be expressed as a list of monomial terms, with addition or subtraction between them. The choice of variable name is arbitrary, but must be consistent across all the monomials used to define a single attribute. The order of monomial terms is arbitrary, each monomial degree must occur at most once.

Example:

#poly = #polynomial.int_polynomial<x**1024 + 1>

Parameters:

ParameterC++ typeDescription
polynomial::mlir::heir::polynomial::IntPolynomial

PrimitiveRootAttr

an attribute containing an integer and its degree as a root of unity

Syntax:

#polynomial.primitive_root<
  ::mlir::IntegerAttr,   # value
  ::mlir::IntegerAttr   # degree
>

A primitive root attribute stores an integer root value and an integer degree, corresponding to a primitive root of unity of the given degree in an unspecified ring.

This is used as an attribute on polynomial.ntt and polynomial.intt ops to specify the root of unity used in lowering the transform.

Example:

#poly = #polynomial.primitive_root<value=123 : i32, degree : 7 index>

Parameters:

ParameterC++ typeDescription
value::mlir::IntegerAttr
degree::mlir::IntegerAttr

RingAttr

an attribute specifying a polynomial ring

Syntax:

#polynomial.ring<
  Type,   # coefficientType
  ::mlir::heir::polynomial::IntPolynomialAttr   # polynomialModulus
>

A ring describes the domain in which polynomial arithmetic occurs. The ring attribute in polynomial represents the more specific case of polynomials with a single indeterminate; whose coefficients can be represented by another MLIR type (coefficientType).

All semantics pertaining to arithmetic in the ring must be owned by the coefficient type. For example, if the polynomials are with integer coefficients taken modulo a prime $p$, then coefficientType must be a type that represents integers modulo $p$, such as mod_arith<p>.

Additionally, a polynomial ring may specify a polynomialModulus, which converts polynomial arithmetic to the analogue of modular integer arithmetic, where each polynomial is represented as its remainder when dividing by the modulus. For single-variable polynomials, a polynomial modulus is always specified via a single polynomial.

An expressive example is polynomials with i32 coefficients, whose coefficients are taken modulo 2**32 - 5, with a polynomial modulus of x**1024 - 1.

#poly_mod = #polynomial.int_polynomial<-1 + x**1024>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly_mod>

%0 = ... : polynomial.polynomial<#ring>

In this case, the value of a polynomial is always “converted” to a canonical form by applying repeated reductions by setting x**1024 = 1 and simplifying.

Parameters:

ParameterC++ typeDescription
coefficientTypeType
polynomialModulus::mlir::heir::polynomial::IntPolynomialAttr

TypedFloatPolynomialAttr

a typed float_polynomial

Syntax:

#polynomial.typed_float_polynomial<
  ::mlir::Type,   # type
  ::mlir::heir::polynomial::FloatPolynomialAttr   # value
>

Example:

!poly_ty = !polynomial.polynomial<ring=<coefficientType=f32>>
#poly = float<1.4 x**7 + 4.5> : !poly_ty
#poly_verbose = #polynomial.typed_float_polynomial<1.4 x**7 + 4.5> : !poly_ty

Parameters:

ParameterC++ typeDescription
type::mlir::Type
value::mlir::heir::polynomial::FloatPolynomialAttr

TypedIntPolynomialAttr

a typed int_polynomial

Syntax:

#polynomial.typed_int_polynomial<
  ::mlir::Type,   # type
  ::mlir::heir::polynomial::IntPolynomialAttr   # value
>

Example:

!poly_ty = !polynomial.polynomial<ring=<coefficientType=i32>>
#poly = int<1 x**7 + 4> : !poly_ty
#poly_verbose = #polynomial.typed_int_polynomial<1 x**7 + 4> : !poly_ty

Parameters:

ParameterC++ typeDescription
type::mlir::Type
value::mlir::heir::polynomial::IntPolynomialAttr

Polynomial types

PolynomialType

An element of a polynomial ring.

Syntax:

!polynomial.polynomial<
  ::mlir::heir::polynomial::RingAttr   # ring
>

A type for polynomials in a polynomial quotient ring.

Parameters:

ParameterC++ typeDescription
ring::mlir::heir::polynomial::RingAttran attribute specifying a polynomial ring

Polynomial ops

polynomial.add (heir::polynomial::AddOp)

Addition operation between polynomials.

Syntax:

operation ::= `polynomial.add` operands attr-dict `:` type($result)

Performs polynomial addition on the operands. The operands may be single polynomials or containers of identically-typed polynomials, i.e., polynomials from the same underlying ring with the same coefficient types.

This op is defined to occur in the ring defined by the ring attribute of the two operands, meaning the arithmetic is taken modulo the polynomialModulus of the ring as well as modulo any semantics defined by the coefficient type.

Example:

// add two polynomials modulo x^1024 - 1
#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%0 = polynomial.constant int<1 + x**2> : !polynomial.polynomial<#ring>
%1 = polynomial.constant int<x**5 - x + 1> : !polynomial.polynomial<#ring>
%2 = polynomial.add %0, %1 : !polynomial.polynomial<#ring>

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhspolynomial-like
rhspolynomial-like

Results:

ResultDescription
resultpolynomial-like

polynomial.constant (heir::polynomial::ConstantOp)

Define a constant polynomial via an attribute.

Example:

!int_poly_ty = !polynomial.polynomial<ring=<coefficientType=i32>>
%0 = polynomial.constant int<1 + x**2> : !int_poly_ty

!float_poly_ty = !polynomial.polynomial<ring=<coefficientType=f32>>
%1 = polynomial.constant float<0.5 + 1.3e06 x**2> : !float_poly_ty

Traits: AlwaysSpeculatableImplTrait, InferTypeOpAdaptor

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
value::mlir::Attributea typed float_polynomial or a typed int_polynomial

Results:

ResultDescription
outputAn element of a polynomial ring.

polynomial.from_tensor (heir::polynomial::FromTensorOp)

Creates a polynomial from integer coefficients stored in a tensor.

Syntax:

operation ::= `polynomial.from_tensor` $input attr-dict `:` type($input) `->` type($output)

polynomial.from_tensor creates a polynomial value from a tensor of coefficients. The input tensor must list the coefficients in degree-increasing order.

The input one-dimensional tensor may have size at most the degree of the ring’s polynomialModulus generator polynomial, with smaller dimension implying that all higher-degree terms have coefficient zero.

Example:

#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%two = arith.constant 2 : i32
%five = arith.constant 5 : i32
%coeffs = tensor.from_elements %two, %two, %five : tensor<3xi32>
%poly = polynomial.from_tensor %coeffs : tensor<3xi32> -> !polynomial.polynomial<#ring>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputranked tensor of any type values

Results:

ResultDescription
outputAn element of a polynomial ring.

polynomial.intt (heir::polynomial::INTTOp)

Computes the reverse integer Number Theoretic Transform (NTT).

Syntax:

operation ::= `polynomial.intt` $input attr-dict `:` qualified(type($input)) `->` type($output)

polynomial.intt computes the reverse integer Number Theoretic Transform (INTT) on the input tensor. This is the inverse operation of the polynomial.ntt operation.

The input tensor is interpreted as a point-value representation of the output polynomial at powers of a primitive n-th root of unity (see polynomial.ntt). The ring of the polynomial is taken from the required encoding attribute of the tensor.

The choice of primitive root may be optionally specified.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
root::mlir::heir::polynomial::PrimitiveRootAttran attribute containing an integer and its degree as a root of unity

Operands:

OperandDescription
inputranked tensor of Integer type with modular arithmetic values

Results:

ResultDescription
outputAn element of a polynomial ring.

polynomial.leading_term (heir::polynomial::LeadingTermOp)

Compute the leading term of the polynomial.

Syntax:

operation ::= `polynomial.leading_term` operands attr-dict `:` type($input) `->` `(` type($degree) `,` type($coefficient) `)`

The degree of a polynomial is the largest $k$ for which the coefficient a_k of x^k is nonzero. The leading term is the term a_k * x^k, which this op represents as a pair of results. The first is the degree k as an index, and the second is the coefficient, whose type matches the coefficient type of the polynomial’s ring attribute.

Example:

#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%0 = polynomial.constant int<1 + x**2> : !polynomial.polynomial<#ring>
%1, %2 = polynomial.leading_term %0 : !polynomial.polynomial<#ring> -> (index, i32)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputAn element of a polynomial ring.

Results:

ResultDescription
degreeindex
coefficientany type

polynomial.monic_monomial_mul (heir::polynomial::MonicMonomialMulOp)

Multiply a polynomial by a monic monomial.

Syntax:

operation ::= `polynomial.monic_monomial_mul` operands attr-dict `:` functional-type(operands, results)

Multiply a polynomial by a monic monomial, meaning a polynomial of the form 1 * x^k for an index operand k.

In some special rings of polynomials, such as a ring of polynomials modulo x^n - 1, monomial_mul can be interpreted as a cyclic shift of the coefficients of the polynomial. For some rings, this results in optimized lowerings that involve rotations and rescaling of the coefficients of the input.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputpolynomial-like
monomialDegreeindex

Results:

ResultDescription
outputpolynomial-like

polynomial.monomial (heir::polynomial::MonomialOp)

Create a polynomial that consists of a single monomial.

Syntax:

operation ::= `polynomial.monomial` operands attr-dict `:` functional-type(operands, results)

Construct a polynomial that consists of a single monomial term, from its degree and coefficient as dynamic inputs.

The coefficient type of the output polynomial’s ring attribute must match the coefficient input type.

Example:

#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%deg = arith.constant 1023 : index
%five = arith.constant 5 : i32
%0 = polynomial.monomial %five, %deg : (i32, index) -> !polynomial.polynomial<#ring>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
coefficientany type
degreeindex

Results:

ResultDescription
outputAn element of a polynomial ring.

polynomial.mul (heir::polynomial::MulOp)

Multiplication operation between polynomials.

Syntax:

operation ::= `polynomial.mul` operands attr-dict `:` type($result)

Performs polynomial multiplication on the operands. The operands may be single polynomials or containers of identically-typed polynomials, i.e., polynomials from the same underlying ring with the same coefficient types.

This op is defined to occur in the ring defined by the ring attribute of the two operands, meaning the arithmetic is taken modulo the polynomialModulus of the ring as well as modulo any semantics defined by the coefficient type.

Example:

// multiply two polynomials modulo x^1024 - 1
#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%0 = polynomial.constant int<1 + x**2> : !polynomial.polynomial<#ring>
%1 = polynomial.constant int<x**5 - x + 1> : !polynomial.polynomial<#ring>
%2 = polynomial.mul %0, %1 : !polynomial.polynomial<#ring>

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhspolynomial-like
rhspolynomial-like

Results:

ResultDescription
resultpolynomial-like

polynomial.mul_scalar (heir::polynomial::MulScalarOp)

Multiplication by a scalar of the field.

Syntax:

operation ::= `polynomial.mul_scalar` operands attr-dict `:` type($polynomial) `,` type($scalar)

Multiplies the polynomial operand’s coefficients by a given scalar value. The scalar input must have the same type as the polynomial ring’s coefficientType.

Example:

// multiply two polynomials modulo x^1024 - 1
#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%0 = polynomial.constant int<1 + x**2> : !polynomial.polynomial<#ring>
%1 = arith.constant 3 : i32
%2 = polynomial.mul_scalar %0, %1 : !polynomial.polynomial<#ring>, i32

Traits: AlwaysSpeculatableImplTrait, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
polynomialpolynomial-like
scalarany type

Results:

ResultDescription
outputpolynomial-like

polynomial.ntt (heir::polynomial::NTTOp)

Computes point-value tensor representation of a polynomial.

Syntax:

operation ::= `polynomial.ntt` $input attr-dict `:` qualified(type($input)) `->` type($output)

polynomial.ntt computes the forward integer Number Theoretic Transform (NTT) on the input polynomial. It returns a tensor containing a point-value representation of the input polynomial. The output tensor has shape equal to the degree of the ring’s polynomialModulus. The polynomial’s RingAttr is embedded as the encoding attribute of the output tensor.

Given an input polynomial F(x) over a ring whose polynomialModulus has degree n, and a primitive n-th root of unity omega_n, the output is the list of $n$ evaluations

f[k] = F(omega[n]^k) ; k = {0, ..., n-1}

The choice of primitive root may be optionally specified.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
root::mlir::heir::polynomial::PrimitiveRootAttran attribute containing an integer and its degree as a root of unity

Operands:

OperandDescription
inputAn element of a polynomial ring.

Results:

ResultDescription
outputranked tensor of Integer type with modular arithmetic values

polynomial.sub (heir::polynomial::SubOp)

Subtraction operation between polynomials.

Syntax:

operation ::= `polynomial.sub` operands attr-dict `:` type($result)

Performs polynomial subtraction on the operands. The operands may be single polynomials or containers of identically-typed polynomials, i.e., polynomials from the same underlying ring with the same coefficient types.

This op is defined to occur in the ring defined by the ring attribute of the two operands, meaning the arithmetic is taken modulo the polynomialModulus of the ring as well as modulo any semantics defined by the coefficient type.

Example:

// subtract two polynomials modulo x^1024 - 1
#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%0 = polynomial.constant int<1 + x**2> : !polynomial.polynomial<#ring>
%1 = polynomial.constant int<x**5 - x + 1> : !polynomial.polynomial<#ring>
%2 = polynomial.sub %0, %1 : !polynomial.polynomial<#ring>

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhspolynomial-like
rhspolynomial-like

Results:

ResultDescription
resultpolynomial-like

polynomial.to_tensor (heir::polynomial::ToTensorOp)

Creates a tensor containing the coefficients of a polynomial.

Syntax:

operation ::= `polynomial.to_tensor` $input attr-dict `:` type($input) `->` type($output)

polynomial.to_tensor creates a dense tensor value containing the coefficients of the input polynomial. The output tensor contains the coefficients in degree-increasing order.

Operations that act on the coefficients of a polynomial, such as extracting a specific coefficient or extracting a range of coefficients, should be implemented by composing to_tensor with the relevant tensor dialect ops.

The output tensor has shape equal to the degree of the polynomial ring attribute’s polynomialModulus, including zeroes.

Example:

#poly = #polynomial.int_polynomial<x**1024 - 1>
#ring = #polynomial.ring<coefficientType=i32, polynomialModulus=#poly>
%two = arith.constant 2 : i32
%five = arith.constant 5 : i32
%coeffs = tensor.from_elements %two, %two, %five : tensor<3xi32>
%poly = polynomial.from_tensor %coeffs : tensor<3xi32> -> !polynomial.polynomial<#ring>
%tensor = polynomial.to_tensor %poly : !polynomial.polynomial<#ring> -> tensor<1024xi32>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputAn element of a polynomial ring.

Results:

ResultDescription
outputranked tensor of any type values

7.12 - Random

Random types

DistributionType

A random distribution type

Syntax:

!random.distribution<
  ::mlir::heir::random::Distribution   # distribution_type
>

A generic type, representing a specific random distribution type of either uniform or gaussian as an attribute ($distribution_type).

Parameters:

ParameterC++ typeDescription
distribution_type::mlir::heir::random::Distribution

PRNGType

A pseudorandom number generator type

Syntax: !random.prng

A type that provides pseudorandom number generator.

Random ops

random.discrete_gaussian_distribution (heir::random::DiscreteGaussianDistributionOp)

Initializes the Discrete Gaussian Distribution

Syntax:

operation ::= `random.discrete_gaussian_distribution` operands attr-dict `:` functional-type(operands, results)

Initializes the Discrete Gaussian Distribution. The distribution is initialized with a mean and a standard deviation and pseudorandom generator that provides the source of the randomness.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
mean::mlir::IntegerAttrAn Attribute containing a integer value
stddev::mlir::IntegerAttrAn Attribute containing a integer value whose value is non-negative

Operands:

OperandDescription
inputA pseudorandom number generator type

Results:

ResultDescription
outputA random distribution type

random.discrete_uniform_distribution (heir::random::DiscreteUniformDistributionOp)

Initializes the Discrete Uniform Distribution

Syntax:

operation ::= `random.discrete_uniform_distribution` $input `{` `range` `=` `[` $min `,` $max `]` `}` attr-dict `:` `(` qualified(type($input)) `)` `->` type($output)

Initializes the Discrete Uniform Distribution. The distribution is initialized with a minimum and a maximum value and pseudo random generator that provides the source of the randomness. The distribution is inclusive of the minimum and exclusive of the maximum.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
min::mlir::IntegerAttrAn Attribute containing a integer value
max::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputA pseudorandom number generator type

Results:

ResultDescription
outputA random distribution type

random.init_prng (heir::random::InitOp)

Initializes the pseudorandom number generator with a seed.

Syntax:

operation ::= `random.init_prng` operands attr-dict `:` functional-type(operands, results)

Initializes the PRNG with a seed. The seed is dynamically provided due to protocols that agree on shared randomness. The PRNG is used to initialized the random distributions such as the discrete gaussian distribution and the discrete uniform distribution. This initialization also takes as input a number of bits that are generated for each number value sampled (num_bits). For instance, a num_bits of 32 will mean that distributions will generate a 32-bit integer value. We expect that the seed initialization is done statically and globally once per thread for all distributions; however, if multiple threads are generating randomness, then seed initialization should be done per thread; otherwise there is no guarantee of consistent behavior. Thread safety is so far not considered.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
num_bits::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
seedsignless-integer-like

Results:

ResultDescription
outputA pseudorandom number generator type

random.sample (heir::random::SampleOp)

Samples from a distribution

Syntax:

operation ::= `random.sample` operands attr-dict `:` functional-type(operands, results)

Samples from the distribution to obtain a random value or tensor of values.

Operands:

OperandDescription
inputA random distribution type

Results:

ResultDescription
outputsignless-integer-like

7.13 - RNS

RNS types

RNSType

A residue number system representation

Syntax:

!rns.rns<
  ::llvm::ArrayRef<mlir::Type>   # basisTypes
>

Parameters:

ParameterC++ typeDescription
basisTypes::llvm::ArrayRef<mlir::Type>

RNS ops

RNS additional definitions

TypeInterface definitions

RNSBasisTypeInterface (RNSBasisTypeInterface)

This interface is required for a type to be used as a parameter to an rns type.

Methods:

isCompatibleWith

bool isCompatibleWith(::mlir::Type otherRnsBasisType);

Returns true if this type is compatible with another type in the same RNS basis. In particular, the set of types used for a single RNS basis are never equal as types, but instead have some common attribute that must be checked here. For example, an RNS type where the basis types are polynomials would return true if the two types are both polynomial types, even if they have different coefficient moduli.

Another example is using mod arith types as the basis types, where by the nature of chinese reminder theorem, it is required that the modulus of them must be mutually coprime.

isCompatibleWith must be commutative, in the sense that type1.isCompatibleWith(type2) if and only if type2.isCompatibleWith(type1).

NOTE: This method must be implemented by the user.

7.14 - Secret

‘secret’ Dialect

Secret is a dialect for computations that operate on encrypted data.

Secret is intended to serve as a scheme-agnostic front-end for the HEIR ecosystem of dialects. It is supposed to be fully interoperable with the rest of MLIR via secret.generic, while lower-level HEIR dialects would have custom types for arithmetic on secret integers of various bit widths.

Secret types

SecretType

A secret value

Syntax:

!secret.secret<
  Type   # valueType
>

A generic wrapper around another MLIR type, representing an encrypted value but not specifying the manner of encryption. This is useful in HEIR because the compiler may choose various details of the FHE scheme based on the properties of the input program, the backend target hardware, and cost models of the various passes.

Parameters:

ParameterC++ typeDescription
valueTypeType

Secret ops

secret.cast (heir::secret::CastOp)

A placeholder cast from one secret type to another

Syntax:

operation ::= `secret.cast` $input attr-dict `:` qualified(type($input)) `to` qualified(type($output))

A cast operation represents a type cast from one secret type to another, that is used to enable the intermixing of various equivalent secret types before a lower-level FHE scheme has been chosen.

For example, secret.cast can be used to convert a secret<i8> to a secret<tensor<8xi1>> as a compatibility layer between boolean and non-boolean parts of a program. The pass that later lowers the IR to specific FHE schemes would need to replace these casts with appropriate scheme-specific operations, and it is left to those later passes to determine which casts are considered valid.

Example:

%result = secret.cast %0 : !secret.secret<i8> to !secret.secret<tensor<8xi1>>
%result2 = secret.cast %0 : !secret.secret<i8> to !secret.secret<tensor<2xi4>>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA secret value

Results:

ResultDescription
outputA secret value

secret.conceal (heir::secret::ConcealOp)

Convert a non-secret value into a secret

Syntax:

operation ::= `secret.conceal` $cleartext attr-dict `:` type($cleartext) `->` type($output)

Convert a value to a secret containing the same value.

This op represents a scheme-agnostic encryption operation, as well as a “trivial encryption” operation which is needed for some FHE schemes. This op is also useful for type materialization in the dialect conversion framework.

Examples:

%Y = secret.conceal %value : i32 -> !secret.secret<i32>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cleartextany type

Results:

ResultDescription
outputA secret value

secret.generic (heir::secret::GenericOp)

Lift a plaintext computation to operate on secrets.

secret.generic lifts a plaintext computation to operate on one or more secrets. The lifted computation is represented as a region containing a single block terminated by secret.yield. The arguments of the secret.generic may include one or more !secret.secret types. The arguments of the block in the op’s body correspond to the underlying plaintext types of the secrets.

secret.generic is not isolated from above, so you may directly reference values in the enclosing scope. This is required to support using secret.generic inside of ops with AffineScope, while having the body of the generic use the induction variables defined by the affine scope.

Basic examples:

Add two secret integers together

%Z = secret.generic ins(%X, %Y : !secret.secret<i32>, !secret.secret<i32>) {
  ^bb0(%x: i32, %y: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

Add a secret value with a plaintext value. I.e., not all arguments to the op need be secret.

%Z = secret.generic ins(%X, %Y : i32, !secret.secret<i32>) {
  ^bb0(%x: i32, %y: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

The same as above, but the plaintext op is not passed through the basic block.

%y = arith.constant 7: i32
%Z = secret.generic ins(%X : !secret.secret<i32>) {
  ^bb0(%x: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

Traits: SingleBlockImplicitTerminator<YieldOp>, SingleBlock

Operands:

OperandDescription
inputsvariadic of any type

Results:

ResultDescription
resultsvariadic of any type

secret.reveal (heir::secret::RevealOp)

Convert a secret value into a non-secret

Syntax:

operation ::= `secret.reveal` $input attr-dict `:` type($input) `->` type($cleartext)

Convert a secret into a non-secret containing the same value.

This op represents a scheme-agnostic decryption operation. This op is also useful for target materialization in the dialect conversion framework.

Examples:

%Y = secret.reveal %secret_value : !secret.secret<i32> -> i32

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA secret value

Results:

ResultDescription
cleartextany type

secret.separator (heir::secret::SeparatorOp)

Convert a non-secret value into a secret

Syntax:

operation ::= `secret.separator` attr-dict ($inputs^ `:` type($inputs))?

This operation is used as a separation boundary between logical subunits of the module. This is used in conjunction with --secret-distribute-generic=distribute-through=secret.separator to break a generic around these separators and allow for optimization passses to analyze and optimize the sub-units locally.

In order to allow bufferization of modules with this operation, we must register a (bogus) memory effect that also prevents this operation from being trivially dead during operation folding.

This operation also accepts operands, which act as boundaries between the logical units. This enforces separation of memref and affine optimizations between the subunits, preventing optimizations from removing the operand and combining the two separated regions. The operand can be thought of as an return value of the logical subunit.

Interfaces: MemoryEffectOpInterface (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{MemoryEffects::Write on ::mlir::SideEffects::DefaultResource}

Operands:

OperandDescription
inputsvariadic of any type

secret.yield (heir::secret::YieldOp)

Secret yield operation

secret.yield is a special terminator operation for blocks inside regions in secret generic ops. It returns the cleartext value of the corresponding private computation to the immediately enclosing secret generic op.

Traits: AlwaysSpeculatableImplTrait, HasParent<GenericOp>, ReturnLike, Terminator

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), RegionBranchTerminatorOpInterface

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
valuesvariadic of any type

7.15 - TensorExt

TensorExt attributes

SIMDPackingAttr

An attribute describing the SIMD packing of a tensor.

Syntax:

#tensor_ext.simd_packing<
  ::mlir::DenseI64ArrayAttr,   # in
  ::mlir::DenseI64ArrayAttr,   # padding
  ::mlir::DenseI64ArrayAttr,   # out
  int64_t   # padding_value
>

This attribute is used as the encoding attribute on a tensor. It describes the transformations that were applied to an input tensor to pack it into the given tensor.

The in attribute describes the shape of the original tensor. The following transformations are applied to the input tensor.

  1. Padding is applied first. The padding attribute is an array with the same size as the input tensor shape. Padding is applied at the end of the array using the padding_value attribute (default zero). The result after zero padding should be a power of two.

  2. The padded result is replicated or split to fill the output tensor shape.

For example,

#packing = #tensor_ext.simd_packing<
  in = [7],
  padding = [1],
  padding_value = 0,
  out = [16],
>

may be used on a tensor type like

tensor<1x16xi32, #packing>

If the original tensor had values [1, 2, 3, 4, 5, 6, 7] then a tensor with this attribute contains the data [1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0].

Parameters:

ParameterC++ typeDescription
in::mlir::DenseI64ArrayAttr
padding::mlir::DenseI64ArrayAttr
out::mlir::DenseI64ArrayAttr
padding_valueint64_t

TensorExt ops

tensor_ext.rotate (heir::tensor_ext::RotateOp)

Rotate a tensor some number of indices left.

Syntax:

operation ::= `tensor_ext.rotate` operands attr-dict `:` qualified(type($tensor)) `,` type($shift)

This op represents a left-rotation of a tensor by given number of indices. Negative shift values are interpreted as right-rotations.

This corresponds to the rotate operation in arithmetic FHE schemes like BGV.

This operation’s current behavior allows rotating multi-dimensional tensors by rotating along the tensor’s only non-unit dimension. This assumes the tensor is packed along the non-unit dimension.

// In the future, the op will be adjusted to support rotations of general // multi-dimensional tensors with a vector of rotation indices for each // dimension. The lowering will implement the correct operations to rotate // the tensor along the indices given its packing.

Examples:

%0 = ... : tensor<16xi32>
%c7 = arith.constant 7 : i32
%1 = tensor_ext.rotate %0, %c7 : tensor<16xi32>, i32

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
tensortensor of any type values
shiftsignless-integer-like

Results:

ResultDescription
outputtensor of any type values

7.16 - TfheRust

’tfhe_rust’ Dialect

The thfe_rust dialect is an exit dialect for generating rust code against the tfhe-rs library API, using the shortint parameters and encoding scheme.

See https://github.com/zama-ai/tfhe-rs

TfheRust types

EncryptedInt8Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type

Syntax: !tfhe_rust.ei8

EncryptedInt16Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type

Syntax: !tfhe_rust.ei16

EncryptedInt32Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type

Syntax: !tfhe_rust.ei32

EncryptedInt64Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type

Syntax: !tfhe_rust.ei64

EncryptedInt128Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type

Syntax: !tfhe_rust.ei128

EncryptedInt256Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Syntax: !tfhe_rust.ei256

EncryptedUInt2Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type

Syntax: !tfhe_rust.eui2

EncryptedUInt3Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type

Syntax: !tfhe_rust.eui3

EncryptedUInt4Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type

Syntax: !tfhe_rust.eui4

EncryptedUInt8Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type

Syntax: !tfhe_rust.eui8

EncryptedUInt10Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type

Syntax: !tfhe_rust.eui10

EncryptedUInt12Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type

Syntax: !tfhe_rust.eui12

EncryptedUInt14Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type

Syntax: !tfhe_rust.eui14

EncryptedUInt16Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type

Syntax: !tfhe_rust.eui16

EncryptedUInt32Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type

Syntax: !tfhe_rust.eui32

EncryptedUInt64Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type

Syntax: !tfhe_rust.eui64

EncryptedUInt128Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type

Syntax: !tfhe_rust.eui128

EncryptedUInt256Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type

Syntax: !tfhe_rust.eui256

LookupTableType

A univariate lookup table used for programmable bootstrapping.

Syntax: !tfhe_rust.lookup_table

ServerKeyType

The short int server key required to perform homomorphic operations.

Syntax: !tfhe_rust.server_key

TfheRust ops

tfhe_rust.apply_lookup_table (heir::tfhe_rust::ApplyLookupTableOp)

Syntax:

operation ::= `tfhe_rust.apply_lookup_table` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
inputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
lookupTableA univariate lookup table used for programmable bootstrapping.

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.create_trivial (heir::tfhe_rust::CreateTrivialOp)

Syntax:

operation ::= `tfhe_rust.create_trivial` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
valueinteger

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.generate_lookup_table (heir::tfhe_rust::GenerateLookupTableOp)

Syntax:

operation ::= `tfhe_rust.generate_lookup_table` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
truthTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.

Results:

ResultDescription
lookupTableA univariate lookup table used for programmable bootstrapping.

tfhe_rust.scalar_left_shift (heir::tfhe_rust::ScalarLeftShiftOp)

Syntax:

operation ::= `tfhe_rust.scalar_left_shift` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
ciphertextAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
shiftAmount8-bit integer

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.add (heir::tfhe_rust::AddOp)

Arithmetic add of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.add` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.bitand (heir::tfhe_rust::BitAndOp)

Logical AND of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.bitand` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.sub (heir::tfhe_rust::SubOp)

Arithmetic sub of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.sub` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

7.17 - TfheRustBool

’tfhe_rust_bool’ Dialect

The tfhe_rust_bool dialect is an exit dialect for generating rust code against the tfhe-rs library API, using the boolean parameter set.

See https://github.com/zama-ai/tfhe-rs

TfheRustBool attributes

TfheRustBoolGatesAttr

An Attribute containing an array of strings to store bool gates

Syntax:

#tfhe_rust_bool.tfhe_rust_bool_gates<
  ::llvm::ArrayRef<::mlir::heir::tfhe_rust_bool::TfheRustBoolGateEnumAttr>   # gates
>

This attributes stores a list of integer identifiers for Boolean gates.

Uses following mapping: AND_GATE = 0; NAND_GATE = 1; OR_GATE = 2; NOR_GATE = 3; XOR_GATE = 4; XNOR_GATE = 5; NOT_GATE = 6;

Parameters:

ParameterC++ typeDescription
gates::llvm::ArrayRef<::mlir::heir::tfhe_rust_bool::TfheRustBoolGateEnumAttr>

TfheRustBool types

EncryptedBoolType

An encrypted Boolean corresponding to tfhe-rs’s FHEBool type

Syntax: !tfhe_rust_bool.eb

PackedServerKeyType

The Belfort packed API boolean server key.

Syntax: !tfhe_rust_bool.server_key_enum

ServerKeyType

The boolean server key required to perform homomorphic operations.

Syntax: !tfhe_rust_bool.server_key

TfheRustBool ops

tfhe_rust_bool.create_trivial (heir::tfhe_rust_bool::CreateTrivialOp)

Syntax:

operation ::= `tfhe_rust_bool.create_trivial` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeytfhe rust bool server key
value1-bit signless integer

Results:

ResultDescription
outputAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

tfhe_rust_bool.and (heir::tfhe_rust_bool::AndOp)

Logical AND of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.and` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.mux (heir::tfhe_rust_bool::MuxOp)

Syntax:

operation ::= `tfhe_rust_bool.mux` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
cndAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type
lhsAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type
rhsAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

Results:

ResultDescription
outputAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

tfhe_rust_bool.nand (heir::tfhe_rust_bool::NandOp)

Logical NAND of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.nand` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.nor (heir::tfhe_rust_bool::NorOp)

Logical NOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.nor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.not (heir::tfhe_rust_bool::NotOp)

Syntax:

operation ::= `tfhe_rust_bool.not` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
inputeb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.or (heir::tfhe_rust_bool::OrOp)

Logical OR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.or` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.gates_packed (heir::tfhe_rust_bool::PackedOp)

Syntax:

operation ::= `tfhe_rust_bool.gates_packed` operands attr-dict `:` functional-type(operands, results)

Operation to where different Boolean gates are executed pairwise between elements of two ciphertext arrays.

For example,

%0 = tfhe_rust_bool.packed_gates %a, %b {gates = #tfhe_rust_bool.tfhe_rust_bool_gates<0 : i32, 4 : i32>} :
    (!tfhe_rust_bool.server_key,
     tensor<2x!tfhe_rust_bool.eb>,
     tensor<2x!tfhe_rust_bool.eb>) -> tensor<2x!tfhe_rust_bool.eb>

applies an “and” gate to the first elements of %a and %b and an xor gate to the second elements.

Mapping is defined in the BooleanGates.td file.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
gates::mlir::heir::tfhe_rust_bool::TfheRustBoolGatesAttrAn Attribute containing an array of strings to store bool gates

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.xnor (heir::tfhe_rust_bool::XnorOp)

Logical XNOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.xnor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.xor (heir::tfhe_rust_bool::XorOp)

Logical XOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.xor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations. or The Belfort packed API boolean server key.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

TfheRustBool additional definitions

8 - Passes

-align-tensor-sizes

Resize tensors into tensors with a fixed size final dimension

This pass resizes input tensors with arbitrary sizes into tensors with whose final dimensions has a fixed size. All input tensors are required to be one-dimensional. The --size option specifies the size of the final dimension of the output tensors, and is required to be a power of two.

To align the tensors in the input IR, the pass first zero pads the input to the nearest power of two before replicating or splitting it into the output shape determined by size. The resulting transformation is described in a SIMDPackingAttr encoding attribute on the final tensor.

For example, with size=16,a tensor with 7 elements will be zero-padded to 8 elements, and then replicated twice to fill a tensor with size 16. The SIMDPackingAttr will encode the input shape, the number of elements that were zero-padded, and the output shape.

Input:

%0 = tensor.empty() : tensor<7xi32>

Output:

%0 = tensor.empty() : tensor<16xi32, #tensor_ext.simd_packing<in = [7], padding = [1], out = [16]>>

A tensor with 30 elements will be zero padded with 2 elements and split into two tensors of size 16.

Input:

%0 = tensor.empty() : tensor<30xi32>

Output:

%0 = tensor.empty() : tensor<2x16xi32, #tensor_ext.simd_packing<in = [30], padding = [2], out = [16]>>

Note that this pass does not insert any new operations like reshape, but rather transforms the IR to use tensors with a fixed dimension. This pass may be used to align the sizes of tensors that represent plaintexts and ciphertexts in RLWE schemes that support SIMD slots and operations.

Options

-size : Power of two size of the final dimension of the output tensors.

-annotate-mgmt

Annotate MgmtAttr for secret SSA values in the IR

This pass runs the secretness/level/dimension analysis and annotates the IR with the results, saving it into each op’s attribute dictionary as mgmt.mgmt :

-annotate-secretness

Annotate secret SSA values in the IR

This pass runs the secretness analysis and annotates the IR with the results, saving it into each op’s attribute dictionary as secret :

-apply-folders

Apply all folding patterns from canonicalize

This pass applies all registered folding patterns greedily to the input IR. This is useful when running a full canonicalize is too slow, but applying folders before canonicalize is sufficient to simplify the IR for later passes, or even sufficient to then subsequently run a full canonicalize pass.

This is used to prepare an IR for insert-rotate after fully unrolling loops.

-bgv-to-lattigo

Lower bgv to lattigo dialect.

This pass lowers the bgv dialect to Lattigo dialect.

-bgv-to-lwe

Lower bgv to lwe dialect.

This pass lowers the bgv dialect to lwe dialect. Note that some scheme specific ops (e.g., modswitch) that have no direct analogue in the lwe dialect are left unchanged. TODO (#1193): support both “common” and “full” lwe lowering

-cggi-boolean-vectorize

Group different logic gates with the packed API

This pass groups independent logic gates into a single call of the packed operations. Pass is based on the straight-line-vectorizer, but is fundamentally different. This pass combines any type of boolean gates and is not restricted to combining the same type of gate operand.

Pass is intended for the FPT tfhe-rs API, where packed_gates function get a the boolean gates are passed as a string vector and a left and right vector of ciphertexts. Each boolean gates specified in gates is then applied element wise.

let outputs_ct = fpga_key.packed_gates(&gates, &ref_to_ct_lefts, &ref_to_ct_rights);

Options

-parallelism : Parallelism factor for batching. 0 is infinite parallelism

-cggi-expand-lut

Expands LUTs into LWE operations and programmable bootstraps

This pass expands the linear combination performed in a LUT operation into the component LWE scalar operations and a programmable bootstrap operation.

For example, a LUT3 operation is composed of three LWE ciphertext inputs $c, b, a$ (in MSB to LSB ordering) which must be combined via the linear combination $4 * c + 2 * b + a$ before being fed into a programmable bootstrap defined by the lookup table.

This pass supports LUT2, LUT3, and LutLincomb operations.

-cggi-set-default-parameters

Set default parameters for CGGI ops

This pass adds default parameters to all CGGI ops as cggi_params named attributes, overriding any existing attribute set with that name.

This pass is primarily for testing purposes, and as a parameter provider before a proper parameter selection mechanism is added. This pass should not be used in production.

The specific parameters are hard-coded in lib/Dialect/CGGI/Transforms/SetDefaultParameters.cpp.

-cggi-to-jaxite

Lower cggi to jaxite dialect.

-cggi-to-tfhe-rust-bool

Lower cggi to tfhe_rust_bool dialect.

-cggi-to-tfhe-rust

Lower cggi to tfhe_rust dialect.

-ckks-to-lwe

Lower ckks to lwe dialect.

This pass lowers the ckks dialect to lwe dialect. Note that some scheme specific ops (e.g., rescale) that have no direct analogue in the lwe dialect are left unchanged. TODO (#1193): support both “common” and “full” lwe lowering

-collapse-insertion-chains

Collapse chains of extract/insert ops into rotate ops when possible

This pass is a cleanup pass for insert-rotate. That pass sometimes leaves behind a chain of insertion operations like this:

%extracted = tensor.extract %14[%c5] : tensor<16xi16>
%inserted = tensor.insert %extracted into %dest[%c0] : tensor<16xi16>
%extracted_0 = tensor.extract %14[%c6] : tensor<16xi16>
%inserted_1 = tensor.insert %extracted_0 into %inserted[%c1] : tensor<16xi16>
%extracted_2 = tensor.extract %14[%c7] : tensor<16xi16>
%inserted_3 = tensor.insert %extracted_2 into %inserted_1[%c2] : tensor<16xi16>
...
%extracted_28 = tensor.extract %14[%c4] : tensor<16xi16>
%inserted_29 = tensor.insert %extracted_28 into %inserted_27[%c15] : tensor<16xi16>
yield %inserted_29 : tensor<16xi16>

In many cases, this chain will insert into every index of the dest tensor, and the extracted values all come from consistently aligned indices of the same source tensor. In this case, the chain can be collapsed into a single rotate.

Each index used for insertion or extraction must be constant; this may require running --canonicalize or --sccp before this pass to apply folding rules (use --sccp if you need to fold constant through control flow).

-convert-elementwise-to-affine

This pass lowers ElementwiseMappable operations to Affine loops.

This pass lowers ElementwiseMappable operations over tensors to affine loop nests that instead apply the operation to the underlying scalar values.

Usage: ‘–convert-elementwise-to-affine=convert-ops=arith.mulf ' restrict conversion to mulf op from arith dialect.

‘–convert-elementwise-to-affine=convert-ops=arith.addf,arith.divf convert-dialects=bgv’ restrict conversion to addf and divf ops from arith dialect and all of the ops in bgv dialect.

–convert-elementwise-to-affine=convert-dialects=arith restrict conversion to arith dialect so ops only from arith dialect is processed.

–convert-elementwise-to-affine=convert-ops=arith.addf,arith.mulf restrict conversion only to these two ops - addf and mulf - from arith dialect.

Options

-convert-ops      : comma-separated list of ops to run this pass on 
-convert-dialects : comma-separated list of dialects to run this pass on 

-convert-if-to-select

Convert scf.if operations on secret conditions to arith.select operations.

Conversion for If-operations that evaluate secret condition to alternative select operations.

-convert-polynomial-mul-to-ntt

Rewrites polynomial operations to their NTT equivalents

Applies a rewrite pattern to convert polynomial multiplication to the equivalent using the number-theoretic transforms (NTT) when possible.

Polynomial multiplication can be rewritten as polynomial.NTT on each operand, followed by modulo elementwise multiplication of the point-value representation and then the inverse-NTT back to coefficient representation.

-convert-secret-extract-to-static-extract

Convert tensor.extract operations on secret index to static extract operations.

Converts tensor.extract operations that read value at secret index to alternative static tensor.extract operations that extracts value at each index and conditionally selects the value extracted at the secret index.

Note: Running this pass alone does not result in a data-oblivious program; we have to run the --convert-if-to-select pass to the resulting program to convert the secret-dependent If-operation to a Select-operation.

Example input: mlir func.func @main(%secretTensor: !secret.secret<tensor<32xi16>>, %secretIndex: !secret.secret<index>)) -> !secret.secret<i16> { ... %0 = secret.generic ins(%secretTensor, %secretIndex : !secret.secret<tensor<32xi16>>, !secret.secret<index>) { ^bb0(%tensor: tensor<32xi16>, %index: index): // Violation: tensor.extract loads value at secret index %extractedValue = tensor.extract %tensor[%index] : tensor<16xi32> ... }

Output:
```mlir
func.func @main(%secretTensor: !secret.secret<tensor<32xi16>>, %secretIndex: !secret.secret<index>)) -> !secret.secret<i16> {
  ...
  %0 = secret.generic ins(%secretTensor, %secretIndex : !secret.secret<tensor<32xi16>>, !secret.secret<index>) {
  ^bb0(%tensor: tensor<32xi16>, %index: index):
    %extractedValue = affine.for %i=0 to 16 iter_args(%arg= %dummyValue) -> (i32) {
      // 1. Check if %i matches %index
      %cond = arith.cmpi eq, %i, %index : index
      // 2. Extract value at %i
      %value = tensor.extract %tensor[%i] : tensor<16xi32>
      // 3. If %i matches %index, yield %value extracted in (2), else yield %dummyValue
      %result = scf.if %cond -> (i32) {
        scf.yield %value : i32
      } else{
        scf.yield %arg : i32
      }
      // 4. Yield result from (3)
      affine.yield %result : i32

} … }

```

-convert-secret-for-to-static-for

Convert secret scf.for ops to affine.for ops with constant bounds.

Conversion for For-operation that evaluate secret bound(s) to alternative affine For-operation with constant bound(s).

It replaces data-dependent bounds with an If-operation to check the bounds, and conditionally execute and yield values from the For-operation’s body. Note: Running this pass alone does not result in a data-oblivious program; we have to run the --convert-if-to-select pass to the resulting program to convert the secret-dependent If-operation to a Select-operation.

Example input:

  func.func @main(%secretTensor: !secret.secret<tensor<16xi32>>, %secretLower: !secret.secret<index>, %secretUpper: !secret.secret<index>) -> !secret.secret<i32> {
   ...
   %0 = secret.generic ins(%secretTensor, %secretLower, %secretUpper : !secret.secret<tensor<16xi32>>, !secret.secret<index>, !secret.secret<index>){
    ^bb0(%tensor: tensor<16xi32>, %lower : index, %upper : index ):
      ...
      %1 = scf.for %i = %lower to %upper step %step iter_args(%arg = %val) -> (i32) {
        %extracted = tensor.extract %input[%i] : tensor<16xi32>
        %sum = arith.addi %extracted, %arg : i32
        scf.yield %sum : i32
      } {lower = 0, upper = 16}
      secret.yield %1 : i32
  } -> !secret.secret<i32>
  return %0 : !secret.secret<i32>

Output:

  func.func @main(%secretTensor: !secret.secret<tensor<16xi32>>, %secretIndex: !secret.secret<index> {secret.secret}) -> !secret.secret<i32> {
   ...
   %0 = secret.generic ins(%secretTensor, %secretLower, %secretUpper : !secret.secret<tensor<16xi32>>, !secret.secret<index>, !secret.secret<index>){
    ^bb0(%tensor: tensor<16xi32>, %lower : index, %upper : index ):
      ...
      %1 = affine.for %i = 0 to 16 step %step iter_args(%arg = %val) -> (i32) {
        %lowerCond = arith.cmpi sge, %i, %index : index
        %upperCond = arith.cmpi slt, %i, %index : index
        %cond = arith.andi %lowerCond, %upperCond : i1
        %result = scf.if(%cond) -> (i32) {
          %extracted = tensor.extract %input[%i] : tensor<16xi32>
          %sum = arith.addi %extracted, %arg : i32
          scf.yield %sum : i32
        } else {
          scf.yield %arg : i32
        }
        affine.yield %result : i32
      } {lower = 0, upper = 16}
      secret.yield %1 : i32
  } -> !secret.secret<i32>
  return %0 : !secret.secret<i32>

-convert-secret-insert-to-static-insert

Convert tensor.insert operations on secret index to static insert operations.

Converts tensor.insert operations that write to secret index to alternative static tensor.insert operations that inserts the inserted value at each index and conditionally selects the newly produced tensor that contains the value at the secret index.

Note: Running this pass alone does not result in a data-oblivious program; we have to run the --convert-if-to-select pass to the resulting program to convert the secret-dependent If-operation to a Select-operation.

Example input:

func.func @main(%secretTensor: !secret.secret<tensor<32xi16>>, %secretIndex: !secret.secret<index>)) -> !secret.secret<i16> {
  ...
  %0 = secret.generic ins(%secretTensor, %secretIndex : !secret.secret<tensor<32xi16>>, !secret.secret<index>) {
  ^bb0(%tensor: tensor<32xi16>, %index: index):
    // Violation: tensor.insert writes value at secret index
    %inserted = tensor.insert %newValue into %tensor[%index] : tensor<16xi32>
  ...
}

Output:

func.func @main(%secretTensor: !secret.secret<tensor<32xi16>>, %secretIndex: !secret.secret<index>)) -> !secret.secret<i16> {
  ...
  %0 = secret.generic ins(%secretTensor, %secretIndex : !secret.secret<tensor<32xi16>>, !secret.secret<index>) {
  ^bb0(%tensor: tensor<32xi16>, %index: index):
    %inserted = affine.for %i=0 to 16 iter_args(%inputArg = %tensor) -> tensor<16xi32> {
      // 1. Check if %i matches the %index
      %cond = arith.cmpi eq, %i, %index : index
      // 2. Insert %newValue and produce %newTensor
      %newTensor = tensor.insert %value into %inputArg[%i] : tensor<16xi32>
      // 3. If %i matches %inputIndex, yield %newTensor, else yield unchanged input tensor
      %finalTensor = scf.if %cond -> (i32) {
        scf.yield %newTensor : tensor<16xi32>
      } else{
        scf.yield %inputArg : tensor<16xi32>
      }
      // 4. Yield final tensor
      affine.yield %finalTensor : tensor<16xi32>
}
  ...
}

-convert-secret-while-to-static-for

Convert secret scf.while ops to affine.for ops that have constant bounds.

Convert scf.while with a secret condition to affine.for with constant bounds. It replaces the scf.condition operation found in the scf.while loop with an scf.if operation that conditionally executes operations in the while operation’s body and yields values.

A “max_iter” attribute should be specified as part of the secret-dependent scf.while operation to successfully transform to a secret-independent affine.for operation. This attribute determines the maximum number of iterations for the new affine.for operation.

Note: Running this pass alone does not result in a data-oblivious program; we have to run the --convert-if-to-select pass to the resulting program to convert the secret-dependent If-operation to a Select-operation.

Example input:

// C-like code
int main(int secretInput) {
  while (secretInput > 100) {
    secretInput = secretInput * secretInput;
  }
  return secretInput;
}

// MLIR
func.func @main(%secretInput: !secret.secret<i16>) -> !secret.secret<i16> {
  %c100 = arith.constant 100 : i16
  %0 = secret.generic ins(%secretInput : !secret.secret<i16>) {
  ^bb0(%input: i16):
    %1 = scf.while (%arg1 = %input) : (i16) -> i16 {
      %2 = arith.cmpi sgt, %arg1, %c100 : i16
      scf.condition(%2) %arg1 : i16
    } do {
    ^bb0(%arg1: i16):
      %3 = arith.muli %arg1, %arg1 : i16
      scf.yield %3 : i16
    } attributes {max_iter = 16 : i64}
    secret.yield %1 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

Output:

func.func @main(%secretInput: !secret.secret<i16>) -> !secret.secret<i16> {
  %c100 = arith.constant 100 : i16
  %0 = secret.generic ins(%secretInput : !secret.secret<i16>) {
  ^bb0(%input: i16):
    %1 = affine.for 0 to 16 iter_args(%arg1 = %input) -> (i16) {
      %2 = arith.cmpi sgt, %arg1, %c100 : i16
      %3 = scf.if (%2) -> i16{
        %4 = arith.muli %arg1, %arg1 : i16
        scf.yield %4 : i16
      } else {
        scf.yield %arg1 : i16
      }
      affine.yield %3 : i16
    } attributes {max_iter = 16 : i64}
    secret.yield %1 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

-convert-tensor-to-scalars

Effectively ‘unrolls’ tensors of static shape to scalars.

This pass will convert a static-shaped tensor type to a TypeRange containing product(dim) copies of the element type of the tensor. This pass currently includes two patterns:

  1. It converts tensor.from_elements operations to the corresponding scalar inputs.
  2. It converts tensor.insert operations by updating the ValueRange corresponding to the converted input and updating it with the scalar to be inserted.

It also applies folders greedily to simplify, e.g., extract(from_elements).

Note: The pass is designed to be run on an IR, where the only operations with tensor typed operands are tensor “management” operations such as insert/extract, with all other operations (e.g., arith operations) already taking (extracted) scalar inputs. For example, an IR where elementwise operations have been converted to scalar operations via --convert-elementwise-to-affine.

The pass might insert new tensor.from_elements operations or manually create the scalar ValueRange via inserting tensor.extract operations if any operations remain that operate on tensors. The pass currently applies irrespective of tensor size, i.e., might be very slow for large tensors.

TODO (#1023): Extend this pass to support more tensor operations, e.g., tensor.slice

Options

-max-size : Limits `unrolling` to tensors with at most max-size elements

-expand-copy

Expands memref.copy ops to explicit affine loads and stores

This pass removes memref copy operations by expanding them to affine loads and stores. This pass introduces affine loops over the dimensions of the MemRef, so must be run prior to any affine loop unrolling in a pipeline.

Input

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    memref.copy %alloc, %alloc_0 : memref<1x1xi32> to memref<1x1xi32>
  }
}

Output

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    affine.for %arg0 = 0 to 2 {
      affine.for %arg1 = 0 to 3 {
        %1 = affine.load %alloc[%arg0, %arg1] : memref<2x3xi32>
        affine.store %1, %alloc_0[%arg0, %arg1] : memref<2x3xi32>
      }
    }
  }
}

When --disable-affine-loop=true is set, then the output becomes

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %0 = affine.load %alloc[%c0, %c0] : memref<2x3xi32>
    affine.store %0, %alloc_0[%c0, %c0] : memref<2x3xi32>
    %1 = affine.load %alloc[%c0, %c1] : memref<2x3xi32>
    affine.store %1, %alloc_0[%c0, %c1] : memref<2x3xi32>
    %2 = affine.load %alloc[%c0, %c2] : memref<2x3xi32>
    affine.store %2, %alloc_0[%c0, %c2] : memref<2x3xi32>
    [...]
  }
}

Options

-disable-affine-loop : Use this to control to disable using affine loops

-extract-loop-body

Extracts logic of a loop bodies into functions.

This pass extracts logic in the inner body of for loops into functions.

This pass requires that tensors are lowered to memref. It expects that a loop body contains a number of affine.load statements used as inputs to the extracted function, and a single affine.store used as the extracted function’s output.

Input

module {
  func.func @loop_body() {
    %c-128_i8 = arith.constant -128 : i8
    %c127_i8 = arith.constant 127 : i8
    %alloc_7 = memref.alloc() {alignment = 64 : i64} : memref<25x20x8xi8>
    affine.for %arg1 = 0 to 25 {
      affine.for %arg2 = 0 to 20 {
        affine.for %arg3 = 0 to 8 {
          %98 = affine.load %alloc_6[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
          %99 = arith.cmpi slt, %arg0, %c-128_i8 : i8
          %100 = arith.select %99, %c-128_i8, %arg0 : i8
          %101 = arith.cmpi sgt, %arg0, %c127_i8 : i8
          %102 = arith.select %101, %c127_i8, %100 : i8
          affine.store %102, %alloc_7[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
        }
      }
    }
  }
}

Output

module {
  func.func @loop_body() {
    %alloc_7 = memref.alloc() {alignment = 64 : i64} : memref<25x20x8xi8>
    affine.for %arg1 = 0 to 25 {
      affine.for %arg2 = 0 to 20 {
        affine.for %arg3 = 0 to 8 {
          %98 = affine.load %alloc_6[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
          %102 = func.call @__for_loop(%98) : (i8) -> i8
          affine.store %102, %alloc_7[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
        }
      }
    }
  }
  func.func private @__for_loop(%arg0: i8) -> i8 {
    %c-128_i8 = arith.constant -128 : i8
    %c127_i8 = arith.constant 127 : i8
    %99 = arith.cmpi slt, %arg0, %c-128_i8 : i8
    %100 = arith.select %99, %c-128_i8, %arg0 : i8
    %101 = arith.cmpi sgt, %arg0, %c127_i8 : i8
    %102 = arith.select %101, %c127_i8, %100 : i8
    return %102 : i8
  }
}

Options

-min-loop-size : Use this to control the minimum loop size to apply this pass
-min-body-size : Use this to control the minimum loop body size to apply this pass

-forward-insert-to-extract

Forward inserts to extracts within a single block

This pass is similar to forward-store-to-load pass where store ops are forwarded load ops; here instead tensor.insert ops are forwarded to tensor.extract ops.

Does not support complex control flow within a block, nor ops with arbitrary subregions.

-forward-store-to-load

Forward stores to loads within a single block

This pass is a simplified version of mem2reg and similar passes. It analyzes an operation, finding all basic blocks within that op that have memrefs whose stores can be forwarded to loads.

Does not support complex control flow within a block, nor ops with arbitrary subregions.

-full-loop-unroll

Fully unroll all loops

Scan the IR for affine.for loops and unroll them all.

-insert-rotate

Vectorize arithmetic FHE operations using HECO-style heuristics

This pass implements the SIMD-vectorization passes from the HECO paper.

The pass operates by identifying arithmetic operations that can be suitably combined into a combination of cyclic rotations and vectorized operations on tensors. It further identifies a suitable “slot target” for each operation and heuristically aligns the operations to reduce unnecessary rotations.

This pass by itself does not eliminate any operations, but instead inserts well-chosen rotations so that, for well-structured code (like unrolled affine loops), a subsequent --cse and --canonicalize pass will dramatically reduce the IR. As such, the pass is designed to be paired with the canonicalization patterns in tensor_ext, as well as the collapse-insertion-chains pass, which cleans up remaining insertion and extraction ops after the main simplifications are applied.

Unlike HECO, this pass operates on plaintext types and tensors, along with the HEIR-specific tensor_ext dialect for its cyclic rotate op. It is intended to be run before lowering to a scheme dialect like bgv.

-linalg-canonicalizations

This pass canonicalizes the linalg.transpose operation of a constant into a transposed constant.

This pass canonicalizes the linalg.transpose operation of a constant into a transposed constant.

-linalg-to-tensor-ext

Lower linalg.matmul to arith and tensor_ext dialects.

This pass lowers the linalg.matmul to a mixture of affine, tensor, and via the Halevi-Shoup and squat matrix multiplication algorithms.

-lwe-add-client-interface

Add client interfaces to (R)LWE encrypted functions

This pass adds encrypt and decrypt functions for each compiled function in the IR. These functions maintain the same interface as the original function, while the compiled function may lose some of this information by the lowerings to ciphertext types (e.g., a scalar ciphertext, when lowered through RLWE schemes, must be encoded as a tensor).

Options

-use-public-key          : If true, generate a client interface that uses a public key for encryption.
-one-value-per-helper-fn : If true, split encryption helpers into separate functions for each SSA value.

-lwe-set-default-parameters

Set default parameters for LWE ops

This pass adds default parameters to all lwe types as the lwe_params attribute, and for lwe ops as the params attribute, overriding any existing attributes set with those names.

This pass is primarily for testing purposes, and as a parameter provider before a proper parameter selection mechanism is added. This pass should not be used in production.

The specific parameters are hard-coded in lib/Dialect/LWE/Transforms/SetDefaultParameters.cpp.

-lwe-to-openfhe

Lower lwe to openfhe dialect.

This pass lowers the lwe dialect to Openfhe dialect. Currently, this also includes patterns that apply directly to ckks and bgv dialect operations. TODO (#1193): investigate if the need for ckks/bgv patterns in --lwe-to-openfhe is permanent.

-lwe-to-polynomial

Lower lwe to polynomial dialect.

This pass lowers the lwe dialect to polynomial dialect.

-memref-global-replace

MemrefGlobalReplacePass forwards global memrefs accessors to arithmetic values

This pass forwards constant global MemRef values to referencing affine loads. This pass requires that the MemRef global values are initialized as constants and that the affine load access indices are constants (i.e. not variadic). Unroll affine loops prior to running this pass.

MemRef removal is required to remove any memory allocations from the input model (for example, TensorFlow models contain global memory holding model weights) to support FHE transpilation.

Input

module {
  memref.global "private" constant @__constant_8xi16 : memref<2x4xi16> = dense<[[-10, 20, 3, 4], [5, 6, 7, 8]]>
  func.func @main() -> i16 {
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %0 = memref.get_global @__constant_8xi16 : memref<2x4xi16>
    %1 = affine.load %0[%c1, %c1 + %c2] : memref<2x4xi16>
    return %1 : i16
  }
}

Output

module {
  func.func @main() -> i16 {
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %c8_i16 = arith.constant 8 : i16
    return %c8_i16 : i16
  }
}

-mod-arith-to-arith

Lower mod_arith to standard arith.

This pass lowers the mod_arith dialect to their arith equivalents.

-mod-arith-to-mac

Finds consecutive ModArith mul and add operations and converts them to a Mac operation

Walks over the programs to find Add operations, it checks if the any operands originates from a mul operation. If so, it converts the Add operation to a Mac operation and removes the mul operation.

-openfhe-configure-crypto-context

Configure the crypto context in OpenFHE

This pass generates helper functions to generate and configure the OpenFHE crypto context for the given function. Generating the crypto context sets the appropriate encryption parameters, while the configuration generates the necessary evaluation keys (relinearization and rotation keys).

For example, for an MLIR function @my_func, the generated helpers have the following signatures

func.func  @my_func__generate_crypto_context() -> !openfhe.crypto_context

func.func  @my_func__configure_crypto_context(!openfhe.crypto_context, !openfhe.private_key) -> !openfhe.crypto_context

Options

-entry-function      : Default entry function name of entry function.
-level-budget-encode : Level budget for CKKS bootstrap encode (s2c) phase
-level-budget-decode : Level budget for CKKS bootstrap decode (c2s) phase
-insecure            : Whether to use insecure parameter for faster evaluation(should only be used in test) (defaults to false)

-operation-balancer

This pass balances addition and multiplication operations.

This pass examines a tree or graph of add and multiplication operations and balances them to minimize the depth of the tree. This exposes better parallelization and reducing the multiplication depth can decrease the parameters used in FHE, which improves performance. This pass is not necessarily optimal, as there may be intermediate computations that this pass does not optimally minimize the depth for.

The algorithm is to analyze a graph of addition operations and do a depth-first search for the operands (from the last computed values in the graph). If there are intermediate computations that are used more than once, then the pass treats that computation as its own tree to balance instead of trying to minimize the global depth of the tree.

This pass only runs on addition and multiplication operations on the arithmetic dialect that are encapsulated inside a secret.generic.

This pass was inspired by section 2.6 of ‘EVA Improved: Compiler and Extension Library for CKKS’ by Chowdhary et al.

-optimize-relinearization

Optimize placement of relinearization ops

This pass defers relinearization ops as late as possible in the IR. This is more efficient in cases where multiplication operations are followed by additions, such as in a dot product. Because relinearization also adds error, deferring it can reduce the need for bootstrapping.

In this pass, we use an integer linear program to determine the optimal relinearization strategy. It solves an ILP for each func op in the IR.

The assumptions of this pass include:

  • All return values of functions must be linearized.
  • All ciphertext arguments to an op must have the same key basis
  • Rotation op inputs must have be linearized.

For an ILP model specification, see the docs at the HEIR website. The model is an adaptation of the ILP described in a blog post by Jeremy Kun.

Options

-use-loc-based-variable-names : When true, the ILP uses op source locations in variable names, which can help debug ILP model bugs.

-polynomial-to-mod-arith

Lower polynomial to standard MLIR dialects.

This pass lowers the polynomial dialect to standard MLIR plus mod_arith, including possibly ops from affine, tensor, linalg, and arith.

-remove-unused-memref

Cleanup any unused memrefs

Scan the IR for unused memrefs and remove them.

This pass looks for locally allocated memrefs that are never used and deletes them. This pass can be used as a cleanup pass from other IR simplifications that forward stores to loads.

-rotate-and-reduce

Use a logarithmic number of rotations to reduce a tensor.

This pass identifies when a commutative, associative binary operation is used to reduce all of the entries of a tensor to a single value, and optimizes the operations by using a logarithmic number of reduction operations.

In particular, this pass identifies an unrolled set of operations of the form (the binary ops may come in any order):

%0 = tensor.extract %t[0] : tensor<8xi32>
%1 = tensor.extract %t[1] : tensor<8xi32>
%2 = tensor.extract %t[2] : tensor<8xi32>
%3 = tensor.extract %t[3] : tensor<8xi32>
%4 = tensor.extract %t[4] : tensor<8xi32>
%5 = tensor.extract %t[5] : tensor<8xi32>
%6 = tensor.extract %t[6] : tensor<8xi32>
%7 = tensor.extract %t[7] : tensor<8xi32>
%8 = arith.addi %0, %1 : i32
%9 = arith.addi %8, %2 : i32
%10 = arith.addi %9, %3 : i32
%11 = arith.addi %10, %4 : i32
%12 = arith.addi %11, %5 : i32
%13 = arith.addi %12, %6 : i32
%14 = arith.addi %13, %7 : i32

and replaces it with a logarithmic number of rotate and addi operations:

%0 = tensor_ext.rotate %t, 4 : tensor<8xi32>
%1 = arith.addi %t, %0 : tensor<8xi32>
%2 = tensor_ext.rotate %1, 2 : tensor<8xi32>
%3 = arith.addi %1, %2 : tensor<8xi32>
%4 = tensor_ext.rotate %3, 1 : tensor<8xi32>
%5 = arith.addi %3, %4 : tensor<8xi32>

-secret-capture-generic-ambient-scope

Capture the ambient scope used in a secret.generic

For each value used in the body of a secret.generic op, which is defined in the ambient scope outside the generic, add it to the argument list of the generic.

-secret-distribute-generic

Distribute generic ops through their bodies.

Converts generic ops whose region contains many ops into smaller sequences of generic ops whose regions contain a single op, dropping the generic part from any resulting generic ops that have no secret.secret inputs. If the op has associated regions, and the operands are not secret, then the generic is distributed recursively through the op’s regions as well.

This pass is intended to be used as part of a front-end pipeline, where a program that operates on a secret type annotates the input to a region as secret, and then wraps the contents of the region in a single large secret.generic, then uses this pass to simplify it.

The distribute-through option allows one to specify a comma-separated list of op names (e.g., distribute-thorugh="affine.for,scf.if"), which limits the distribution to only pass through those ops. If unset, all ops are distributed through when possible.

Options

-distribute-through : comma-separated list of ops that should be distributed through

-secret-extract-generic-body

Extract the bodies of all generic ops into functions

This pass extracts the body of all generic ops into functions, and replaces the generic bodies with call ops. Used as a sub-operation in some passes, and extracted into its own pass for testing purposes.

This pass works best when --secret-generic-absorb-constants is run before it so that the extracted function contains any constants used in the generic op’s body.

-secret-forget-secrets

Convert secret types to standard types

Drop the secret<...> type from the IR, replacing it with the contained type and the corresponding cleartext computation.

-secret-generic-absorb-constants

Copy constants into a secret.generic body

For each constant value used in the body of a secret.generic op, which is defined in the ambient scope outside the generic, add it’s definition into the generic body.

-secret-generic-absorb-dealloc

Copy deallocs of internal memrefs into a secret.generic body

For each memref allocated and used only within a body of a secret.generic op, add it’s dealloc of the memref into its generic body.

-secret-insert-mgmt-bgv

Place BGV ciphertext management operations

This pass implements the following placement strategy:

For relinearize, after every homomorphic ciphertext-ciphertext multiplication, a mgmt.relinearize is placed after the operation. This is done to ensure that the ciphertext keeps linear.

For modulus switching, it is inserted right before a homomorphic multiplication, including ciphertext-plaintext ones. There is an option include-first controlling whether to switch modulus before the first multiplication.

User can check the FLEXIBLEAUTOEXT and FLEXIBLEAUTO mode in OpenFHE as a reference. To know more technical difference about them, user can refer to the paper “Revisiting homomorphic encryption schemes for finite firelds”.

Then, for level-mismatching binary operations like addition and subtraction, additional modulus switch is placed for the operand until it reaches the same level.

This is different from crosslevel operation handling in other implementations like using modulus switching and level drop together. The reason we only use modulus switching is for simplicity for now. Further optimization on this pass could implement such a strategy.

Before yield the final result, a modulus switching is placed if it is a result of multiplication or derived value of a multiplication.

Also, it annotates the mgmt.mgmt attribute for each operation, which includes the level and dimension information of a ciphertext. This information is subsequently used by the secret-to-bgv pass to properly lower to corresponding RNS Type.

Example of multiplication+addition:

func.func @func(%arg0: !secret.secret<i16>, %arg1: !secret.secret<i16>) -> !secret.secret<i16> {
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<i16>, !secret.secret<i16>) {
  ^bb0(%arg2: i16, %arg3: i16):
    %1 = arith.muli %arg2, %arg3 : i16
    %2 = arith.addi %1, %arg3 : i16
    secret.yield %2 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

which get transformed to:

func.func @func(%arg0: !secret.secret<i16>, %arg1: !secret.secret<i16>) -> !secret.secret<i16> {
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<i16>, !secret.secret<i16>) attrs = {arg0 = {mgmt.mgmt = #mgmt.mgmt<level = 1>}, arg1 = {mgmt.mgmt = #mgmt.mgmt<level = 1>}} {
  ^bb0(%arg2: i16, %arg3: i16):
    %1 = arith.muli %arg2, %arg3 {mgmt.mgmt = #mgmt.mgmt<level = 1, dimension = 3>} : i16
    %2 = mgmt.relinearize %1 {mgmt.mgmt = #mgmt.mgmt<level = 1>} : i16
    %3 = arith.addi %2, %arg3 {mgmt.mgmt = #mgmt.mgmt<level = 1>} : i16
    %4 = mgmt.modreduce %3 {mgmt.mgmt = #mgmt.mgmt<level = 0>} : i16
    secret.yield %4 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

Options

-include-first-mul : Modulus switching right before the first multiplication (default to false)

-secret-insert-mgmt-ckks

Place CKKS ciphertext management operations

Check the description of secret-insert-mgmt-bgv. This pass implements similar strategy, where mgmt.modreduce stands for ckks.rescale here.

Options

-include-first-mul : Modulus switching right before the first multiplication (default to false)
-slot-number       : Default number of slots use for ciphertext space.

-secret-merge-adjacent-generics

Merge two adjacent generics into a single generic

This pass merges two immedaitely sequential generics into a single generic. Useful as a sub-operation in some passes, and extracted into its own pass for testing purposes.

-secret-to-bgv

Lower secret to bgv dialect.

This pass lowers an IR with secret.generic blocks containing arithmetic operations to operations on ciphertexts with the BGV dialect.

The pass assumes that the secret.generic regions have been distributed through arithmetic operations so that only one ciphertext operation appears per generic block. It also requires that canonicalize was run so that non-secret values used are removed from the secret.generic’s block arguments.

The pass requires that all types are tensors of a uniform shape matching the dimension of the ciphertext space specified my poly-mod-degree.

Options

-poly-mod-degree      : Default degree of the cyclotomic polynomial modulus to use for ciphertext space.
-coefficient-mod-bits : Default number of bits of the prime coefficient modulus to use for the ciphertext space.

-secret-to-cggi

Lower secret to cggi dialect.

This pass lowers the secret dialect to cggi dialect.

-secret-to-ckks

Lower secret to ckks dialect.

This pass lowers an IR with secret.generic blocks containing arithmetic operations to operations on ciphertexts with the CKKS dialect.

The pass assumes that the secret.generic regions have been distributed through arithmetic operations so that only one ciphertext operation appears per generic block. It also requires that canonicalize was run so that non-secret values used are removed from the secret.generic’s block arguments.

The pass requires that all types are tensors of a uniform shape matching the dimension of the ciphertext space specified my poly-mod-degree.

Options

-poly-mod-degree      : Default degree of the cyclotomic polynomial modulus to use for ciphertext space.
-coefficient-mod-bits : Default number of bits of the prime coefficient modulus to use for the ciphertext space.

-secretize

Adds secret argument attributes to entry function

Helper pass that adds a secret.secret attribute argument to each function argument. By default, the pass applies to all functions in the module. This may be overridden with the option -function=func_name to apply to a single function only.

Options

-function : function to add secret annotations to

-straight-line-vectorize

A vectorizer for straight line programs.

This pass ignores control flow and only vectorizes straight-line programs within a given region.

Options

-dialect : Use this to restrict the dialect whose ops should be vectorized.

-tosa-to-secret-arith

Lower tosa.sigmoid to secret arith dialects.

This pass lowers the tosa.sigmoid dialect to the polynomial approximation -0.004 * x^3 + 0.197 * x + 0.5 (composed of arith, affine, and tensor operations).

This polynomial approximation of sigmoid only works over the range [-5, 5] and is taken from the paper ‘Logisitic regression over encrypted data from fully homomorphic encryption’ by Chen et al..

-unroll-and-forward

Loop unrolls and forwards stores to loads.

This pass processes the first function in a given module, and, starting from the first loop, iteratively does the following:

  1. Fully unroll the loop.
  2. Scan for load ops. For each load op with a statically-inferrable access index:
  3. Backtrack to the original memref alloc
  4. Find all store ops at the corresponding index (possibly transitively through renames/subviews of the underlying alloc).
  5. Find the last store that occurs and forward it to the load.
  6. If the original memref is an input memref, then forward through any renames to make the target load load directly from the argument memref (instead of any subviews, say)
  7. Apply the same logic to any remaining loads not inside any for loop.

This pass requires that tensors are lowered to memref, and only supports affine loops with affine.load/store ops.

Memrefs that result from memref.get_global ops are excluded from forwarding, even if they are loaded with a static index, and are instead handled by memref-global-replace, which should be run after this pass.

-wrap-generic

Wraps regions using secret args in secret.generic bodies

This pass converts functions (func.func) with {secret.secret} annotated arguments to use !secret.secret<...> types and wraps the function body in a secret.generic region. The output type is also converted to !secret.secret<...>.

Example input:

  func.func @main(%arg0: i32 {secret.secret}) -> i32 {
    %0 = arith.constant 100 : i32
    %1 = arith.addi %0, %arg0 : i32
    return %1 : i32
  }

Output:

  func.func @main(%arg0: !secret.secret<i32>) -> !secret.secret<i32> {
    %0 = secret.generic ins(%arg0 : !secret.secret<i32>) {
    ^bb0(%arg1: i32):
      %1 = arith.constant 100 : i32
      %2 = arith.addi %0, %arg1 : i32
      secret.yield %2 : i32
    } -> !secret.secret<i32>
    return %0 : !secret.secret<i32>
  }

-yosys-optimizer

Invoke Yosys to perform circuit optimization.

This pass invokes Yosys to convert an arithmetic circuit to an optimized boolean circuit that uses the arith and comb dialects.

Note that booleanization changes the function signature: multi-bit integers are transformed to a tensor of booleans, for example, an i8 is converted to tensor<8xi1>.

The optimizer will be applied to each secret.generic op containing arithmetic ops that can be optimized.

Optional parameters:

  • abc-fast: Run the abc optimizer in “fast” mode, getting faster compile time at the expense of a possibly larger output circuit.
  • unroll-factor: Before optimizing the circuit, unroll loops by a given factor. If unset, this pass will not unroll any loops.
  • print-stats: Prints statistics about the optimized circuits.
  • mode={Boolean,LUT}: Map gates to boolean gates or lookup table gates.
  • use-submodules: Extract the body of a generic op into submodules. Useful for large programs with generics that can be isolated. This should not be used when distributing generics through loops to avoid index arguments in the function body.

Statistics

total circuit size : The total circuit size for all optimized circuits, after optimization is done.