heir icon indicating copy to clipboard operation
heir copied to clipboard

[Draft] Scheme selection passes

Open kragall opened this issue 5 months ago • 11 comments

This is a draft of how scheme selection could be handled with HEIR on a very abstract level. The idea is that this could be helpful to non-expert users and could be a stepping stone towards dealing with scheme switching, see the discussion in #2019.

kragall avatar Jul 22 '25 11:07 kragall

I think the core question here is really what the cost model needs to look like. I think the current bool/bit/arith ops counter approach is too simplistic to be useful, yet at the same time I don't think we need some super complex model based on lots of benchmarks/etc.

I think the hardest part is to find some way to track roughly what kind of mult-depth/bootstrapping obligation a program induces (when viewed as an arithmetic problem). Accounting for the cost of basic arithmetic (considering bitwidth, as mentioned by Asra) should be relatively straightfoward, especially if we make some simplifying assumptions on roughly how expensive a matmul/etc will be (which does depend on the kernel, but I don't think the cost model needs to be all that accurate).

Since really, the main question for HEIR right now is "do we do binarization or do we do arithmetization", we can probably just track two accountings of the costs, one assuming we went for CGGI and one assuming we went for CKKS/BGV/BFV and ignore the finer points inside those approaches until HEIR actually supports, e.g, circuit bootstrapped CGGI or interpolation-based airhtmetization for BFV/BGV.

Btw, I don't think its very important for the cost model to be "correct" at the moment, it just needs to be reasonable, i.e., don't pick CGGI for a program with 12 64-bit matmuls just because it sees one comparison at the end, or pick CKKS with N=64k and 1000-bit modulus for a program that only operates on like 4 small inputs in total, just because it's all add/mul ops.

AlexanderViand avatar Jul 24 '25 00:07 AlexanderViand

@kragall I see the "PR format requirements" failed - you should follow the steps at https://heir.dev/docs/development/ and run pre-commit run --all-files to clean those up, and probably take a look at https://heir.dev/docs/development/ide/ to make sure your IDE formats things correctly automatically.

AlexanderViand avatar Jul 24 '25 00:07 AlexanderViand

Sorry, forgot about pre-commit when I switched to my new laptop. Now it's reinstalled and the checks should succeed

kragall avatar Aug 08 '25 14:08 kragall

The current SchemeInfoAnalysis annotates the func operation with the different types of ops that occur inside the function. The SchemeSelectionAnalysis is currently empty, because I can't get it to visit the func op.

You can run both analysis by using --annotate-scheme-info, e.g. on tests/Examples/common/dot_product_8.mlir . If you use the --debug --debug-only=SchemeSelection flags, the SchemeSelectionAnalysis prints out all ops that it visits.

kragall avatar Aug 08 '25 15:08 kragall

The current SchemeInfoAnalysis annotates the func operation with the different types of ops that occur inside the function. The SchemeSelectionAnalysis is currently empty, because I can't get it to visit the func op.

You can run both analysis by using --annotate-scheme-info, e.g. on tests/Examples/common/dot_product_8.mlir . If you use the --debug --debug-only=SchemeSelection flags, the SchemeSelectionAnalysis prints out all ops that it visits.

There must be some unpushed changes because heir-opt doesn't build on your branch. It's missing a BUILD file in lib/Analysis/SchemeInfoAnalysis

j2kun avatar Aug 08 '25 21:08 j2kun

Adding the build file and otherwise it seems to work for me:

$ bazel run //tools:heir-opt -- --annotate-scheme-info --debug --debug-only=SchemeInfo $PWD/tests/Examples/common/dot_product_8.mlir
Visiting: arith.constant. Visiting: arith.constant. Visiting: tensor.extract. Visiting: tensor.extract. Visiting: arith.muli. Counting: NatureOfComputation(numBoolOps=0; numBitOps=0; numIntArithOps=1; numRealArithOps=0; numCmpOps=0; numNonLinOps=0)
Visiting: arith.addi. Counting: NatureOfComputation(numBoolOps=0; numBitOps=0; numIntArithOps=1; numRealArithOps=0; numCmpOps=0; numNonLinOps=0)
Visiting: arith.addi. Counting: NatureOfComputation(numBoolOps=0; numBitOps=0; numIntArithOps=1; numRealArithOps=0; numCmpOps=0; numNonLinOps=0)
Counting here: arith.constant
Counting here: arith.constant
Counting here: affine.for
Counting here: tensor.extract
Counting here: tensor.extract
Counting here: arith.muli
Counting here: arith.addi
Counting here: affine.yield
Counting here: func.return
Writing annotations here: func.func
module {
  func.func @dot_product(%arg0: tensor<8xi16> {secret.secret}, %arg1: tensor<8xi16> {secret.secret}) -> i16 attributes {natcomp.bitOps = 0 : i64, natcomp.boolOps = 0 : i64, natcomp.cmpOps = 0 : i64, natcomp.intOps = 3 : i64, natcomp.nonLinOps = 0 : i64, natcomp.realOps = 0 : i64} {
    %c0 = arith.constant 0 : index
    %c0_i16 = arith.constant 0 : i16
    %0 = affine.for %arg2 = 0 to 8 iter_args(%arg3 = %c0_i16) -> (i16) {
      %extracted = tensor.extract %arg0[%arg2] : tensor<8xi16>
      %extracted_0 = tensor.extract %arg1[%arg2] : tensor<8xi16>
      %1 = arith.muli %extracted, %extracted_0 {natcomp.intOps = 1 : i64} : i16
      %2 = arith.addi %arg3, %1 {natcomp.intOps = 1 : i64} : i16
      affine.yield %2 : i16
    }
    return %0 : i16
  }
}

j2kun avatar Aug 08 '25 21:08 j2kun

Perhaps one thing to note is that the analysis won't "visit" your func.func directly because the data flow framework handles that by another means (initializing all the func args to the entry state to start the worklist)

j2kun avatar Aug 08 '25 21:08 j2kun

So visitOperation is not supposed to visit func.func. Does this mean that the best course of action would be to add a function in SchemeInfoAnalysis that's called after the annotate function and add a walk through the module?

kragall avatar Aug 09 '25 15:08 kragall

I'm not sure I understand the problem. The function is annotated in the example above. What outcome are you trying to achieve?

j2kun avatar Aug 09 '25 16:08 j2kun

After annotating func.func similar to the example above, the next step in my opinion would be to use the annotation for actually selecting a scheme. I wanted to do this in the SchemeSelectionAnalysis and use either a call to the annotate-module pass or a similar annotation as the result of this analysis. But for the analysis to work I need to access the annotations of func.func and I tried to do this similar to the other analyses I've come across, i.e. by using a type-switch statement in visitOperation to gather the information about all func.func ops in the lattice and then run the analysis in a separate function.

kragall avatar Aug 11 '25 15:08 kragall

Ah yes, you won't be able to do it in an analysis using the data flow framework. But you could just do the analysis work in the pass after the analysis is run, as you said with a walk. You can also just create any sort of class object you want and call it from inside runOnOperation.

j2kun avatar Aug 11 '25 16:08 j2kun