Skip to content

Commit 49d7ddb

Browse files
j2kuncopybara-github
authored andcommitted
Add elementwise mappable variation for rlwe_encode
This change adds a new ElementwiseByOperandOpInterface method that allows an op to specify which dimensions of a tensor-typed operand should be mapped over. This is necessary to support having an elementwise op like lwe.rlwe_encode, whose input is a tensor<k x slots> and whose output is a tensor<k x lwe.lwe_plaintext<...>>. This is needed in turn because server-side secret.conceal lowers to an elementwise trivial_encrypt, which lowers to an elementwise encode + encrypt. The verifier is updated to ensure that the "mapped shape" (subset of dimensions specified by the interface as mappable) aligns across operands and the output. Then the ElementwiseToAffine pass is updated to use this interface to support lwe.rlwe_encode. PiperOrigin-RevId: 888110533
1 parent 23e2143 commit 49d7ddb

11 files changed

Lines changed: 376 additions & 85 deletions

File tree

lib/Dialect/HEIRInterfaces.cpp

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,67 +75,68 @@ LogicalResult verifyElementwiseByOperandImpl(
7575
ElementwiseByOperandOpInterface opInterface) {
7676
Operation* op = opInterface.getOperation();
7777

78-
auto typeToShapeStr = [](Type type) {
79-
if (auto rankedTensorType = dyn_cast<RankedTensorType>(type)) {
80-
std::string shapeStr = "(";
81-
for (auto dim : rankedTensorType.getShape()) {
82-
shapeStr += std::to_string(dim) + ",";
83-
}
84-
shapeStr += ")";
85-
return shapeStr;
86-
}
87-
return std::string("(not statically ranked)");
78+
auto typeToShapeStr = [](ArrayRef<int64_t> shape) {
79+
return "(" +
80+
llvm::join(
81+
llvm::map_range(shape,
82+
[](int64_t dim) { return std::to_string(dim); }),
83+
", ") +
84+
")";
8885
};
8986

90-
std::optional<TensorType> tensorType;
91-
int64_t operandIndex = 0;
87+
std::optional<SmallVector<int64_t>> mappedShape;
88+
int64_t firstMappableOperandIndex = -1;
89+
9290
for (auto [i, operand] : llvm::enumerate(op->getOperands())) {
9391
auto thisTensorType = dyn_cast<TensorType>(operand.getType());
94-
if (!thisTensorType)
95-
// Non-tensor types are acceptable, and need not be specified as mappable
96-
// or not mappable by the interface.
97-
continue;
92+
if (!thisTensorType) continue;
9893

9994
if (opInterface.operandIsMappable(i)) {
100-
if (!tensorType) {
101-
tensorType = thisTensorType;
102-
operandIndex = i;
95+
SmallVector<int64_t> thisMappedShape;
96+
for (int dim : opInterface.mappedDimensionsForOperand(i)) {
97+
thisMappedShape.push_back(thisTensorType.getDimSize(dim));
98+
}
99+
100+
if (!mappedShape) {
101+
mappedShape = thisMappedShape;
102+
firstMappableOperandIndex = i;
103103
continue;
104104
}
105105

106-
if (thisTensorType.getShape() != tensorType->getShape()) {
106+
if (thisMappedShape != *mappedShape) {
107107
return op->emitOpError()
108-
<< "expected all mappable operands to have the same shape, "
109-
<< "but found shape " << typeToShapeStr(*tensorType)
110-
<< " at operand " << operandIndex << " and "
111-
<< typeToShapeStr(thisTensorType) << " at operand " << i;
108+
<< "expected all mappable operands to have the same mapped "
109+
"shape, but found mapped shape "
110+
<< typeToShapeStr(*mappedShape) << " at operand "
111+
<< firstMappableOperandIndex << " and "
112+
<< typeToShapeStr(thisMappedShape) << " at operand " << i;
112113
}
113114
}
114115
}
115116

116117
for (auto [i, result] : llvm::enumerate(op->getResults())) {
117118
auto thisTensorType = dyn_cast<TensorType>(result.getType());
118-
if (tensorType && !thisTensorType)
119+
if (mappedShape && !mappedShape->empty() && !thisTensorType)
119120
return op->emitOpError()
120-
<< "expected all results operands to have the same tensor shape, "
121-
<< "as the mappable input operands, but found shape "
122-
<< typeToShapeStr(*tensorType) << " at operand " << operandIndex
123-
<< " and result " << i << " of non-tensor type "
124-
<< result.getType();
121+
<< "expected all results to be tensors with shape "
122+
<< typeToShapeStr(*mappedShape)
123+
<< " due to mappable operands, but result " << i
124+
<< " is of non-tensor type " << result.getType();
125125

126-
if (!tensorType && thisTensorType)
126+
if (!mappedShape && thisTensorType)
127127
return op->emitOpError()
128-
<< "No operands were tensor typed, but result at index " << i
129-
<< " is a tensor of shape " << typeToShapeStr(thisTensorType);
128+
<< "No operands were mappable, but result at index " << i
129+
<< " is a tensor of shape "
130+
<< typeToShapeStr(thisTensorType.getShape());
130131

131-
if (tensorType && thisTensorType &&
132-
thisTensorType.getShape() != tensorType->getShape()) {
132+
if (mappedShape && thisTensorType &&
133+
thisTensorType.getShape() != ArrayRef<int64_t>(*mappedShape)) {
133134
return op->emitOpError()
134135
<< "expected all tensor results to have the same shape as "
135136
"mappable operands, but found shape "
136-
<< typeToShapeStr(*tensorType) << " at operand " << operandIndex
137-
<< " and shape " << typeToShapeStr(thisTensorType) << " at result "
138-
<< i;
137+
<< typeToShapeStr(*mappedShape) << " at operand "
138+
<< firstMappableOperandIndex << " and shape "
139+
<< typeToShapeStr(thisTensorType.getShape()) << " at result " << i;
139140
}
140141
}
141142

lib/Dialect/HEIRInterfaces.td

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ def ElementwiseByOperandOpInterface : OpInterface<"ElementwiseByOperandOpInterfa
119119
be replicated and not mapped.
120120

121121
This trait is designed to work with the `convert-elementwise-to-affine`
122-
pass.
122+
pass. This pass assumes that the mapped dimensions for each operand
123+
constitute a contiguous prefix of that operand's dimensions.
123124
}];
124125

125126
let methods = [
@@ -130,6 +131,24 @@ def ElementwiseByOperandOpInterface : OpInterface<"ElementwiseByOperandOpInterfa
130131
/*args=*/(ins "unsigned":$operandIndex)
131132
>,
132133

134+
InterfaceMethod<
135+
/*desc=*/"Return the subset of indices along which this operand is mapped elementise",
136+
/*retTy=*/"llvm::SmallVector<int>",
137+
/*methodName=*/"mappedDimensionsForOperand",
138+
/*args=*/(ins "unsigned":$operandIndex),
139+
/*methodBody=*/[{}],
140+
/*defaultImplementation=*/[{
141+
// By default, map over all dimensions.
142+
auto operandType = $_op->getOperand(operandIndex).getType();
143+
if (auto tensorTy = llvm::dyn_cast<mlir::ShapedType>(operandType)) {
144+
auto r = llvm::seq<int>(0, tensorTy.getRank());
145+
llvm::SmallVector<int, 4> v(r.begin(), r.end());
146+
return v;
147+
}
148+
return {};
149+
}]
150+
>,
151+
133152
// It may make sense some day to allow only a subset of op results to be
134153
// mapped, but for now we assume all results are tensors of the same shape
135154
// as the mapped operands.

lib/Dialect/LWE/IR/LWEOps.cpp

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "lib/Dialect/Polynomial/IR/PolynomialAttributes.h"
1111
#include "lib/Dialect/RNS/IR/RNSOps.h"
1212
#include "lib/Dialect/RNS/IR/RNSTypes.h"
13+
#include "llvm/include/llvm/ADT/Sequence.h" // from @llvm-project
1314
#include "llvm/include/llvm/ADT/TypeSwitch.h" // from @llvm-project
1415
#include "llvm/include/llvm/Support/ErrorHandling.h" // from @llvm-project
1516
#include "mlir/include/mlir/IR/BuiltinAttributes.h" // from @llvm-project
@@ -72,8 +73,12 @@ LogicalResult RMulRingEltOp::verify() {
7273
}
7374

7475
LogicalResult TrivialEncryptOp::verify() {
75-
auto plaintextSpace = this->getInput().getType().getPlaintextSpace();
76-
auto outPlaintextSpace = this->getOutput().getType().getPlaintextSpace();
76+
auto plaintextSpace =
77+
cast<LWEPlaintextType>(getElementTypeOrSelf(this->getInput().getType()))
78+
.getPlaintextSpace();
79+
auto outPlaintextSpace =
80+
cast<LWECiphertextType>(getElementTypeOrSelf(this->getOutput().getType()))
81+
.getPlaintextSpace();
7782

7883
if (plaintextSpace != outPlaintextSpace) {
7984
return this->emitOpError()
@@ -174,12 +179,8 @@ LogicalResult EncodeOp::verify() {
174179
}
175180

176181
LogicalResult RLWEEncodeOp::verify() {
177-
if (auto tensorTy = dyn_cast<ShapedType>(getInput().getType())) {
178-
if (tensorTy.getRank() > 1) {
179-
return emitOpError() << "RLWEEncodeOp only supports 1D tensors";
180-
}
181-
}
182-
return verifyEncodingAndTypeMatch(getInput().getType(), getEncoding());
182+
return verifyEncodingAndTypeMatch(getElementTypeOrSelf(getInput().getType()),
183+
getEncoding());
183184
}
184185

185186
LogicalResult RLWEDecodeOp::verify() {
@@ -325,6 +326,23 @@ void RMulPlainOp::getCanonicalizationPatterns(RewritePatternSet& results,
325326
results.add<lwe::PutCiphertextInFirstOperand<RMulPlainOp>>(context);
326327
}
327328

329+
bool RLWEEncodeOp::operandIsMappable(unsigned operandIndex) {
330+
// only `input`
331+
return operandIndex == 0;
332+
}
333+
334+
SmallVector<int> RLWEEncodeOp::mappedDimensionsForOperand(
335+
unsigned operandIndex) {
336+
auto operandType = getOperation()->getOperand(operandIndex).getType();
337+
if (auto tensorTy = dyn_cast<mlir::ShapedType>(operandType)) {
338+
// rank - 1 because the trailing dimension is the slot dimension.
339+
auto r = llvm::seq<int>(0, tensorTy.getRank() - 1);
340+
llvm::SmallVector<int, 4> v(r.begin(), r.end());
341+
return v;
342+
}
343+
return {};
344+
}
345+
328346
} // namespace lwe
329347
} // namespace heir
330348
} // namespace mlir

lib/Dialect/LWE/IR/LWEOps.td

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class HasEncoding<
2323
"the first arg's type's encoding matches the given encoding",
2424
CPred<
2525
comparator # "(" #
26-
"::llvm::cast<lwe::" # ty # ">($" # encodingHolder # ".getType()).getPlaintextSpace().getEncoding(), " #
26+
"::llvm::cast<lwe::" # ty # ">(getElementTypeOrSelf($" # encodingHolder # ".getType())).getPlaintextSpace().getEncoding(), " #
2727
"$" # encoding # ")"
2828
>
2929
>;
@@ -38,8 +38,8 @@ class EncodingsMatch<
3838
"the first arg's type's encoding matches the given encoding",
3939
CPred<
4040
comparator # "(" #
41-
"::llvm::cast<lwe::" # ty1 # ">($" # encodingHolder1 # ".getType()).getPlaintextSpace().getEncoding(), " #
42-
"::llvm::cast<lwe::" # ty2 # ">($" # encodingHolder2 # ".getType()).getPlaintextSpace().getEncoding())"
41+
"::llvm::cast<lwe::" # ty1 # ">(getElementTypeOrSelf($" # encodingHolder1 # ".getType())).getPlaintextSpace().getEncoding(), " #
42+
"::llvm::cast<lwe::" # ty2 # ">(getElementTypeOrSelf($" # encodingHolder2 # ".getType())).getPlaintextSpace().getEncoding())"
4343
>
4444
>;
4545

@@ -102,16 +102,14 @@ def LWE_EncodeOp : LWE_Op<"encode"> {
102102

103103
let results = (outs LWEPlaintext:$output);
104104
let assemblyFormat = "$input attr-dict `:` qualified(type($input)) `to` qualified(type($output))";
105-
106-
// Verify that the input type and the application data are compatible.
107105
let hasVerifier = 1;
108106
}
109107

110108
def LWE_TrivialEncryptOp: LWE_Op<"trivial_encrypt", [
111109
EncodingsMatch<"input", "LWEPlaintextType", "output", "LWECiphertextType">]> {
112110
let summary = "Create a trivial encryption of a plaintext.";
113-
let arguments = (ins LWEPlaintext:$input);
114-
let results = (outs LWECiphertext:$output);
111+
let arguments = (ins LWEPlaintextLike:$input);
112+
let results = (outs LWECiphertextLike:$output);
115113
let assemblyFormat = [{
116114
$input attr-dict `:` type(operands) `->` type(results)
117115
}];
@@ -183,20 +181,29 @@ def LWE_MulScalarOp : LWE_Op<"mul_scalar", [ElementwiseMappable,
183181
let hasFolder = 1;
184182
}
185183

186-
def LWE_RLWEEncodeOp : LWE_Op<"rlwe_encode", [HasEncoding<"output", "encoding", "LWEPlaintextType">]> {
184+
def LWE_RLWEEncodeOp : LWE_Op<"rlwe_encode", [
185+
HasEncoding<"output", "encoding", "LWEPlaintextType">,
186+
DeclareOpInterfaceMethods<ElementwiseByOperandOpInterface, [
187+
"operandIsMappable",
188+
// When encoding a tensor<3x1024xi16>, for example, the leading dimension
189+
// is mapped over and the trailing dimension is encoded into slots, so the
190+
// result type is tensor<3x!pt>.
191+
"mappedDimensionsForOperand",
192+
]>,
193+
]> {
187194
let summary = "Encode an integer to yield an RLWE plaintext";
188195
let description = [{
189-
Encode an integer to yield an RLWE plaintext.
196+
Encode a packed cleartext tensor to yield an RLWE plaintext.
190197

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

196203
Examples:
197204

198205
```
199-
%Y = lwe.rlwe_encode %value {encoding = #enc, ring = #ring}: i1 to !lwe.rlwe_plaintext<encoding = #enc, ring = #ring>
206+
%Y = lwe.rlwe_encode %value {encoding = #enc, ring = #ring}: i16 to !lwe.rlwe_plaintext<encoding = #enc, ring = #ring>
200207
```
201208
}];
202209

@@ -206,10 +213,8 @@ def LWE_RLWEEncodeOp : LWE_Op<"rlwe_encode", [HasEncoding<"output", "encoding",
206213
Polynomial_RingAttr:$ring
207214
);
208215

209-
let results = (outs LWEPlaintext:$output);
216+
let results = (outs LWEPlaintextLike:$output);
210217
let assemblyFormat = "$input attr-dict `:` qualified(type($input)) `->` qualified(type($output))";
211-
212-
// Verify that the input type and the encoding are compatible.
213218
let hasVerifier = true;
214219
}
215220

lib/Dialect/Secret/Conversions/Patterns.cpp

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "lib/Utils/ContextAwareTypeConversion.h"
1818
#include "llvm/include/llvm/ADT/STLExtras.h" // from @llvm-project
1919
#include "llvm/include/llvm/Support/Debug.h" // from @llvm-project
20+
#include "llvm/include/llvm/Support/DebugLog.h" // from @llvm-project
2021
#include "llvm/include/llvm/Support/FormatVariadic.h" // from @llvm-project
2122
#include "mlir/include/mlir/Dialect/Affine/IR/AffineOps.h" // from @llvm-project
2223
#include "mlir/include/mlir/Dialect/Arith/IR/Arith.h" // from @llvm-project
@@ -62,13 +63,47 @@ Value insertKeyArgument(func::FuncOp parentFunc, Type encryptionKeyType,
6263
return keyBlockArg;
6364
}
6465

66+
LogicalResult ConvertClientConceal::lowerToTrivialEncryption(
67+
secret::ConcealOp op, OpAdaptor adaptor,
68+
ContextAwareConversionPatternRewriter& rewriter) const {
69+
LDBG() << "Lowering conceal op as a trivial encryption";
70+
auto mgmtAttrResult = getTypeConverter()->getContextualAttr(op.getResult());
71+
if (failed(mgmtAttrResult)) {
72+
return rewriter.notifyMatchFailure(op, "found no mgmt attr");
73+
}
74+
Type resultTy = getTypeConverter()->convertType(op.getResult().getType(),
75+
mgmtAttrResult.value());
76+
auto ctTy = cast<lwe::LWECiphertextType>(getElementTypeOrSelf(resultTy));
77+
auto plaintextTy =
78+
lwe::LWEPlaintextType::get(op.getContext(), ctTy.getPlaintextSpace());
79+
80+
Type encodeOpResultTy = plaintextTy;
81+
if (auto resultTensorTy = cast<RankedTensorType>(resultTy)) {
82+
encodeOpResultTy =
83+
RankedTensorType::get(resultTensorTy.getShape(), plaintextTy);
84+
}
85+
86+
// Intentionally use op.getCleartext() because we don't want to type-convert
87+
// the input to a ciphertext.
88+
auto encoded = lwe::RLWEEncodeOp::create(
89+
rewriter, op.getLoc(), encodeOpResultTy, op.getCleartext(),
90+
ctTy.getPlaintextSpace().getEncoding(),
91+
ctTy.getPlaintextSpace().getRing());
92+
auto newOp =
93+
lwe::TrivialEncryptOp::create(rewriter, op.getLoc(), resultTy, encoded);
94+
newOp->setAttrs(op->getAttrs());
95+
rewriter.replaceOp(op, newOp);
96+
return success();
97+
}
98+
6599
LogicalResult ConvertClientConceal::matchAndRewrite(
66100
secret::ConcealOp op, OpAdaptor adaptor,
67101
ContextAwareConversionPatternRewriter& rewriter) const {
68102
func::FuncOp parentFunc = op->getParentOfType<func::FuncOp>();
69-
if (!parentFunc || !parentFunc->hasAttr(kClientEncFuncAttrName)) {
70-
return op->emitError() << "expected to be inside a function with attribute "
71-
<< kClientEncFuncAttrName;
103+
if (!parentFunc) return failure();
104+
105+
if (!isClientHelper(parentFunc)) {
106+
return lowerToTrivialEncryption(op, adaptor, rewriter);
72107
}
73108

74109
// The encryption func encrypts a single value, so it must have a single

lib/Dialect/Secret/Conversions/Patterns.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ struct ConvertClientConceal
3939
ContextAwareConversionPatternRewriter& rewriter) const override;
4040

4141
private:
42+
LogicalResult lowerToTrivialEncryption(
43+
secret::ConcealOp op, OpAdaptor adaptor,
44+
ContextAwareConversionPatternRewriter& rewriter) const;
45+
4246
bool usePublicKey;
4347
polynomial::RingAttr ring;
4448
};

0 commit comments

Comments
 (0)