|
10 | 10 | #include "lib/Dialect/Preprocessing/IR/PreprocessingOps.h" |
11 | 11 | #include "lib/Dialect/Preprocessing/IR/PreprocessingTypes.h" |
12 | 12 | #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 |
15 | 17 | #include "mlir/include/mlir/Dialect/Arith/IR/Arith.h" // from @llvm-project |
16 | 18 | #include "mlir/include/mlir/Dialect/Func/IR/FuncOps.h" // from @llvm-project |
17 | 19 | #include "mlir/include/mlir/Dialect/Tensor/IR/Tensor.h" // from @llvm-project |
|
27 | 29 | #include "mlir/include/mlir/IR/MLIRContext.h" // from @llvm-project |
28 | 30 | #include "mlir/include/mlir/IR/OpDefinition.h" // from @llvm-project |
29 | 31 | #include "mlir/include/mlir/IR/Operation.h" // from @llvm-project |
| 32 | +#include "mlir/include/mlir/IR/PatternMatch.h" // from @llvm-project |
30 | 33 | #include "mlir/include/mlir/IR/Region.h" // from @llvm-project |
31 | 34 | #include "mlir/include/mlir/IR/TypeUtilities.h" // from @llvm-project |
32 | 35 | #include "mlir/include/mlir/IR/Types.h" // from @llvm-project |
@@ -119,13 +122,106 @@ static bool isAllowedPlaintextType(Type type) { |
119 | 122 | return false; |
120 | 123 | } |
121 | 124 |
|
| 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 | + |
122 | 205 | struct SplitPreprocessingPass |
123 | 206 | : impl::SplitPreprocessingBase<SplitPreprocessingPass> { |
124 | 207 | using SplitPreprocessingBase::SplitPreprocessingBase; |
125 | 208 |
|
126 | 209 | void runOnOperation() override { |
127 | 210 | Operation* root = getOperation(); |
128 | 211 |
|
| 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 | + |
129 | 225 | // Annotate each encode op with a stable site id |
130 | 226 | int32_t encodeId = 0; |
131 | 227 | root->walk([&](PlaintextEncodeOpInterface op) { |
@@ -180,6 +276,15 @@ struct SplitPreprocessingPass |
180 | 276 | (void)runPipeline(pipeline, preprocessingFuncOp); |
181 | 277 | (void)runPipeline(pipeline, preprocessedFuncOp); |
182 | 278 | (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); |
183 | 288 | } |
184 | 289 |
|
185 | 290 | void updateOriginalFunc(FuncOp funcOp, FuncOp preprocessingFuncOp, |
|
0 commit comments