Skip to content

Commit 0f1bf99

Browse files
Merge pull request #3327 from google:alex/affine-fix
PiperOrigin-RevId: 962877358
2 parents eb1161f + 6630c6a commit 0f1bf99

4 files changed

Lines changed: 182 additions & 2 deletions

File tree

lib/Transforms/SplitPreprocessing/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ cc_library(
1818
"@heir//lib/Dialect/Preprocessing/IR:Dialect",
1919
"@heir//lib/Utils:AttributeUtils",
2020
"@llvm-project//llvm:Support",
21+
"@llvm-project//mlir:AffineDialect",
22+
"@llvm-project//mlir:AffineTransforms",
2123
"@llvm-project//mlir:ArithDialect",
2224
"@llvm-project//mlir:FuncDialect",
2325
"@llvm-project//mlir:IR",

lib/Transforms/SplitPreprocessing/SplitPreprocessing.cpp

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
#include "lib/Dialect/Preprocessing/IR/PreprocessingOps.h"
1111
#include "lib/Dialect/Preprocessing/IR/PreprocessingTypes.h"
1212
#include "lib/Utils/AttributeUtils.h"
13-
#include "llvm/include/llvm/ADT/STLExtras.h" // from @llvm-project
14-
#include "llvm/include/llvm/ADT/SmallVector.h" // from @llvm-project
13+
#include "llvm/include/llvm/ADT/STLExtras.h" // from @llvm-project
14+
#include "llvm/include/llvm/ADT/SmallVector.h" // from @llvm-project
15+
#include "mlir/include/mlir/Dialect/Affine/IR/AffineOps.h" // from @llvm-project
16+
#include "mlir/include/mlir/Dialect/Affine/Transforms/Passes.h" // from @llvm-project
1517
#include "mlir/include/mlir/Dialect/Arith/IR/Arith.h" // from @llvm-project
1618
#include "mlir/include/mlir/Dialect/Func/IR/FuncOps.h" // from @llvm-project
1719
#include "mlir/include/mlir/Dialect/Tensor/IR/Tensor.h" // from @llvm-project
@@ -27,6 +29,7 @@
2729
#include "mlir/include/mlir/IR/MLIRContext.h" // from @llvm-project
2830
#include "mlir/include/mlir/IR/OpDefinition.h" // from @llvm-project
2931
#include "mlir/include/mlir/IR/Operation.h" // from @llvm-project
32+
#include "mlir/include/mlir/IR/PatternMatch.h" // from @llvm-project
3033
#include "mlir/include/mlir/IR/Region.h" // from @llvm-project
3134
#include "mlir/include/mlir/IR/TypeUtilities.h" // from @llvm-project
3235
#include "mlir/include/mlir/IR/Types.h" // from @llvm-project
@@ -119,13 +122,106 @@ static bool isAllowedPlaintextType(Type type) {
119122
return false;
120123
}
121124

125+
// Rebuild each affine.for in `funcOp` without the iter_args whose region
126+
// argument and loop result are both unused.
127+
//
128+
// When cloning a loop into the preprocessing function we leave behind a dummy
129+
// initializer for any loop-carried value that isn't part of the plaintext
130+
// slice (e.g. a ciphertext accumulator), relying on remove-dead-values to drop
131+
// the now-unused iter_arg. That works for scf.for, but remove-dead-values only
132+
// poisons (it does not structurally remove) dead affine.for iter_args, so a
133+
// ub.poison-typed loop-carried value would otherwise survive into backend
134+
// lowering, where it cannot be converted or emitted. This performs the removal
135+
// remove-dead-values cannot, after which the dummy initializers become dead and
136+
// are cleaned up normally.
137+
static void removeDeadAffineForIterArgs(func::FuncOp funcOp) {
138+
IRRewriter rewriter(funcOp.getContext());
139+
140+
SmallVector<affine::AffineForOp> loops;
141+
funcOp.walk([&](affine::AffineForOp forOp) { loops.push_back(forOp); });
142+
143+
for (affine::AffineForOp forOp : loops) {
144+
unsigned numIterArgs = forOp.getNumIterOperands();
145+
if (numIterArgs == 0) continue;
146+
147+
SmallVector<unsigned> keptIndices;
148+
for (unsigned i = 0; i < numIterArgs; ++i) {
149+
if (!forOp.getRegionIterArgs()[i].use_empty() ||
150+
!forOp.getResult(i).use_empty()) {
151+
keptIndices.push_back(i);
152+
}
153+
}
154+
if (keptIndices.size() == numIterArgs) continue; // nothing dead
155+
156+
rewriter.setInsertionPoint(forOp);
157+
SmallVector<Value> keptInits;
158+
for (unsigned i : keptIndices) keptInits.push_back(forOp.getInits()[i]);
159+
160+
auto newLoop = affine::AffineForOp::create(
161+
rewriter, forOp.getLoc(), forOp.getLowerBoundOperands(),
162+
forOp.getLowerBoundMap(), forOp.getUpperBoundOperands(),
163+
forOp.getUpperBoundMap(), forOp.getStepAsInt(), keptInits);
164+
165+
// Trim the existing terminator down to the kept loop-carried values.
166+
auto yieldOp =
167+
cast<affine::AffineYieldOp>(forOp.getBody()->getTerminator());
168+
SmallVector<Value> keptYields;
169+
for (unsigned i : keptIndices) keptYields.push_back(yieldOp.getOperand(i));
170+
rewriter.modifyOpInPlace(
171+
yieldOp, [&]() { yieldOp.getOperandsMutable().assign(keptYields); });
172+
173+
// With no kept iter_args the builder added a default terminator; drop it so
174+
// the merged (trimmed) affine.yield is the loop's only terminator.
175+
if (keptInits.empty()) {
176+
rewriter.eraseOp(newLoop.getBody()->getTerminator());
177+
}
178+
179+
// Map the old block arguments onto the new loop: induction var, then each
180+
// iter_arg. Kept ones map to the new region args; dead ones are unused, so
181+
// their (type-matched, dominating) original initializer is a safe
182+
// placeholder that is never actually referenced.
183+
SmallVector<Value> blockArgReplacements;
184+
blockArgReplacements.push_back(newLoop.getInductionVar());
185+
unsigned keptCursor = 0;
186+
for (unsigned i = 0; i < numIterArgs; ++i) {
187+
if (keptCursor < keptIndices.size() && keptIndices[keptCursor] == i) {
188+
blockArgReplacements.push_back(
189+
newLoop.getRegionIterArgs()[keptCursor++]);
190+
} else {
191+
blockArgReplacements.push_back(forOp.getInits()[i]);
192+
}
193+
}
194+
rewriter.mergeBlocks(forOp.getBody(), newLoop.getBody(),
195+
blockArgReplacements);
196+
197+
for (auto [newIdx, oldIdx] : llvm::enumerate(keptIndices)) {
198+
rewriter.replaceAllUsesWith(forOp.getResult(oldIdx),
199+
newLoop.getResult(newIdx));
200+
}
201+
rewriter.eraseOp(forOp);
202+
}
203+
}
204+
122205
struct SplitPreprocessingPass
123206
: impl::SplitPreprocessingBase<SplitPreprocessingPass> {
124207
using SplitPreprocessingBase::SplitPreprocessingBase;
125208

126209
void runOnOperation() override {
127210
Operation* root = getOperation();
128211

212+
// The storage layout sizes each site by the enclosing loops' trip counts,
213+
// while the store/load indices are those loops' induction variables.
214+
// Normalize affine loops before capturing those indices so a later loop
215+
// normalization cannot rewrite a captured index to an affine.apply of the
216+
// original non-zero-based, non-unit-step induction variable.
217+
OpPassManager normalizeLoops("builtin.module");
218+
normalizeLoops.addNestedPass<func::FuncOp>(
219+
affine::createAffineLoopNormalizePass(true));
220+
if (failed(runPipeline(normalizeLoops, root))) {
221+
signalPassFailure();
222+
return;
223+
}
224+
129225
// Annotate each encode op with a stable site id
130226
int32_t encodeId = 0;
131227
root->walk([&](PlaintextEncodeOpInterface op) {
@@ -180,6 +276,15 @@ struct SplitPreprocessingPass
180276
(void)runPipeline(pipeline, preprocessingFuncOp);
181277
(void)runPipeline(pipeline, preprocessedFuncOp);
182278
(void)runPipeline(pipeline, funcOp);
279+
280+
// remove-dead-values poisons but cannot structurally strip a dead
281+
// affine.for iter_arg (it only does so for scf.for), so the dummy
282+
// ciphertext iter_arg left when cloning a loop survives as a ub.poison
283+
// loop-carried value that later backend lowering can neither convert nor
284+
// emit. Strip those dead iter_args now, then re-run the cleanup so the
285+
// orphaned ub.poison initializers are removed too.
286+
removeDeadAffineForIterArgs(preprocessingFuncOp);
287+
(void)runPipeline(pipeline, preprocessingFuncOp);
183288
}
184289

185290
void updateOriginalFunc(FuncOp funcOp, FuncOp preprocessingFuncOp,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// RUN: heir-opt %s --split-preprocessing | FileCheck %s
2+
3+
// A loop carries a ciphertext iter_arg (the running accumulator) and also
4+
// encodes a plaintext per iteration. The ciphertext value is not part of the
5+
// plaintext slice, so when the loop is cloned into the preprocessing function
6+
// its iter_arg becomes dead. remove-dead-values only poisons (it cannot
7+
// structurally strip) a dead affine.for iter_arg, so split-preprocessing must
8+
// remove it itself -- otherwise a ub.poison-typed loop-carried value survives
9+
// and later backend lowering can neither convert nor emit it.
10+
11+
!Z36028797017456641_i64 = !mod_arith.int<36028797017456641 : i64>
12+
!Z35184371138561_i64 = !mod_arith.int<35184371138561 : i64>
13+
!Z35184372121601_i64 = !mod_arith.int<35184372121601 : i64>
14+
#inverse_canonical_encoding = #lwe.inverse_canonical_encoding<scaling_factor = 0>
15+
#key = #lwe.key<>
16+
#ring_f64_1_x1024 = #polynomial.ring<coefficientType = f64, polynomialModulus = <1 + x**1024>>
17+
!rns_L2 = !rns.rns<!Z36028797017456641_i64, !Z35184371138561_i64, !Z35184372121601_i64>
18+
!pt = !lwe.lwe_plaintext<plaintext_space = <ring = #ring_f64_1_x1024, encoding = #inverse_canonical_encoding>>
19+
#ring_rns_L2_1_x1024 = #polynomial.ring<coefficientType = !rns_L2, polynomialModulus = <1 + x**1024>>
20+
#ciphertext_space_L2 = #lwe.ciphertext_space<ring = #ring_rns_L2_1_x1024, encryption_type = mix>
21+
!ct_L2 = !lwe.lwe_ciphertext<plaintext_space = <ring = #ring_f64_1_x1024, encoding = #inverse_canonical_encoding>, ciphertext_space = #ciphertext_space_L2, key = #key, modulus_chain = #lwe.modulus_chain<elements = <36028797017456641 : i64, 35184371138561 : i64, 35184372121601 : i64>, current = 2>>
22+
23+
// The preprocessing loop must be a pure store loop: no iter_args, no ub.poison.
24+
// CHECK: func.func @f__preprocessing() -> !preprocessing.storage<!pt>
25+
// CHECK-NOT: ub.poison
26+
// CHECK: affine.for %[[I:.*]] = 0 to 4 {
27+
// CHECK-NOT: iter_args
28+
// CHECK: %[[PT:.*]] = lwe.rlwe_encode
29+
// CHECK: preprocessing.store %[[PT]], %{{.*}}[%[[I]]] site 0<!pt> : !pt, <!pt>
30+
// CHECK: return
31+
32+
module attributes {backend.openfhe, ckks.schemeParam = #ckks.scheme_param<logN = 14, Q = [36028797017456641, 35184371138561, 35184372121601], P = [1152921504607338497, 1152921504608747521], logDefaultScale = 45>, scheme.ckks} {
33+
func.func @f(%arg0: tensor<1x!ct_L2>) -> tensor<1x!ct_L2> {
34+
%cst = arith.constant dense<1.0> : tensor<1024xf32>
35+
%0 = affine.for %i = 0 to 4 iter_args(%sum = %arg0) -> (tensor<1x!ct_L2>) {
36+
%pt = lwe.rlwe_encode %cst {encoding = #inverse_canonical_encoding, ring = #ring_f64_1_x1024} : tensor<1024xf32> -> !pt
37+
%from = tensor.from_elements %pt : tensor<1x!pt>
38+
%1 = ckks.add_plain %sum, %from : (tensor<1x!ct_L2>, tensor<1x!pt>) -> tensor<1x!ct_L2>
39+
affine.yield %1 : tensor<1x!ct_L2>
40+
}
41+
return %0 : tensor<1x!ct_L2>
42+
}
43+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// RUN: heir-opt %s --split-preprocessing --affine-loop-normalize | FileCheck %s
2+
3+
// split-preprocessing uses enclosing loop induction variables as storage
4+
// indices. A non-zero lower bound or non-unit step must therefore be normalized
5+
// before splitting, so the storage is indexed by iteration number rather than
6+
// by the original induction variable values.
7+
8+
!Z36028797017456641_i64 = !mod_arith.int<36028797017456641 : i64>
9+
!Z35184371138561_i64 = !mod_arith.int<35184371138561 : i64>
10+
!Z35184372121601_i64 = !mod_arith.int<35184372121601 : i64>
11+
#inverse_canonical_encoding = #lwe.inverse_canonical_encoding<scaling_factor = 0>
12+
#ring_f64_1_x1024 = #polynomial.ring<coefficientType = f64, polynomialModulus = <1 + x**1024>>
13+
!rns_L2 = !rns.rns<!Z36028797017456641_i64, !Z35184371138561_i64, !Z35184372121601_i64>
14+
!pt = !lwe.lwe_plaintext<plaintext_space = <ring = #ring_f64_1_x1024, encoding = #inverse_canonical_encoding>>
15+
16+
// CHECK: func.func @noncanonical_affine_loop__preprocessing
17+
// CHECK: affine.for %[[I:.*]] = 0 to 2 {
18+
// CHECK-NOT: affine.apply
19+
// CHECK: preprocessing.store %{{.*}}, %{{.*}}[%[[I]]] site 0
20+
// CHECK: return
21+
22+
module attributes {backend.openfhe, ckks.schemeParam = #ckks.scheme_param<logN = 14, Q = [36028797017456641, 35184371138561, 35184372121601], P = [1152921504607338497, 1152921504608747521], logDefaultScale = 45>, scheme.ckks} {
23+
func.func @noncanonical_affine_loop() {
24+
%cst = arith.constant dense<1.0> : tensor<1024xf32>
25+
affine.for %i = 1 to 7 step 3 {
26+
%pt = lwe.rlwe_encode %cst {encoding = #inverse_canonical_encoding, ring = #ring_f64_1_x1024} : tensor<1024xf32> -> !pt
27+
}
28+
return
29+
}
30+
}

0 commit comments

Comments
 (0)