11#include " lib/Analysis/ILPBootstrapPlacementAnalysis/ILPBootstrapPlacementAnalysis.h"
22
33#include < cmath>
4+ #include < optional>
45#include < sstream>
56#include < string>
67#include < utility>
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
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
6264namespace 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+
7581static 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+
79100static 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 .
338359static 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 }
0 commit comments