Skip to content

Commit b72a6a1

Browse files
committed
ilp-bootstrap-placement: level-dependent cost model and solver hygiene
1 parent 75bdbf3 commit b72a6a1

11 files changed

Lines changed: 343 additions & 77 deletions

File tree

lib/Analysis/ILPBootstrapPlacementAnalysis/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ cc_library(
1616
"@heir//lib/Analysis/SecretnessAnalysis",
1717
"@heir//lib/Dialect/Mgmt/IR:Dialect",
1818
"@heir//lib/Dialect/Secret/IR:Dialect",
19+
"@heir//lib/Dialect/TensorExt/IR:Dialect",
1920
"@llvm-project//llvm:Support",
2021
"@llvm-project//mlir:Analysis",
2122
"@llvm-project//mlir:ArithDialect",

lib/Analysis/ILPBootstrapPlacementAnalysis/ILPBootstrapPlacementAnalysis.cpp

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "lib/Analysis/ILPBootstrapPlacementAnalysis/ILPBootstrapPlacementAnalysis.h"
22

33
#include <cmath>
4+
#include <optional>
45
#include <sstream>
56
#include <string>
67
#include <utility>
@@ -9,6 +10,7 @@
910
#include "lib/Analysis/SecretnessAnalysis/SecretnessAnalysis.h"
1011
#include "lib/Dialect/Mgmt/IR/MgmtAttributes.h"
1112
#include "lib/Dialect/Secret/IR/SecretOps.h"
13+
#include "lib/Dialect/TensorExt/IR/TensorExtOps.h"
1214
#include "llvm/include/llvm/ADT/DenseMap.h" // from @llvm-project
1315
#include "llvm/include/llvm/ADT/STLExtras.h" // from @llvm-project
1416
#include "llvm/include/llvm/ADT/SmallVector.h" // from @llvm-project
@@ -53,11 +55,11 @@
5355
// plaintext constants contribute Sw
5456
// * node transitions relate each op's input state to each result state by
5557
// either direct rescale/modswitch management or a bootstrap transition
56-
// * yielded result scales are constrained to explicit nonzero mgmt.mgmt
57-
// scales
58-
// on corresponding secret.generic results
59-
// - Objective: minimize bootstrap and rescale costs, with a small tie-breaker
60-
// favoring higher remaining levels.
58+
// * a yielded value is pinned to the level annotated on its secret.generic
59+
// result by an mgmt.mgmt attr, and to the annotated scale when nonzero
60+
// - Objective: minimize total bootstrap and rescale cost. With a per-level cost
61+
// model, also charge each tracked op its latency at its input level. A tiny
62+
// per-level term breaks ties toward higher levels for output values.
6163

6264
namespace math_opt = ::operations_research::math_opt;
6365

@@ -72,10 +74,29 @@ static bool isMultiplication(Operation* op) {
7274
return isa<arith::MulFOp>(op) || isa<arith::MulIOp>(op);
7375
}
7476

77+
static bool isAdditionLike(Operation* op) {
78+
return isa<arith::AddFOp, arith::AddIOp, arith::SubFOp, arith::SubIOp>(op);
79+
}
80+
7581
static bool isConstantLike(Value value) {
7682
return value.getDefiningOp<arith::ConstantOp>() != nullptr;
7783
}
7884

85+
// The level-dependent latency term for one op, or nullopt if the op class has
86+
// no level-dependent cost.
87+
static std::optional<LinearCost> levelCostForOp(Operation* op,
88+
DataFlowSolver* solver,
89+
const OpCostModel& costModel) {
90+
bool ctCt = llvm::count_if(op->getOperands(), [&](auto opd) {
91+
return isSecret(opd, solver);
92+
}) >= 2;
93+
if (isMultiplication(op)) return ctCt ? costModel.mulCtCt : costModel.mulCtPt;
94+
if (isAdditionLike(op)) return ctCt ? costModel.addCtCt : costModel.addCtPt;
95+
if (isa<arith::NegFOp>(op)) return costModel.negate;
96+
if (isa<tensor_ext::RotateOp>(op)) return costModel.rotate;
97+
return std::nullopt;
98+
}
99+
79100
static int roundedValue(const math_opt::VariableMap<double>& varMap,
80101
const math_opt::Variable& var) {
81102
return static_cast<int>(std::round(varMap.at(var)));
@@ -331,23 +352,37 @@ static void addOperandEdgeConstraints(ILPModelState& state) {
331352
}
332353
}
333354

334-
// Add output-boundary scale constraints for values yielded from secret.generic.
335-
// A yield is constrained only when the corresponding generic result has an
336-
// explicit nonzero mgmt.mgmt scale; otherwise the ILP may choose any supported
337-
// result scale and the later annotation pass records that chosen state.
355+
// Add output-boundary constraints for values yielded from secret.generic.
356+
// When the corresponding generic result carries an explicit mgmt.mgmt attr,
357+
// the yielded value's level is pinned to the annotated level, and (in CKKS
358+
// mode) a nonzero annotated scale pins the yielded value's scale.
338359
static LogicalResult addYieldConstraints(ILPModelState& state) {
339360
auto genericOp = cast<secret::GenericOp>(state.body->getParentOp());
340361
for (Operation& op : state.body->getOperations()) {
341362
auto yieldOp = dyn_cast<secret::YieldOp>(op);
342363
if (!yieldOp) continue;
343364
for (auto [index, operand] : llvm::enumerate(yieldOp->getOperands())) {
344365
if (!isSecret(operand, state.solver)) continue;
345-
if (!state.valueScaleVars.contains(operand)) continue;
346-
if (state.levelOnly) continue;
366+
if (!state.valueLevelVars.contains(operand)) continue;
347367

348368
mgmt::MgmtAttr mgmtAttr =
349369
mgmt::findMgmtAttrAssociatedWith(genericOp.getResult(index));
350-
if (!mgmtAttr || mgmtAttr.getScale() == 0) continue;
370+
if (!mgmtAttr) continue;
371+
372+
int resultLevel = mgmtAttr.getLevel();
373+
if (resultLevel < 0 || resultLevel > state.bootstrapWaterline) {
374+
genericOp->emitError()
375+
<< "cannot constrain yielded value " << index
376+
<< " from secret.generic result mgmt.mgmt level " << resultLevel
377+
<< "; expected level in [0, " << state.bootstrapWaterline << "]";
378+
return failure();
379+
}
380+
state.model.AddLinearConstraint(
381+
state.valueLevelVars.at(operand) == resultLevel,
382+
"yieldResultLevel" + std::to_string(index));
383+
384+
if (state.levelOnly || mgmtAttr.getScale() == 0) continue;
385+
if (!state.valueScaleVars.contains(operand)) continue;
351386

352387
int resultScale = mgmtAttr.getScale();
353388
if (resultScale < state.sw || resultScale > state.scaleMax) {
@@ -419,24 +454,33 @@ static void addNodeTransitionConstraints(ILPModelState& state,
419454
}
420455
}
421456

422-
static void addObjective(ILPModelState& state, int bootstrapCost,
423-
int rescaleCost) {
457+
static void addObjective(ILPModelState& state, const OpCostModel& costModel) {
424458
math_opt::LinearExpression objective;
425459
for (auto& [op, bootstrapVar] : state.bootstrapVars) {
426-
objective += bootstrapCost * bootstrapVar;
460+
objective += costModel.bootstrapCost * bootstrapVar;
427461
}
428462
for (auto& [op, rescaleVar] : state.nodeRescaleVars) {
429-
objective += rescaleCost * rescaleVar;
463+
objective += costModel.rescaleCost * rescaleVar;
430464
}
431465
for (auto& [operand, rescaleVar] : state.edgeRescaleVars) {
432-
objective += rescaleCost * rescaleVar;
466+
objective += costModel.rescaleCost * rescaleVar;
433467
}
434-
// Tie-breaker: level constraints are one-sided, so among equal-cost
435-
// solutions the solver could pick gratuitously low levels (free modswitches).
436-
// The small negative weight prefers the highest feasible level for each
437-
// value without outweighing a unit of bootstrap/rescale cost. TODO: remove
438-
// in the next iteration, when the objective minimizes performance cost and
439-
// per-level operation costs make level choices matter directly.
468+
// Level-dependent op latency: each tracked op is charged
469+
// slope * inputLevel + intercept for its cost class, so the solver prefers
470+
// to run expensive ops (muls, rotations) at low levels.
471+
if (costModel.hasLevelCosts) {
472+
for (Operation* op : state.trackedOps) {
473+
auto cost = levelCostForOp(op, state.solver, costModel);
474+
if (!cost.has_value()) continue;
475+
objective += cost->slope * state.inputLevelVars.at(op) + cost->intercept;
476+
}
477+
}
478+
// Tie-breaker on value (result) levels: level constraints are one-sided, so
479+
// among equal-cost solutions the solver could pick gratuitously low levels
480+
// (free modswitches decoded as spurious level_reduce ops). The small
481+
// negative weight prefers the highest feasible level for each value. Op
482+
// *input* levels are separate variables and get real downward pressure from
483+
// the level-dependent latency terms above, so the two do not conflict.
440484
for (auto& [value, levelVar] : state.valueLevelVars) {
441485
objective += -0.001 * levelVar;
442486
}
@@ -497,16 +541,26 @@ LogicalResult ILPBootstrapPlacementAnalysis::solve() {
497541
addOperandEdgeConstraints(state);
498542
if (failed(addYieldConstraints(state))) return failure();
499543
addNodeTransitionConstraints(state, bootstrapLevelLowerBound);
500-
addObjective(state, bootstrapCost, rescaleCost);
544+
addObjective(state, costModel);
501545

502546
LLVM_DEBUG({
503547
std::stringstream ss;
504548
ss << state.model;
505549
llvm::dbgs() << "--- ILP model ---\n" << ss.str() << "--- end model ---\n";
506550
});
507551

552+
// Solve to a 1% relative optimality gap, matching Orbit's solver
553+
// configuration (Gurobi MIPGap / CBC gapRel = 0.01). Proving full optimality
554+
// often dominates solve time on large instances while improving the
555+
// objective by less than measurement noise in the profiled cost models. On
556+
// small instances the solver typically closes the gap entirely, so this
557+
// rarely changes the chosen placement.
558+
constexpr double kRelativeMipGap = 0.01;
559+
math_opt::SolveArguments solveArgs;
560+
solveArgs.parameters.relative_gap_tolerance = kRelativeMipGap;
561+
508562
const absl::StatusOr<math_opt::SolveResult> status =
509-
math_opt::Solve(state.model, math_opt::SolverType::kGscip);
563+
math_opt::Solve(state.model, math_opt::SolverType::kGscip, solveArgs);
510564
if (!status.ok()) {
511565
std::stringstream ss;
512566
ss << "Error solving the problem: " << status.status() << "\n";
@@ -520,8 +574,8 @@ LogicalResult ILPBootstrapPlacementAnalysis::solve() {
520574
case math_opt::TerminationReason::kFeasible:
521575
break;
522576
default:
523-
llvm::errs() << "The problem does not have a feasible solution. "
524-
"Termination status code: "
577+
llvm::errs() << "No feasible solution found (the problem may be "
578+
"infeasible). Termination status code: "
525579
<< static_cast<int>(result.termination.reason) << "\n";
526580
return failure();
527581
}

lib/Analysis/ILPBootstrapPlacementAnalysis/ILPBootstrapPlacementAnalysis.h

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ class raw_ostream;
1414

1515
namespace mlir {
1616
namespace heir {
17+
18+
// A latency model of the form cost(level) = slope * level + intercept,
19+
// fitted from a per-level latency table.
20+
struct LinearCost {
21+
double slope = 0.0;
22+
double intercept = 0.0;
23+
};
24+
25+
// Costs used by the ILP objective. Bootstrap and rescale management decisions
26+
// are charged constant costs. When hasLevelCosts is set, each tracked op is
27+
// additionally charged a level-dependent latency at its input level,
28+
// distinguishing ciphertext-ciphertext (CtCt) from ciphertext-plaintext (CtPt)
29+
// operands.
30+
struct OpCostModel {
31+
double bootstrapCost = 0.0;
32+
double rescaleCost = 0.0;
33+
bool hasLevelCosts = false;
34+
LinearCost addCtCt;
35+
LinearCost addCtPt;
36+
LinearCost mulCtCt;
37+
LinearCost mulCtPt;
38+
LinearCost rotate;
39+
LinearCost negate;
40+
};
41+
1742
class ILPBootstrapPlacementAnalysis {
1843
public:
1944
enum class ScaleMode { kCKKS, kLevelOnly };
@@ -39,16 +64,16 @@ class ILPBootstrapPlacementAnalysis {
3964
ILPBootstrapPlacementAnalysis(Operation* op, DataFlowSolver* solver,
4065
int bootstrapWaterline, int scaleWaterline,
4166
int scaleFactorBits,
42-
int bootstrapLevelLowerBound, int bootstrapCost,
43-
int rescaleCost, ScaleMode scaleMode)
67+
int bootstrapLevelLowerBound,
68+
const OpCostModel& costModel,
69+
ScaleMode scaleMode)
4470
: opToRunOn(op),
4571
solver(solver),
4672
bootstrapWaterline(bootstrapWaterline),
4773
scaleWaterline(scaleWaterline),
4874
scaleFactorBits(scaleFactorBits),
4975
bootstrapLevelLowerBound(bootstrapLevelLowerBound),
50-
bootstrapCost(bootstrapCost),
51-
rescaleCost(rescaleCost),
76+
costModel(costModel),
5277
scaleMode(scaleMode) {}
5378
~ILPBootstrapPlacementAnalysis() = default;
5479

@@ -86,8 +111,7 @@ class ILPBootstrapPlacementAnalysis {
86111
int scaleWaterline;
87112
int scaleFactorBits;
88113
int bootstrapLevelLowerBound;
89-
int bootstrapCost;
90-
int rescaleCost;
114+
OpCostModel costModel;
91115
ScaleMode scaleMode;
92116
llvm::DenseMap<Operation*, bool> solution;
93117
llvm::DenseMap<Value, int> solutionLevelBeforeBootstrap;

0 commit comments

Comments
 (0)