-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for selectors #125
Comments
One potential syntax improvement I mentioned in #191 (comment): we could use boolean operators to make the selectors more readable. Specifically, something like:
Instead of:
The boolean operators would be pretty simple:
|
One of the things I've been thinking about is how to combine different branches of the
Naively, the above would generate the following two constraints: Ideally, however, we'd get something like this: I'm hoping that these type of optimizations we will be able to do with optimization passes in the IR - though, it might be quite a bit simpler to do them while building an algebraic graph for the selector expression. However, consider the following constraints:
Obviously, these are exactly the same constraints, but naive parsing would give us the following expressions: And I'm actually not sure how easy (or whether at all possible), it would be to reduce them to the optimized form shown previously. So, I think there is an argument to be made for optimizing these types of expressions at the time when we build an algebraic graph for selector expressions. One way to do this would be to evaluate all
By noticing that two constraints evaluate to the same value ( |
We could also keep a I think there are other cases that might be trickier to simplify like above if the user doesn't write them consistently. For example, consider something like:
These should be combined but it may be tricky to combine them. We could of course ignore cases like these and just output the unoptimized form. |
I think evaluating at a random point may be more robust as something like |
That doesn't sound right - wouldn't you actually have four constraints, because each match arm calls an evaluator which consists of two constraints; and while each match arm is supposed to be mutually exclusive with the others, so two of the constraints at any given point would be effectively no-ops, they all must still be evaluated at every point right? While it seems possible to combine constraints in such a way that the properties of both are upheld simultaneously, it isn't clear how we could do so as a generic transformation, safely. It would be really important to have a proof of the safety of that transformation, or the risk of accidentally breaking constraints would be really high. That said, I wonder if this would be an ideal application for e-graphs. Input all of the rewrite rules that we'd want to apply, and are known to be safe, e.g. With that in mind, to me this really calls into question the benefit of
Checking whether match arms are exclusive seems like a good problem for a SAT solver - generate a variable for each trace column, and then ask it to determine if the disjunction of the selectors is satisfiable. If it isn't, then that means none of the selectors are valid (i.e. they are nonsensical expressions like For example:
First, we assign each scalar it's own variable:
We'd input the disjunction of the selectors to the solver, i.e.
If we apply these solutions to each selector, we only ever observe one selector at a time evaluating to true. This tells us the selectors are mutually exclusive, so the match constraint is valid. But let's consider what would happen if we had the following code instead:
The variables we assign would be the same here, but this time, we get the following solutions:
When applying the second solution, this time both selectors evaluate to true. This tells us the selectors are not mutually exclusive, which should result in an error being raised.
You can't extrapolate equivalance of those expressions based on equality of the results - there could always be values for the variables which produce different results, and the only way to know that is to try all inputs. This is where egraphs really shine, because they are designed to canonicalize/rewrite expressions and determine equivalence based on a set of rules. So in your example, you could add |
I guess there is an even more naive way to write them as 4 separate constraints. But they can trivially be combined into two constraints as I described in the comment. Basically, we can have 4 constraints like so: Or we can have 2 constraints which look like this: And assuming values of
This is the main reason for having
I think this could be a separate and very interesting problem to solve - but I'd say that a fully generic solution is much more complicated as compared to what we can do with
Being able to combine constraints in the way I described above is very important. The reasons is that during STARK protocol, we need to generate a random value for each constraint. So, if we have 4 constraints, we need 4 random values, if we have 2 constraints, we need only 2 random values. Generating and applying these random values is very expensive for the verifier - so, the fewer there are the better. In the specific case of Miden VM, we'd probably have more than 3000 individual constraints, but if we combine them together as described above, we probably will end up with fewer than 300. At 3000 constraints, the system basically becomes not suitable for our use case, but at 300 it is not too bad. A good example of this is stack constraints (still described using old syntax). There is about 88 different arms in the match statement. All of these arms are mutually exclusive and all of them have at least 16 constraints (some more). If we were to describe them individually, we'd end up with close to 1,500 constraints. But if we combine them together, we should end up somewhere between 50 and 100. This is a huge (and crucial) improvement.
Yes, I think this would be a good problem to tackle in the future. There are other things we'll need to check there as well. For example, the values used in the
Generally true, but if we draw inputs from large enough domain the probability of failure is negligible. In our case, we can tolerate probability of failure of |
One other thing to note here: we are not dealing with arbitrary functions here but with polynomials of bounded degree. In fact, the degree of our polynomials will never be greater than This is because two distinct polynomials of degree |
So to be clear, I definitely recognize how they can be combined in this specific instance, but that doesn't actually get us to a transformation which is applicable in general, at least as stated so far. In this specific case, we have a simple true/false selector, and both arms of the match constraint express equalities against the same columns - that seems like the absolute best case scenario. In practice, I would imagine selectors will involve sets of columns, with at least some non-overlapping members (e.g. Consider the following contrived example:
How does this get combined? To be clear, I'm not saying it can't be, rather it isn't clear to me what the actual transformation is symbolically. I think this is why egraphs end up being a nice solution for this, because we are able to easily implement and reason about the small step semantics of the transformations we wish to apply and can prove the correctness of, and thanks to the egraph formalism, be able to say confidently that the resulting output is itself correct, because the output must be equivalent to the input.
Without automated verification, there is a huge safety hole here - while
This is a good example of where it isn't possible for us to validate the mutual exclusivity of the selectors. From the perspective of static analysis, each selector is expressed in terms of a different column in
I think likely shakes out to be about the same amount of effort either way; you either implement an evaluator and evaluate a couple randomly chosen values from the domain, and then act on the results of that evaluation - or you construct an egraph representing the constraints and ask it if the constraints are equivalent. An alternative, less sophisticated approach which can produce similar results, is to implement a canonicalization pass which performs a series of local transformations that guarantee certain properties of the resulting program. For example, you might canonicalize the ordering of operands for commutative operators by name, so To sum up, I think what I'm really looking for are the following:
Given the following abstract representation of an
The transformation starts by combining constraints for each selector:
It is then finalized by combining all of the selectors and their corresponding constraints:
I have no idea if that transformation would be correct/safe, but it was just an example to illustrate how we can apply the transformation symbolically without having to reason about specific programs. |
I probably should have explained the methodology of how constraints can be combined much earlier.
There are two main rules:
Let's go through the example you gave. In this example, we base selector expressions on 3 columns:
If these constraint is missing, we should not be able to use these columns in selector expressions. For now, the language does not enforce this, but in the future, we should probably give a warning (or maybe even an error) if we can't determine that all selector inputs are binary. With the above in mind, the correct way to write your example would be:
Next, using notation similar to yours, the naive (but still extremely useful) way to combine constraints would be: Input:
Output:
Basically, we take the first constraint from each arm, multiply them by their respective selectors, and add them together. Then we do the same with the second constraint from each arm etc. For your specific example, the combined constraints would look like this: Where Notice that if As I mentioned above, this is the naive method of combining constraints. Using evaluation at a random point, can do things even better (though not in this specific case). I'll describe this approach in more detail in a separate comment.
Agreed. I think we should try to automate as much as possible, but this will take time. I want to start with something that works assuming the user coded to constraints correctly, and then over time we can add more checks.
This is actually not that difficult - though, may require additional syntax in the language. Specifically, all selectors are generated by a single compute_op_flags() function. This function takes a vector of 9 binary values (though, currently we don't have a way to enforce that values are binary) and outputs a vector of 88 binary values. We can very easily run through all possible combinations of 9 input values and check that for each combination only a single value is set to I'm not saying that this is the best way to perform such checks, but we could do it this way if needed. But again, this is something we'll do in the future - once all the basic things are working. |
Frequently, we want to apply some set of constraints only if some condition is true. For example, for Rescue Prime constraints, we usually want to apply them only on the first 7 steps of an 8 step cycle. This, of course, could be baked into the constraints themselves, but that would make them much less readable and modular.
So, some syntax which will help with applying selectors could be very helpful. Here is how this could work:
Single selector
First, if we have just a single selector, we could do something like this:
The above specifies that to all constraints returned from bar evaluator would be multiplied by k0. Specifically, the final constraint would be:
We could also have the compiler check whether$k_0$ is binary and output either an error or a warning if it isn't.
Multi selector
While we should be able to describe everything using single selectors, frequently it could be much more convenient to specify that one out of several constraints should apply in a given situation. For this, we could use something like that:
The above would translate into the following constraint:
We could also have the compiler check whether all
when
conditions are mutually exclusive and output an error or a warning if they are not.The above methodology would also work if evaluators return different number of constraints. For example:
The above would translate to two constraints:
And if some constraints are redundant, the compiler can simplify those constraints very easily. For example:
Would translate to something like:
Instead of the second constraint being something like:
$$a \cdot (c' - b) + (1 - a) \cdot (c' - b) = 0$$
Originally posted by @bobbinth in 0xPolygonMiden/miden-vm#254 (comment)
The text was updated successfully, but these errors were encountered: