-
Notifications
You must be signed in to change notification settings - Fork 98
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
Witgen: Use bus natively #2361
base: main
Are you sure you want to change the base?
Witgen: Use bus natively #2361
Conversation
Current state: |
0087d14
to
2b6fe08
Compare
e5bbf2b
to
b2b1f6d
Compare
executor/src/witgen/machines/mod.rs
Outdated
let receive = send | ||
.try_match_static(bus_receives) | ||
.expect("No matching receive!"); | ||
let multiplicity_column = if receive.is_unconstrained() { |
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 multiplicity column is only referenced rarely and I think can be removed in a follow-up PR. For now, I try to preserve the old behavior to minimize code changes.
bus_receive(ARITH_INTERACTION_ID, [0, x, y, z], latch * used, latch); | ||
col witness bus_selector; | ||
std::utils::force_bool(bus_selector); | ||
bus_receive(ARITH_INTERACTION_ID, [0, x, y, z], latch * bus_selector, latch * bus_selector); |
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 is an interesting one. In this test, we use a normal link and a bus interaction in parallel. Bus interactions used to be ignored, but now we actually do each call twice. That's fine, but then we need a new selector, because otherwise it makes the permutation argument invalid.
// Witgen will choose the degree 8. | ||
min_degree: 4, | ||
max_degree: 8, | ||
// Witgen will choose the degree 4. |
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.
Because each call happens twice (see above), I made the main machine smaller, so the submachine is still big enough.
607673d
to
adaee1f
Compare
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.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20
.
Benchmark suite | Current: adaee1f | Previous: dcf7ad9 | Ratio |
---|---|---|---|
evaluator-benchmark/std::math::ff::inverse |
8367 ns/iter (± 5 ) |
6891 ns/iter (± 7 ) |
1.21 |
evaluator-benchmark/std::math::ff::mul |
1399 ns/iter (± 1 ) |
1091 ns/iter (± 2 ) |
1.28 |
evaluator-benchmark/sqrt_879882356 |
37171 ns/iter (± 25 ) |
29796 ns/iter (± 29 ) |
1.25 |
evaluator-benchmark/sqrt_1882356 |
31062 ns/iter (± 18 ) |
25014 ns/iter (± 20 ) |
1.24 |
evaluator-benchmark/sqrt_1187956 |
31010 ns/iter (± 17 ) |
24875 ns/iter (± 22 ) |
1.25 |
evaluator-benchmark/sqrt_56 |
21719 ns/iter (± 14 ) |
17577 ns/iter (± 14 ) |
1.24 |
evaluator-benchmark/sort_33 |
957278 ns/iter (± 2595 ) |
786193 ns/iter (± 864 ) |
1.22 |
evaluator-benchmark/sort_100 |
3291219 ns/iter (± 3341 ) |
2727854 ns/iter (± 5186 ) |
1.21 |
evaluator-benchmark/sort_300 |
11502917 ns/iter (± 20460 ) |
9522945 ns/iter (± 34285 ) |
1.21 |
This comment was automatically generated by workflow using github-action-benchmark.
adaee1f
to
a1401d1
Compare
This is something I came across in #2361: The `FixedLookupMachine` does not recognize lookups of the form `[...] in [col1, col2, 3, col4]`. The reason that this was not an issue before is that the PIL optimizer simplifies such lookups. But it does not if they are represented as bus interactions. With this PR, the `FixedLookupMachine` looks for either fixed columns or compile-time constants on the RHS.
Co-authored-by: chriseth <[email protected]>
/// binary value. If it is zero, the multiplicity must be zero as well. | ||
/// For example, it could also be a binary fixed column, indicating | ||
/// where the multiplicity can be non-zero. | ||
pub selected_tuple: SelectedExpressions<T>, |
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.
bus.asm
line I'm referring to: https://github.com/powdr-labs/powdr/blob/main/std/protocols/bus.asm#L32
Does selected_tuple.expression
contain id
and tuple
from bus.asm
and selected_tuple.selector
contain latch
from bus.asm
?
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.
Yes, except the ID is separate, see convert_phantom_bus_interaction
. I'm pretty sure this will change in the future, for now, this is designed to be close to the lookups / permutations we're working with before, to keep the code changes reasonable.
/// committed. | ||
/// Note that this is the absolute multiplicity, i.e., the negation has | ||
/// been removed. | ||
pub multiplicity: Option<AlgebraicExpression<T>>, |
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.
bus.asm
line I'm referring to: https://github.com/powdr-labs/powdr/blob/main/std/protocols/bus.asm#L32
Does multiplicity
contain multiplicity
from bus.asm
?
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.
Yes, if this came from a phantom bus interaction.
use powdr_number::FieldElement; | ||
|
||
#[derive(Clone, Eq, PartialEq, Hash, Debug)] | ||
pub struct BusSend<T> { |
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.
BusSend
contains no multiplicity
because multiplicity
equals selector
for sends (basically all binary)?
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.
Yes
pub fn is_unconstrained(&self) -> bool { | ||
// TODO: This always works in practice, but we should properly check the | ||
// range constraints on the multiplicity column. | ||
self.multiplicity.as_ref() != Some(&self.selected_tuple.selector) | ||
} | ||
} |
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.
Relatedly, I wonder why we never constrain that selectors sent to the bus are binary. A couple places where it could be done:
https://github.com/powdr-labs/powdr/blob/main/std/protocols/permutation_via_bus.asm#L18-L25
https://github.com/powdr-labs/powdr/blob/main/std/protocols/lookup_via_bus.asm#L17-L27
latch
here: https://github.com/powdr-labs/powdr/blob/main/std/protocols/bus.asm#L132-L135
Does it mean that we assume that selectors are properly constrained already in the main machine and sub machines before they are unpacked for bus interactions?
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 guess the analogy is a native lookup / permutation: It also doesn't add any range constraints on its arguments. Arguably it should, but for now we want the PIL functions to be usable as a drop-in replacement for native lookups / permutations, so that we can support both without too many edge cases.
right: &SelectedExpressions<T>, | ||
rhs_multiplicity: Option<AlgebraicExpression<T>>, | ||
) -> Vec<Identity<T>> { | ||
// +1 because we want to be sure it is non-zero |
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.
Very small point but do we want the interaction ID to be non zero because it's fed into the denominator of LogUp? However wouldn't the LogUp equation still ensure proper lookup because we also have linear combination of regular columns in the lookup/permutation?
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 allowing zero would be a soundness bug. The reason is that the way we fingerprint tuples before sending it to the bus is that the fingerprint of
Although, now that I think about it, in this case, the ID is only introduced for witgen and doesn't actually appear in the witness, so maybe I'll just remove that.
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.
Also I think the translation from send/receive to polynomial constraints should do the +1
because that is where this restriction is introduced.
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.
There are no polynomial constraints involved here though, this code translates (phantom or native) lookup / permutation arguments.
I changed the interaction ID to come from the ID counter instead. Also, the counter is initialized with max(max_interaction_id, max_bus_id) + 1
now, so this also solves the problem of potentially colliding IDs if you have bus interactions and lookups / permutations (which happens more often than I thought, e.g. when you have a native lookup but also links, and use --linker-mode bus
).
// TODO: We should instead analyze the range constraints of the | ||
// multiplicity 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.
Why? Just looking at the sign of multiplicity is enough to distinguish between send and receive?
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, see the TODO ;)
As long as you use the std lib and don't create your own phantom bus interactions manually, it should always work though.
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, sorry, I misread your comment and thought you meant the opposite.
For example, using -m
as multiplicity is recognized correctly as a send, but not -1 * m
. Also -(-m)
will be recognized as a receive, but is a send ;)
true => Identity::BusReceive(BusReceive { | ||
id: bus_interaction.id, | ||
interaction_id, | ||
multiplicity: Some(multiplicity), | ||
selected_tuple, | ||
}), | ||
false => { | ||
assert_eq!(multiplicity, bus_interaction.latch); | ||
Identity::BusSend(BusSend { | ||
id: bus_interaction.id, | ||
interaction_id: AlgebraicExpression::Number(interaction_id), | ||
selected_tuple, | ||
}) | ||
} |
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.
Quite minor but why do we have T
for the interaction_id
field of the inner BusReceive
but AlgebraicExpression<T>
for the interaction_id
field of the inner BusSend
type?
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.
Because we expect that receives have a static one, but receives could be dynamic. For example, you could have a receive with ID (instr_mul + instr_add) * ARITH_ID + instr_keccak * KECCAK_ID
.
|
||
// Add an annotation for witness generation | ||
to_phantom_lookup(lookup_constraint, multiplicities); |
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.
Why don't we need lookup/permutation annotations anymore? Is it becasue we can match caller and callee via interaction_id
? It looks like we also removed references to PhantomBusInteraction
in this PR. Does it mean that this line is no longer needed? https://github.com/powdr-labs/powdr/blob/main/std/protocols/bus.asm#L36
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.
Relatedly, will each submachine have the same interaction_id
for all sends and receives?
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.
Before this PR, we completely ignored PhantomBusInteractions
in witgen, now we take it seriously. See the warning I just removed: 9e8dbef
If we would still yield a phantom lookup, we would say that each call has to execute twice, which disagrees with the constraints.
So, we still need the phantom bus interaction, but we don't also need the phantom lookup / permutation anymore.
// We're only interested in bus receives, and polynomial identities | ||
// for sure don't translate to bus receives, so we can filter them out. | ||
.filter(|identity| !matches!(identity, AnalyzedIdentity::Polynomial(_))) | ||
.flat_map(|identity| convert_identity(&mut dummy_id_counter, identity)) |
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 there really no better way than re-doing the conversion all over again here just to get the multiplicity columns?
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.
OK, I stored them in the global range constraints. This also removes the need for the machine extractor to know about the analyzed identity!
I'm also not very happy than the code in the machine extractor now has three different abstractions of the same concept: Analyzed::Lookup, Machine::Connection and now BusSend/BusReceive |
selector: bus_interaction.latch.clone(), | ||
expressions, | ||
}; | ||
match is_receive { |
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.
Doesn't clippy tell you to use if is_receive { ... } else { ... }
instead?
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
@@ -249,14 +254,11 @@ fn propagate_constraints<T: FieldElement>( | |||
false | |||
} | |||
} | |||
// Bus receives are handled when they are matched with a send. | |||
// Cannot derive a range constraint from a receive. | |||
Identity::BusReceive(..) => false, | |||
Identity::BusSend(send) => { | |||
let receive = send.try_match_static(bus_receives).unwrap(); |
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.
Why unwrap if it cannot match? Shouldn't we just ignore it in that case?
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.
Sure, but I don't really want to think about the dynamic case in this PR. There are many places were we unwrap now and have to handle it later. But I could change that we don't have try_match_static
in the first place, only match_static
, which panics. And then I can undo that in the next PR.
// In that case, we can remove the lookup, because its only function is to enforce | ||
// the range constraint. | ||
if receive.selected_tuple.expressions.len() == 1 { | ||
// In that case, we can remove the bus interaction pair, |
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.
Do we actually remove the full pair? We only remove the send, right? There could be other sends on the same bus.
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.
That is a good point, actually. They are never removed and that is also correct, because the same receive might also handle a conditional range constraint (e.g. if you have [b1] in [BYTES]; cond $ [b2] in [BYTES]
). Which means I can also remove the handling of the multiplicity columns of removed receives :)
bus_receive(ARITH_INTERACTION_ID, [0, x, y, z], latch * used, latch); | ||
col witness bus_selector; | ||
std::utils::force_bool(bus_selector); | ||
bus_receive(ARITH_INTERACTION_ID, [0, x, y, z], latch * bus_selector, latch * bus_selector); |
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 multiplicity of bus receive be negative here?
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, because bus_receive
adds the negation. Basically, if you only use bus_send
and bus_receive
in PIL, and only deal with Identity::BusSend
and Identity::BusReceive
, you don't need to know how each is implemented. Probably it would make sense to have a different Constr
variant in PIL as well, so this goes away entirely, but that should be the topic of another PR.
self.process_lookup_or_permutation(*id, left, rows) | ||
} | ||
Identity::BusSend(bus_interaction) => self.process_lookup_or_permutation( | ||
bus_interaction.id, |
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 now see that this changes from an identity id to a bus/interaction id. Could you change the names of the parameters?
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.
Also shouldn't it be a T instead of a u64?
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.
oh wait, it's bus_interaction.id
instead of bus_interaction.interaction_id
.
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.
now interaction_id
instead of bus_id
;)
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 yeah, in the future, we'll have to to change the machine call interface to receive a run-time bus ID instead of a static identity ID (which currently determines the bus ID uniquely and statically).
Depends on #2371
This PR introduces a new
Identity
, which is similar topowdr_ast::analyzed::Identity
, but it only has "bus send" and "bus receive" variants, instead of (phantom) lookup / permutation / bus interaction. Any lookup or permutation gets translated to an equivalent pair of bus interactions. This abstracts how the machine connection is implemented exactly.Right now, we assume that each bus send and receive has a compile-time interaction ID, by which they can be statically matched. This is always the case if the interaction originated from a lookup or permutation, so this is fully backward-compatible.
But what this already gives us is many-to-one relationships: We can have several sends with the same interaction ID, that map to the same receive. As a result, we can drastically remove the number of receives, as Thibaut is doing in #2359.
As a next step, we can also make the send interaction ID dynamic, which allows for a reduction of sends. For that, we need to relax the assumption that the interaction ID is known at compile time and match while solving the constraint system.
Benchmark results