-
Notifications
You must be signed in to change notification settings - Fork 102
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
Linear constraint remover #2425
base: main
Are you sure you want to change the base?
Conversation
Depends on #2426. With this PR, we use the annotations added in #2412 and #2426 to find the columns being generated. This allows us to detect when the folded payload is not persisted as a witness column. The background is that @Schaeff is working on #2425, which would undo persisting the folded payloads in some cases, allowing us to spend fewer witness columns per bus interaction. With this PR, this should be fine: The column type will change from witness to intermediate, which means that the bus witgen will not output any folded columns. It can be tested by changing the `materialize_folded` bool to `false`, e.g. [here](https://github.com/powdr-labs/powdr/blob/e77d3801c1decff039fd0ec6bbeb55ed734357fb/std/protocols/bus.asm#L48) and running: ``` cargo run pil test_data/asm/block_to_block.asm \ -o output -f --linker-mode bus --prove-with mock --field gl ``` This used to fail before this PR.
pilopt/src/lib.rs
Outdated
// We require `left` to be a single, non-shifted, witness column `w` | ||
let w = if let AlgebraicExpression::Reference( | ||
r @ AlgebraicReference { | ||
poly_id: | ||
PolyID { | ||
ptype: PolynomialType::Committed, | ||
.. | ||
}, | ||
next: false, | ||
.. | ||
}, | ||
) = left.as_ref() | ||
{ | ||
r | ||
} else { | ||
return None; | ||
}; | ||
|
||
// we require `y` to be a multi-linear expression over non-shifted columns | ||
if right.contains_next_ref(intermediate_columns) | ||
|| right.degree_with_cache(intermediate_columns, &mut Default::default()) != 1 | ||
{ | ||
return None; | ||
} | ||
|
||
// we require `y` not to be a single, non-shifted, witness column, because this case is already covered and does not require an intermediate column | ||
// TODO: maybe we should avoid this edge case by removing the other optimizer, see xxx | ||
if matches!( | ||
right.as_ref(), | ||
AlgebraicExpression::Reference(AlgebraicReference { | ||
poly_id: PolyID { | ||
ptype: PolynomialType::Committed, | ||
.. | ||
}, | ||
next: false, | ||
.. | ||
}) | ||
) { | ||
return None; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by this chunk. It seems that we have opposite requirements for left and right being a single, non-shifted, witness column AND also no requirements for left being a multi-linear expression. In practice, shouldn't we have both left and right be single and non shifted multilinear expression OR witness column?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current pattern is:
w = lin
where w
is a non shifted witness column and lin is a multilinear expression over non-shifted columns.
Additionally, we require lin
NOT to be a single non shifted column, because we already have another optimizer for this case that we don't want to interfere with here.
Are you suggesting that we could also support lin = w
with the same requirements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but both are possible cases, right? I don't see why we should consider w = lin
and not do it for lin = w
, could you elaborate on that?
backend/src/mock/mod.rs
Outdated
.flat_map(|pil| pil.identities.iter()) | ||
.flat_map(|identity| identity.all_children()) | ||
.flat_map(|pil| { | ||
pil.identities | ||
.iter() | ||
.flat_map(|identity| identity.all_children()) | ||
.chain( | ||
// Note: we iterate on a `HashMap` so the ordering is not guaranteed, but this is ok since we're building another map. | ||
pil.intermediate_columns | ||
.values() | ||
.flat_map(|(_, def)| def.iter().flat_map(|d| d.all_children())), | ||
) | ||
.collect_vec() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this edit because we now move some identities to intermediates, so we have to use both as the preimage of challenge derivation, in order to guarantee sufficient randomness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's simpler than that:
col a = challenge(0, 0); // the challenge is declared in the intermediate definition
a = a * a; // in the identity, the challenge does not appear
Here we simply look through all expressions as opposed to starting with identities and visiting the definitions of the intermediates they access, which is the same assuming that unused intermediates have been optimized away. If not, we may end up introducing challenges which are not needed.
ast/src/analyzed/mod.rs
Outdated
self.identities | ||
.iter() | ||
.flat_map(|identity| identity.all_children()) | ||
.chain( | ||
// Note: we iterate on a `HashMap` so the ordering is not guaranteed, but this is ok since we're building another map. | ||
self.intermediate_columns | ||
.values() | ||
.flat_map(|(_, def)| def.iter().flat_map(|d| d.all_children())), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.identities | |
.iter() | |
.flat_map(|identity| identity.all_children()) | |
.chain( | |
// Note: we iterate on a `HashMap` so the ordering is not guaranteed, but this is ok since we're building another map. | |
self.intermediate_columns | |
.values() | |
.flat_map(|(_, def)| def.iter().flat_map(|d| d.all_children())), | |
) | |
self.all_children() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
backend/src/stwo/circuit_builder.rs
Outdated
// This function creates a list of the names of the constant polynomials that have next references | ||
// Note that the anaylsis should also dereference next references to intermediate polynomials |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// This function creates a list of the names of the constant polynomials that have next references | |
// Note that the anaylsis should also dereference next references to intermediate polynomials | |
/// This function creates a list of the names of the constant polynomials that have next references | |
/// Note that the anaylsis should also dereference next references to intermediate polynomials |
@@ -58,6 +58,14 @@ pub fn try_value_to_expression<T: FieldElement>( | |||
Expressionizer { poly_id_to_name }.try_value_to_expression(value) | |||
} | |||
|
|||
/// Tries to convert an algebraic expression to an expression. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Tries to convert an algebraic expression to an expression. | |
/// Tries to convert an algebraic expression to an expression that evaluates to the same value. |
pilopt/src/lib.rs
Outdated
@@ -558,6 +563,178 @@ fn extract_constant_lookups<T: FieldElement>(pil_file: &mut Analyzed<T>) { | |||
} | |||
} | |||
|
|||
/// Identifies witness columns that are constrained to a non-shifted (multi)linear expression, replaces the witness column by an intermediate polynomial |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you extend the comment a little? What is the intermediate polynomial and why are we doing this optimization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
.collect(); | ||
|
||
// pattern match identities looking for `w * (1 - w) = 0` and `(1 - w) * w = 0` constraints | ||
let boolean_constrained_witnesses = pil_file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a helper function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
pilopt/src/lib.rs
Outdated
@@ -686,6 +886,67 @@ fn constrained_to_constant<T: FieldElement>( | |||
None | |||
} | |||
|
|||
/// Identifies witness columns that are constrained to a (multi)linear expression. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you specify the semantics a bit more concretely? What does it return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
pilopt/src/lib.rs
Outdated
return None; | ||
}; | ||
|
||
// We require `left` to be a single, non-shifted, witness column `w` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also try with left and right swapped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -891,6 +1118,65 @@ fn equal_constrained<T: FieldElement>( | |||
} | |||
} | |||
|
|||
/// Tries to extract a boolean constrained witness column from a polynomial identity. | |||
/// The pattern used is `x * (1 - x) = 0` or `(1 - x) * x = 0` where `x` is a witness column. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern for boolean constraints is strictly limited to x * (1-x) or (1 - x) * x, There will never be expressions like x * (x - 1), (x - 1) * x or x * x - x, This is guaranteed by the way PIL generates constraints. Is my understanding correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct! It aligns with how witgen detects boolean constraints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the global_constraints.rs
can detect (x-1)*x = 0
as well. From the above, only x * x - x = 0
is not detected.
// add the name of the constant polynomial to the list | ||
constant_with_next_list.insert(name); | ||
/// This function creates a list of the names of the constant polynomials that have next references | ||
/// Note that the anaylsis should also dereference next references to intermediate polynomials |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Note that the anaylsis should also dereference next references to intermediate polynomials | |
/// Note that the analysis should also dereference next references to intermediate polynomials |
@@ -558,6 +563,129 @@ fn extract_constant_lookups<T: FieldElement>(pil_file: &mut Analyzed<T>) { | |||
} | |||
} | |||
|
|||
/// Identifies witness columns that are constrained to a non-shifted (multi)linear expression, replaces the witness column by an intermediate polynomial. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Identifies witness columns that are constrained to a non-shifted (multi)linear expression, replaces the witness column by an intermediate polynomial. | |
/// Identifies witness columns that are constrained to a non-shifted (multi)linear expression, replaces the witness column by an intermediate polynomial defined to be that expression. |
(obvious, but this is what I was missing to understand it :) )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah and maybe also use intermediate column
instead of polynomial
?
.iter() | ||
.filter_map(|(index, ((name, old_poly_id), expression))| { | ||
// Remove the definition. If this fails, we have already replaced it by an intermediate column. | ||
pil_file.definitions.remove(name).map(|(symbol, value)| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you turn this into a function inside Analyzed? I think we should avoid modifying it directly, especially when we have to deal with creating new IDs and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a look into this and it's a bit tricky:
- extracting it requires going from a valid pil to a valid pil
- here we're replacing a witness column by an intermediate column with a given definition
- the result is a valid pil only after we replace all references to the original witness by references to the intermediate. This is because poly_id is not global but local to each polynomial type, so we cannot reuse the one from the witness :(
- the function which does that is
pilopt::substitute_polynomial_references
, which relies ontry_algebraic_expression_to_expression
, which is inpil-analyzer
- therefore,
substitute_polynomial_references
cannot be turned into a method onAnalyzed
without causing a circular dep
This could be simplified a lot if poly_ids were global, then we could simply replace
col witness a;
a = lin
by
col a = lin
reusing the poly_id
Another option would be:
- go through the columns to remove and rename them to some dummy names, do the same for their references (this could be a
rename
method onAnalyzed
) - introduce the new intermediates with the correct names, which is valid since the names are now unused
- replace the references to the dummy names by references to the correct names
- remove the identities involving the dummy witnesses
I think these four steps are valid, although they involve a lot of back and forth. @chriseth do you have any thoughts on these? I personally like the former more (making poly_id global) but I think it's a big change I wouldn't want this PR to be blocked by.
9b5c969
to
be32fff
Compare
/// - `Some(((name, poly_id), expression))` if the identity matches the pattern, where: | ||
/// - `name` is the name of the witness column | ||
/// - `poly_id` is the id of the witness column | ||
/// - `expression` is the (multi)linear expression that the witness column is constrained to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// - `expression` is the (multi)linear expression that the witness column is constrained to | |
/// - `expression` is the (multi)linear expression that the witness column is constrained by |
what
Replace certain multilinear constraints* with intermediate polynomials in pilopt.
*with no next references, not touching public inputs
why
This comment highlights a use-case where materialising a column which is only constrained to a non-shifted multilinear polynomial is wasteful. Fixing this when we create the code is tricky, since we cannot know the degree of the constraint from pil. This is however something pilopt can do, which is what we attempt to do here
unknowns
x = 42
andx = y
which are both multilinear expressions. They do no yield intermediate polynomials, since in that case it is fine to "inline" them. This implementation currently makes sure all three optimizer apply to distinct cases, but maybe it would be better to have a single optimizer which handles all three cases, only introducing intermediate polynomials in the non trivial cases (42
andy
)- Implemented in Better optimization of intermediate polynomials #2532