Files
zeekay 674da0bb9e Name the licence the repository actually carries
The headers said 'BSD3 License with Lux Industries Exceptions', which is not
a licence that exists — the sweep that removed the upstream vendor's name
substituted ours into the licence title instead of correcting it. LICENSE.txt
is the BSD 3-Clause Clear License and the headers now say so.
2026-08-27 16:00:52 -07:00

992 lines
40 KiB
C++

// Part of the Torus Compiler Project, under the BSD 3-Clause Clear
// License. See
// https://github.com/luxfhe/torus/blob/main/LICENSE.txt
// for license information.
#include <iostream>
#include <mlir/Dialect/Arith/IR/Arith.h>
#include <mlir/Dialect/Bufferization/IR/Bufferization.h>
#include <mlir/Dialect/Func/IR/FuncOps.h>
#include <mlir/Dialect/Linalg/IR/Linalg.h>
#include <mlir/IR/Operation.h>
#include "toruslang/Dialect/Optimizer/IR/OptimizerOps.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/DialectConversion.h"
#include "toruslang/Conversion/FHEToTFHEScalar/Pass.h"
#include "toruslang/Conversion/Passes.h"
#include "toruslang/Conversion/Tools.h"
#include "toruslang/Conversion/Utils/Dialects/SCF.h"
#include "toruslang/Conversion/Utils/FuncConstOpConversion.h"
#include "toruslang/Conversion/Utils/RTOpConverter.h"
#include "toruslang/Conversion/Utils/TensorOpTypeConversion.h"
#include "toruslang/Dialect/FHE/IR/FHEDialect.h"
#include "toruslang/Dialect/FHE/IR/FHEOps.h"
#include "toruslang/Dialect/FHE/IR/FHETypes.h"
#include "toruslang/Dialect/RT/IR/RTDialect.h"
#include "toruslang/Dialect/RT/IR/RTOps.h"
#include "toruslang/Dialect/RT/IR/RTTypes.h"
#include "toruslang/Dialect/TFHE/IR/TFHEDialect.h"
#include "toruslang/Dialect/TFHE/IR/TFHEOps.h"
#include "toruslang/Dialect/TFHE/IR/TFHEParameters.h"
#include "toruslang/Dialect/TFHE/IR/TFHETypes.h"
#include "toruslang/Dialect/Tracing/IR/TracingOps.h"
#include "toruslang/Support/logging.h"
namespace FHE = mlir::toruslang::FHE;
namespace TFHE = mlir::toruslang::TFHE;
namespace Tracing = mlir::toruslang::Tracing;
const int CIPHERTEXT_MODULUS = 64;
const int PADDING_LEFT = 1;
int lsb_position(int bitWidth) {
return CIPHERTEXT_MODULUS - 1 - PADDING_LEFT - bitWidth + 1;
}
namespace fhe_to_tfhe_scalar_conversion {
inline void forwardOptimizerID(mlir::Operation *source,
mlir::Operation *destination) {
auto optimizerIdAttr = source->getAttr("TFHE.OId");
if (optimizerIdAttr == nullptr) {
mlir::toruslang::log_verbose() << "No TFHE.OId\n";
return;
}
destination->setAttr("TFHE.OId", optimizerIdAttr);
}
// Set the `signed` attribute to true if the type is signed
inline void markOpIfSigned(mlir::Operation *op,
FHE::FheIntegerInterface resultType) {
auto isSigned = resultType.isSigned();
if (isSigned) {
op->setAttr("signed", mlir::BoolAttr::get(op->getContext(), true));
}
}
inline void
forwardLinearlyOptimizerIDS(mlir::Operation &source,
std::vector<mlir::Value> &destinations) {
// linear dispatch optimizer ids
auto optimizerIds = source.getAttrOfType<mlir::DenseI32ArrayAttr>("TFHE.OId");
if (optimizerIds != nullptr) {
assert((size_t)optimizerIds.size() == destinations.size());
mlir::Builder builder(source.getContext());
for (auto i = 0u; i < destinations.size(); i++) {
auto id = builder.getI32IntegerAttr(optimizerIds[i]);
auto destOp = destinations[i].getDefiningOp();
destOp->setAttr("TFHE.OId", id);
}
} else {
assert(source.getAttrOfType<mlir::IntegerAttr>("TFHE.OId") == nullptr);
mlir::toruslang::log_verbose() << "No TFHE.OId\n";
}
}
namespace typing {
/// Converts an encrypted integer into `TFHE::GlweCiphetext`.
TFHE::GLWECipherTextType convertEncrypted(mlir::MLIRContext *context,
FHE::FheIntegerInterface enc) {
return TFHE::GLWECipherTextType::get(context, TFHE::GLWESecretKey());
}
/// The type converter used to convert `FHE` to `TFHE` types using the scalar
/// strategy.
class TypeConverter : public mlir::TypeConverter {
public:
TypeConverter() {
addConversion([](mlir::Type type) { return type; });
addConversion([&](mlir::FunctionType type) {
llvm::SmallVector<mlir::Type> inputTypes;
llvm::SmallVector<mlir::Type> resultTypes;
for (mlir::Type inputType : type.getInputs())
inputTypes.push_back(this->convertType(inputType));
for (mlir::Type resultType : type.getResults())
resultTypes.push_back(this->convertType(resultType));
mlir::Type res =
mlir::FunctionType::get(type.getContext(), inputTypes, resultTypes);
return res;
});
addConversion([](FHE::FheIntegerInterface type) {
return convertEncrypted(type.getContext(), type);
});
addConversion([](FHE::EncryptedBooleanType type) {
return TFHE::GLWECipherTextType::get(type.getContext(),
TFHE::GLWESecretKey());
});
addConversion([&](mlir::RankedTensorType type) {
return mlir::RankedTensorType::get(
type.getShape(), this->convertType(type.getElementType()));
});
addConversion([&](mlir::toruslang::RT::FutureType type) {
return mlir::toruslang::RT::FutureType::get(
this->convertType(type.dyn_cast<mlir::toruslang::RT::FutureType>()
.getElementType()));
});
addConversion([&](mlir::toruslang::RT::PointerType type) {
return mlir::toruslang::RT::PointerType::get(
this->convertType(type.dyn_cast<mlir::toruslang::RT::PointerType>()
.getElementType()));
});
}
/// Returns a lambda that uses this converter to turn one type into another.
std::function<mlir::Type(mlir::MLIRContext *, mlir::Type)>
getConversionLambda() {
return [&](mlir::MLIRContext *, mlir::Type t) { return convertType(t); };
}
};
} // namespace typing
namespace lowering {
/// A pattern rewriter superclass used by most op rewriters during the
/// conversion.
template <typename T>
struct ScalarOpPattern : public mlir::OpConversionPattern<T> {
ScalarOpPattern(mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: mlir::OpConversionPattern<T>(converter, context, benefit) {}
/// Writes the encoding of a plaintext of arbitrary precision using shift.
mlir::Value
writePlaintextShiftEncoding(mlir::Location location, mlir::Value rawPlaintext,
int64_t encryptedWidth,
mlir::ConversionPatternRewriter &rewriter) const {
int64_t intShift = 64 - 1 - encryptedWidth;
mlir::Value castedInt = rewriter.create<mlir::arith::ExtSIOp>(
location, rewriter.getIntegerType(64), rawPlaintext);
mlir::Value constantShiftOp = rewriter.create<mlir::arith::ConstantOp>(
location, rewriter.getI64IntegerAttr(intShift));
mlir::Value encodedInt = rewriter.create<mlir::arith::ShLIOp>(
location, rewriter.getI64Type(), castedInt, constantShiftOp);
return encodedInt;
}
};
/// Rewriter for the `FHE::add_eint_int` operation.
struct AddEintIntOpPattern : public ScalarOpPattern<FHE::AddEintIntOp> {
AddEintIntOpPattern(mlir::TypeConverter &converter,
mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::AddEintIntOp>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::AddEintIntOp op, FHE::AddEintIntOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
// Write the plaintext encoding
mlir::Value encodedInt = writePlaintextShiftEncoding(
op.getLoc(), adaptor.getB(),
op.getType().cast<FHE::FheIntegerInterface>().getWidth(), rewriter);
// Write the new op
auto newOp = rewriter.replaceOpWithNewOp<TFHE::AddGLWEIntOp>(
op, getTypeConverter()->convertType(op.getType()), adaptor.getA(),
encodedInt);
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
}
};
/// Rewriter for the `FHE::add_eint` operation.
struct AddEintOpPattern : public mlir::OpConversionPattern<FHE::AddEintOp> {
AddEintOpPattern(mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: mlir::OpConversionPattern<FHE::AddEintOp>(converter, context, benefit) {
}
mlir::LogicalResult
matchAndRewrite(FHE::AddEintOp op, FHE::AddEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
// Write the new op
auto newOp = rewriter.replaceOpWithNewOp<TFHE::AddGLWEOp>(
op, getTypeConverter()->convertType(op.getType()),
adaptor.getOperands());
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
}
};
/// Rewriter for the `FHE::sub_eint_int` operation.
struct SubEintIntOpPattern : public ScalarOpPattern<FHE::SubEintIntOp> {
SubEintIntOpPattern(mlir::TypeConverter &converter,
mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::SubEintIntOp>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::SubEintIntOp op, FHE::SubEintIntOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
mlir::Location location = op.getLoc();
mlir::Value eintOperand = op.getA();
mlir::Value intOperand = op.getB();
// Write the integer negation
mlir::Type intType = intOperand.getType();
mlir::Attribute minusOneAttr = mlir::IntegerAttr::get(intType, -1);
mlir::Value minusOne =
rewriter.create<mlir::arith::ConstantOp>(location, minusOneAttr)
.getResult();
auto negative =
rewriter.create<mlir::arith::MulIOp>(location, intOperand, minusOne);
// Write the plaintext encoding
mlir::Value encodedInt = writePlaintextShiftEncoding(
op.getLoc(), negative,
eintOperand.getType().cast<FHE::FheIntegerInterface>().getWidth(),
rewriter);
// Write the new op
auto newOp = rewriter.replaceOpWithNewOp<TFHE::AddGLWEIntOp>(
op, getTypeConverter()->convertType(op.getType()), adaptor.getA(),
encodedInt);
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
};
};
/// Rewriter for the `FHE::sub_int_eint` operation.
struct SubIntEintOpPattern : public ScalarOpPattern<FHE::SubIntEintOp> {
SubIntEintOpPattern(mlir::TypeConverter &converter,
mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::SubIntEintOp>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::SubIntEintOp op, FHE::SubIntEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
// Write the plaintext encoding
mlir::Value encodedInt = writePlaintextShiftEncoding(
op.getLoc(), adaptor.getA(),
op.getB().getType().cast<FHE::FheIntegerInterface>().getWidth(),
rewriter);
// Write the new op
auto newOp = rewriter.replaceOpWithNewOp<TFHE::SubGLWEIntOp>(
op, getTypeConverter()->convertType(op.getType()), encodedInt,
adaptor.getB());
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
};
};
/// Rewriter for the `FHE::sub_eint` operation.
struct SubEintOpPattern : public ScalarOpPattern<FHE::SubEintOp> {
SubEintOpPattern(mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::SubEintOp>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::SubEintOp op, FHE::SubEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
mlir::Location location = op.getLoc();
mlir::Value lhsOperand = adaptor.getA();
mlir::Value rhsOperand = adaptor.getB();
// Write rhs negation
auto negative = rewriter.create<TFHE::NegGLWEOp>(
location, rhsOperand.getType(), rhsOperand);
forwardOptimizerID(op, negative);
// Write new op.
auto newOp = rewriter.replaceOpWithNewOp<TFHE::AddGLWEOp>(
op, getTypeConverter()->convertType(op.getType()), lhsOperand,
negative.getResult());
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
};
};
/// Rewriter for the `FHE::mul_eint_int` operation.
struct MulEintIntOpPattern : public ScalarOpPattern<FHE::MulEintIntOp> {
MulEintIntOpPattern(mlir::TypeConverter &converter,
mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::MulEintIntOp>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::MulEintIntOp op, FHE::MulEintIntOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
mlir::Location location = op.getLoc();
mlir::Value eintOperand = adaptor.getA();
mlir::Value intOperand = adaptor.getB();
// Write the cleartext "encoding"
mlir::Value castedCleartext = rewriter.create<mlir::arith::ExtSIOp>(
location, rewriter.getIntegerType(64), intOperand);
// Write the new op.
auto newOp = rewriter.replaceOpWithNewOp<TFHE::MulGLWEIntOp>(
op, getTypeConverter()->convertType(op.getType()), eintOperand,
castedCleartext);
forwardOptimizerID(op, newOp);
markOpIfSigned(newOp, op.getType().cast<FHE::FheIntegerInterface>());
return mlir::success();
}
};
/// Rewriter for the `FHE::apply_lookup_table` operation.
struct ApplyLookupTableEintOpPattern
: public ScalarOpPattern<FHE::ApplyLookupTableEintOp> {
ApplyLookupTableEintOpPattern(
mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::toruslang::ScalarLoweringParameters loweringParams,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::ApplyLookupTableEintOp>(converter, context,
benefit),
loweringParameters(loweringParams) {}
mlir::LogicalResult
matchAndRewrite(FHE::ApplyLookupTableEintOp op,
FHE::ApplyLookupTableEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
auto inputType = op.getA().getType().cast<FHE::FheIntegerInterface>();
size_t outputBits =
op.getResult().getType().cast<FHE::FheIntegerInterface>().getWidth();
mlir::Value newLut =
rewriter
.create<TFHE::EncodeExpandLutForBootstrapOp>(
op.getLoc(),
mlir::RankedTensorType::get(
mlir::ArrayRef<int64_t>(loweringParameters.polynomialSize),
rewriter.getI64Type()),
op.getLut(),
rewriter.getI32IntegerAttr(loweringParameters.polynomialSize),
rewriter.getI32IntegerAttr(outputBits),
rewriter.getBoolAttr(inputType.isSigned()))
.getResult();
typing::TypeConverter converter;
mlir::Value input = adaptor.getA();
auto operatorIndexes =
op->getAttrOfType<mlir::DenseI32ArrayAttr>("TFHE.OId");
if (operatorIndexes != nullptr) {
assert(operatorIndexes != nullptr && operatorIndexes.size() > 0);
}
if (inputType.isSigned()) {
// If the input is a signed integer, it comes to the bootstrap with a
// signed-leveled encoding (compatible with 2s complement semantics).
// Unfortunately pbs is not compatible with this encoding, since the
// (virtual) msb must be 0 to avoid a lookup in the phantom negative lut.
uint64_t constantRaw = (uint64_t)1 << (inputType.getWidth() - 1);
// Note that the constant must be encoded with one more bit to ensure the
// signed extension used in the plaintext encoding works as expected.
mlir::Value constant = rewriter.create<mlir::arith::ConstantOp>(
op.getLoc(),
rewriter.getIntegerAttr(
rewriter.getIntegerType(inputType.getWidth() + 1), constantRaw));
mlir::Value encodedConstant = writePlaintextShiftEncoding(
op.getLoc(), constant, inputType.getWidth(), rewriter);
auto inputOp = rewriter.create<TFHE::AddGLWEIntOp>(
op.getLoc(), converter.convertType(input.getType()), input,
encodedConstant);
inputOp->setAttr("overflow-detection",
mlir::BoolAttr::get(op->getContext(), false));
if (operatorIndexes != nullptr) {
assert(operatorIndexes.size() == 2);
auto addIndex = operatorIndexes[0];
inputOp->setAttr("TFHE.OId", rewriter.getI32IntegerAttr(addIndex));
}
input = inputOp;
}
// Insert keyswitch
auto ksOp = rewriter.create<TFHE::KeySwitchGLWEOp>(
op.getLoc(), getTypeConverter()->convertType(adaptor.getA().getType()),
input,
TFHE::GLWEKeyswitchKeyAttr::get(op.getContext(), TFHE::GLWESecretKey(),
TFHE::GLWESecretKey(), -1, -1, -1));
if (operatorIndexes != nullptr) {
ksOp->setAttr("TFHE.OId",
rewriter.getI32IntegerAttr(
operatorIndexes[operatorIndexes.size() - 1]));
}
// Insert bootstrap
auto bsOp = rewriter.replaceOpWithNewOp<TFHE::BootstrapGLWEOp>(
op, getTypeConverter()->convertType(op.getType()), ksOp, newLut,
TFHE::GLWEBootstrapKeyAttr::get(op.getContext(), TFHE::GLWESecretKey(),
TFHE::GLWESecretKey(), -1, -1, -1, -1,
-1));
if (operatorIndexes != nullptr) {
bsOp->setAttr("TFHE.OId",
rewriter.getI32IntegerAttr(
operatorIndexes[operatorIndexes.size() - 1]));
}
return mlir::success();
};
private:
mlir::toruslang::ScalarLoweringParameters loweringParameters;
};
template <typename Op>
std::vector<mlir::Value> extractBitWithClearedLowerBits(
Op op, mlir::Type inputType, uint64_t inputBitwidth,
uint64_t outputBitwidth, uint64_t bitPosition, uint64_t polynomialSize,
mlir::Value ainput, mlir::ConversionPatternRewriter &rewriter) {
typing::TypeConverter converter;
auto cInputTy =
converter.convertType(inputType).cast<TFHE::GLWECipherTextType>();
auto loc = op.getLoc();
//---------------------------------------------------------- BIT ISOLATION
// To extract the bit, we use a PBS that look up on the padding bit.
// We first begin by isolating the bit in question on the
// padding bit. This is performed with a homomorphic multiplication (left
// shift basically) of the proper amount. For example:
//
// previousOutput = |0110|01| .... |
// ^
// shiftCst = | 100000|
// previousOutput * shiftCst = |1| .... |
uint64_t rawShiftCst = ((uint64_t)1) << (inputBitwidth - bitPosition);
mlir::Value shiftCst = rewriter.create<mlir::arith::ConstantOp>(
loc, rewriter.getI64IntegerAttr(rawShiftCst));
auto shiftedInput =
rewriter.create<TFHE::MulGLWEIntOp>(loc, cInputTy, ainput, shiftCst);
//-------------------------------------------------------- LUT PREPARATION
// To perform the right shift (kind of), we use a PBS that acts on the
// padding bit. We expect is the following function to be applied (for the
// first round of our example):
//
// f(|0| .... |) = |0000|00| .... |
// f(|1| .... |) = |0000|01| .... |
//
// That being said, a PBS on the padding bit can only encode a symmetric
// function (that is f(1) = -f(0)), by encoding f(0) in the whole table.
// To implement our semantic, we then rely on a trick. We encode the
// following function in the bootstrap:
//
// f(|0| .... |) = |1111|11|1 .... |
// f(|1| .... |) = |0000|00|1 .... |
//
// And add a correction constant:
//
// corrCst = |0000|00|1 .... |
// f(|0| .... |) + corrCst = |0000|00| .... |
// f(|1| .... |) + corrCst = |0000|01| .... |
//
// Hence the following constant lut.
auto firstBitSet = lsb_position(outputBitwidth) - 1;
auto f_0 = ((uint64_t)0 - 1) << firstBitSet;
llvm::SmallVector<int64_t> rawLut(polynomialSize, f_0);
mlir::Value lut = rewriter.create<mlir::arith::ConstantOp>(
loc, mlir::DenseIntElementsAttr::get(
mlir::RankedTensorType::get(rawLut.size(),
rewriter.getIntegerType(64)),
rawLut));
//-------------------------------------------------- CIPHERTEXT ALIGNMENT
// In practice, TFHE ciphertexts are normally distributed around a value.
// That means that if the lookup is performed _as is_, we have almost .5
// probability to return the wrong value. Imagine a ciphertext centered
// around (|0| .... |):
//
// | 0000001... | 1111111... | Virtual lookup table
// _
// / \
// _______________/ \_________________________ Ciphertext distribution
//
// |0| ... | Ciphertexts mean
//
// If the error of the ciphertext is negative, this means that the lookup
// will wrap, and fall on the wrong mega-case...
//
// This is usually taken care of on the lookup table side, but we can also
// slightly shift the ciphertext to center its distribution with the
// center of the mega-case. That is, end up with a situation like this:
//
// | 1111111... | 0000001... | Virtual lookup table
// _
// / \
// ______/ \_________________________ Ciphertext distribution
//
// |0| ... | Ciphertexts mean
//
// This is performed by adding |0|1 .... | to the ciphertext.
uint64_t rawRotationCst = ((uint64_t)1)
<< (CIPHERTEXT_MODULUS - PADDING_LEFT - 1);
mlir::Value rotationCst = rewriter.create<mlir::arith::ConstantOp>(
loc, rewriter.getI64IntegerAttr(rawRotationCst));
auto shiftedRotatedInput = rewriter.create<TFHE::AddGLWEIntOp>(
loc, cInputTy, shiftedInput, rotationCst);
//-------------------------------------------------------------------- PBS
// The lookup is performed ...
auto context = op.getContext();
auto secretKey = TFHE::GLWESecretKey();
auto ksk = TFHE::GLWEKeyswitchKeyAttr::get(context, secretKey, secretKey, -1,
-1, -1);
auto bsk = TFHE::GLWEBootstrapKeyAttr::get(context, secretKey, secretKey, -1,
-1, -1, -1, -1);
auto keyswitched = rewriter.create<TFHE::KeySwitchGLWEOp>(
loc, cInputTy, shiftedRotatedInput, ksk);
auto bootstrapped = rewriter.create<TFHE::BootstrapGLWEOp>(
loc, cInputTy, keyswitched, lut, bsk);
//------------------------------------------------------------- CORRECTION
// The correction is performed to achieve our right shift semantic.
uint64_t rawCorrCst = ((uint64_t)1) << firstBitSet;
mlir::Value corrCst = rewriter.create<mlir::arith::ConstantOp>(
loc, rewriter.getI64IntegerAttr(rawCorrCst));
auto extractedBit =
rewriter.create<TFHE::AddGLWEIntOp>(loc, cInputTy, bootstrapped, corrCst);
// Nothing need to be converted on type side, side the new dialect has no
// precision.
return {shiftedInput, shiftedRotatedInput, keyswitched, bootstrapped,
extractedBit};
}
struct RoundEintOpPattern : public ScalarOpPattern<FHE::RoundEintOp> {
RoundEintOpPattern(
mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::toruslang::ScalarLoweringParameters loweringParams,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::RoundEintOp>(converter, context, benefit),
loweringParameters(loweringParams) {}
::mlir::LogicalResult
matchAndRewrite(FHE::RoundEintOp op, FHE::RoundEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
// The round operator allows to move from a given precision to a smaller one
// by rounding the most significant bits of the message. For example a 5
// bits message:
// 101_11 (23)
// would be rounded to a 3 bit message:
// 110 (6)
//
// The following procedure can be homomorphically applied to implement this
// semantic:
// 1) Propagate the carry of the round around 2^(n_before-n_after)
// performed with a homomorphic adddition.
// 2) For each bits to be discarded we truncate it:
// -> Extract a ciphertext of only the bit to be discarded by
// performing a left shift and a pbs.
// -> Subtract this one from the input by performing a
// homomorphic subtraction.
mlir::Value input = adaptor.getInput();
auto inputType = op.getInput().getType().cast<FHE::FheIntegerInterface>();
mlir::Value output = op.getResult();
uint64_t inputBitwidth = inputType.getWidth();
uint64_t outputBitwidth =
output.getType().cast<FHE::FheIntegerInterface>().getWidth();
uint64_t bitwidthDelta = inputBitwidth - outputBitwidth;
typing::TypeConverter converter;
auto inputTy =
converter.convertType(inputType).cast<TFHE::GLWECipherTextType>();
//-------------------------------------------------------- CARRY PROPAGATION
// The first step we take is to propagate the carry of the round in the
// msbs. This we perform with an addition of cleartext correctly encoded.
// Say we have a 5 bits message that we want to round for 3 bits, we
// perform the following addition:
//
// input = |0101|11| .... |
// carryCst = |0000|10| .... |
// input + carryCst = |0110|01| .... |
uint64_t rawCarryCst = ((uint64_t)1) << (bitwidthDelta - 1);
mlir::Value carryCst = rewriter.create<mlir::arith::ConstantOp>(
op->getLoc(),
rewriter.getIntegerAttr(rewriter.getIntegerType(bitwidthDelta + 1),
rawCarryCst));
mlir::Value encodedCarryCst = writePlaintextShiftEncoding(
op.getLoc(), carryCst, inputBitwidth, rewriter);
auto carryPropagatedVal = rewriter.create<TFHE::AddGLWEIntOp>(
op.getLoc(), inputTy, input, encodedCarryCst);
forwardOptimizerID(op, carryPropagatedVal);
//--------------------------------------------------------------- TRUNCATION
// The second step is to truncate every lsbs to be removed, from the least
// significant one to the most significant one. For example:
//
// previousOutput = |0110|01| .... | (t_0)
// previousOutput = |0110|00| .... | (t_1)
// ^
// previousOutput = |0110|00| .... | (t_2)
// ^
//
// For this, we have to generate a ciphertext that contains only the bit to
// be truncated:
//
// bitToRemove = |0000|01| .... | (t_1)
// ^
// bitToRemove = |0000|00| .... | (t_1)
// ^
mlir::Value previousOutput = carryPropagatedVal;
TFHE::GLWECipherTextType truncationInputTy = inputTy;
for (uint64_t i = 0; i < bitwidthDelta; ++i) {
auto extractionOps = extractBitWithClearedLowerBits(
op, inputType, inputBitwidth, inputBitwidth - i, i,
loweringParameters.polynomialSize, previousOutput, rewriter);
for (auto new_op : extractionOps) {
forwardOptimizerID(op, new_op.getDefiningOp());
}
auto extractedBit = extractionOps.back();
//------------------------------------------------------------- TRUNCATION
// Finally, the extracted bit is subtracted from the input.
auto minusIsolatedBit = rewriter.create<TFHE::NegGLWEOp>(
op.getLoc(), truncationInputTy, extractedBit);
forwardOptimizerID(op, minusIsolatedBit);
truncationInputTy = TFHE::GLWECipherTextType::get(rewriter.getContext(),
TFHE::GLWESecretKey());
auto truncationOutput = rewriter.create<TFHE::AddGLWEOp>(
op.getLoc(), truncationInputTy, previousOutput, minusIsolatedBit);
forwardOptimizerID(op, truncationOutput);
previousOutput = truncationOutput;
}
rewriter.replaceOp(op, {previousOutput});
return mlir::success();
};
private:
mlir::toruslang::ScalarLoweringParameters loweringParameters;
};
struct LsbEintOpPattern : public ScalarOpPattern<FHE::LsbEintOp> {
LsbEintOpPattern(mlir::TypeConverter &converter, mlir::MLIRContext *context,
mlir::toruslang::ScalarLoweringParameters loweringParams,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<FHE::LsbEintOp>(converter, context, benefit),
loweringParameters(loweringParams) {}
::mlir::LogicalResult
matchAndRewrite(FHE::LsbEintOp op, FHE::LsbEintOp::Adaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
mlir::Value ainput = adaptor.getInput();
mlir::Value input = op.getInput();
mlir::Value output = op.getResult();
auto inputType = input.getType().cast<FHE::FheIntegerInterface>();
auto outputType = output.getType().cast<FHE::FheIntegerInterface>();
uint64_t inputBitwidth = inputType.getWidth();
uint64_t outputBitwidth = outputType.getWidth();
auto ops = extractBitWithClearedLowerBits(
op, inputType, inputBitwidth, outputBitwidth, 0,
loweringParameters.polynomialSize, ainput, rewriter);
forwardLinearlyOptimizerIDS(*op, ops);
rewriter.replaceOp(op, ops.back());
return mlir::success();
}
private:
mlir::toruslang::ScalarLoweringParameters loweringParameters;
};
/// A pattern rewriter for Noop-like operators.
template <typename T, typename TAdaptor>
struct NoopScalarOpPattern : public ScalarOpPattern<T> {
NoopScalarOpPattern(mlir::TypeConverter &converter,
mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: ScalarOpPattern<T>(converter, context, benefit) {}
mlir::LogicalResult
matchAndRewrite(T op, TAdaptor adaptor,
mlir::ConversionPatternRewriter &rewriter) const override {
rewriter.replaceOp(op, op.getInput());
return mlir::success();
}
};
/// Rewriter for the `FHE::to_bool` operation.
struct ToBoolOpPattern : public mlir::OpRewritePattern<FHE::ToBoolOp> {
ToBoolOpPattern(mlir::MLIRContext *context, mlir::PatternBenefit benefit = 1)
: mlir::OpRewritePattern<FHE::ToBoolOp>(context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::ToBoolOp op,
mlir::PatternRewriter &rewriter) const override {
auto width =
op.getInput()
.getType()
.dyn_cast<mlir::toruslang::FHE::EncryptedUnsignedIntegerType>()
.getWidth();
if (width == mlir::toruslang::FHE::EncryptedBooleanType::getWidth()) {
rewriter.replaceOp(op, op.getInput());
return mlir::success();
}
// TODO
op->emitError("only support conversion with width 2 for the moment");
return mlir::failure();
}
};
/// Rewriter for the `FHE::from_bool` operation.
struct FromBoolOpPattern : public mlir::OpRewritePattern<FHE::FromBoolOp> {
FromBoolOpPattern(mlir::MLIRContext *context,
mlir::PatternBenefit benefit = 1)
: mlir::OpRewritePattern<FHE::FromBoolOp>(context, benefit) {}
mlir::LogicalResult
matchAndRewrite(FHE::FromBoolOp op,
mlir::PatternRewriter &rewriter) const override {
auto width =
op.getResult()
.getType()
.dyn_cast<mlir::toruslang::FHE::EncryptedUnsignedIntegerType>()
.getWidth();
if (width == mlir::toruslang::FHE::EncryptedBooleanType::getWidth()) {
rewriter.replaceOp(op, op.getInput());
return mlir::success();
}
// TODO
op->emitError("only support conversion with width 2 for the moment");
return mlir::failure();
}
};
} // namespace lowering
struct FHEToTFHEScalarPass : public FHEToTFHEScalarBase<FHEToTFHEScalarPass> {
FHEToTFHEScalarPass(
mlir::toruslang::ScalarLoweringParameters loweringParams)
: loweringParameters(loweringParams){};
void runOnOperation() override {
auto op = this->getOperation();
mlir::ConversionTarget target(getContext());
typing::TypeConverter converter;
//------------------------------------------- Marking legal/illegal dialects
target.addIllegalDialect<FHE::FHEDialect>();
target.addLegalDialect<TFHE::TFHEDialect>();
target.addLegalDialect<mlir::arith::ArithDialect>();
target.addDynamicallyLegalOp<mlir::linalg::GenericOp,
mlir::tensor::GenerateOp>(
[&](mlir::Operation *op) {
return (
converter.isLegal(op->getOperandTypes()) &&
converter.isLegal(op->getResultTypes()) &&
converter.isLegal(op->getRegion(0).front().getArgumentTypes()));
});
target.addDynamicallyLegalOp<mlir::func::FuncOp>(
[&](mlir::func::FuncOp funcOp) {
return converter.isSignatureLegal(funcOp.getFunctionType()) &&
converter.isLegal(&funcOp.getBody());
});
target.addDynamicallyLegalOp<mlir::func::ConstantOp>(
[&](mlir::func::ConstantOp op) {
return FunctionConstantOpConversion<typing::TypeConverter>::isLegal(
op, converter);
});
target.addLegalOp<mlir::func::CallOp>();
target.addLegalOp<mlir::scf::InParallelOp>();
//---------------------------------------------------------- Adding patterns
mlir::RewritePatternSet patterns(&getContext());
// Patterns for the `FHE` dialect operations
patterns.add<
// |_ `FHE::zero_eint`
mlir::toruslang::GenericOneToOneOpConversionPattern<
FHE::ZeroEintOp, TFHE::ZeroGLWEOp, true>,
// |_ `FHE::zero_tensor`
mlir::toruslang::GenericOneToOneOpConversionPattern<
FHE::ZeroTensorOp, TFHE::ZeroTensorGLWEOp, true>,
// |_ `FHE::neg_eint`
mlir::toruslang::GenericOneToOneOpConversionPattern<
FHE::NegEintOp, TFHE::NegGLWEOp, true>,
// |_ `FHE::not`
mlir::toruslang::GenericOneToOneOpConversionPattern<
FHE::BoolNotOp, TFHE::NegGLWEOp, true>>(&getContext(), converter);
// |_ `FHE::add_eint_int`
patterns.add<lowering::AddEintIntOpPattern,
// |_ `FHE::add_eint`
lowering::AddEintOpPattern,
// |_ `FHE::sub_int_eint`
lowering::SubIntEintOpPattern,
// |_ `FHE::sub_eint_int`
lowering::SubEintIntOpPattern,
// |_ `FHE::sub_eint`
lowering::SubEintOpPattern,
// |_ `FHE::mul_eint_int`
lowering::MulEintIntOpPattern,
// |_ `FHE::to_signed`
lowering::NoopScalarOpPattern<FHE::ToSignedOp,
FHE::ToSignedOp::Adaptor>,
// |_ `FHE::to_unsigned`
lowering::NoopScalarOpPattern<FHE::ToUnsignedOp,
FHE::ToUnsignedOp::Adaptor>,
// |_ `FHE::reinterpret_precision`
lowering::NoopScalarOpPattern<
FHE::ReinterpretPrecisionEintOp,
FHE::ReinterpretPrecisionEintOp::Adaptor>>(converter,
&getContext());
// |_ `FHE::apply_lookup_table`
patterns.add<lowering::ApplyLookupTableEintOpPattern,
// |_ `FHE::round`
lowering::RoundEintOpPattern,
// |_ `FHE::lsb`
lowering::LsbEintOpPattern>(converter, &getContext(),
loweringParameters);
// Patterns for boolean conversion ops
patterns.add<lowering::FromBoolOpPattern, lowering::ToBoolOpPattern>(
&getContext());
// Patterns for the relics of the `FHELinalg` dialect operations.
// |_ `linalg::generic` turned to nested `scf::for`
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::linalg::YieldOp>>(patterns.getContext(), converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::GenerateOp, true>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::scf::ForOp>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::scf::ForallOp>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::EmptyOp>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::FromElementsOp>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::DimOp>,
mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::ParallelInsertSliceOp, true>>(&getContext(),
converter);
mlir::toruslang::populateWithTensorTypeConverterPatterns(
patterns, target, converter);
// Patterns for `func` dialect operations.
mlir::populateFunctionOpInterfaceTypeConversionPattern<mlir::func::FuncOp>(
patterns, converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::func::ReturnOp>>(patterns.getContext(), converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::func::ReturnOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::scf::YieldOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::scf::ForOp>(target,
converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::scf::ForallOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::tensor::EmptyOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::tensor::FromElementsOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<
mlir::tensor::ParallelInsertSliceOp>(target, converter);
patterns.add<FunctionConstantOpConversion<typing::TypeConverter>>(
&getContext(), converter);
// Patterns for `tracing` dialect.
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
Tracing::TraceCiphertextOp, true>>(&getContext(), converter);
mlir::toruslang::addDynamicallyLegalTypeOp<Tracing::TraceCiphertextOp>(
target, converter);
// Patterns for `bufferization` dialect operations.
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::bufferization::AllocTensorOp, true>>(patterns.getContext(),
converter);
mlir::toruslang::addDynamicallyLegalTypeOp<
mlir::bufferization::AllocTensorOp>(target, converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::scf::YieldOp>>(&getContext(), converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::toruslang::Optimizer::PartitionFrontierOp, true>>(
&getContext(), converter);
mlir::toruslang::addDynamicallyLegalTypeOp<
mlir::toruslang::Optimizer::PartitionFrontierOp>(target, converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::EmptyOp>>(&getContext(), converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::tensor::EmptyOp>(
target, converter);
patterns.add<mlir::toruslang::TypeConvertingReinstantiationPattern<
mlir::tensor::YieldOp>>(patterns.getContext(), converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::tensor::YieldOp>(
target, converter);
mlir::toruslang::addDynamicallyLegalTypeOp<mlir::tensor::DimOp>(
target, converter);
mlir::toruslang::populateWithRTTypeConverterPatterns(patterns, target,
converter);
//--------------------------------------------------------- Apply conversion
if (mlir::applyPartialConversion(op, target, std::move(patterns))
.failed()) {
this->signalPassFailure();
}
// TODO: Dirty fix to make check test happy
op->walk([](mlir::Operation *op) { op->removeAttr("MANP"); });
}
private:
mlir::toruslang::ScalarLoweringParameters loweringParameters;
};
} // namespace fhe_to_tfhe_scalar_conversion
namespace mlir {
namespace toruslang {
std::unique_ptr<OperationPass<ModuleOp>>
createConvertFHEToTFHEScalarPass(ScalarLoweringParameters loweringParameters) {
return std::make_unique<fhe_to_tfhe_scalar_conversion::FHEToTFHEScalarPass>(
loweringParameters);
}
} // namespace toruslang
} // namespace mlir