-
Notifications
You must be signed in to change notification settings - Fork 101
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
Single step with branching #2274
Conversation
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: b58afc1 | Previous: c83472f | Ratio |
---|---|---|---|
evaluator-benchmark/std::math::ff::reduce |
748 ns/iter (± 1 ) |
588 ns/iter (± 1 ) |
1.27 |
This comment was automatically generated by workflow using github-action-benchmark.
Co-authored-by: Georg Wiese <[email protected]>
…r into single_step_simple
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.
Wild!
I'm a bit worried generating code for every single combination of instruction flags. How would that not be infeasible for realistic examples, such as RISC-V? Also, I'm not sure if it is always possible to derive a unique witness if multiple instructions are active. Their constraints could be conflicting, right? In that case, code gen would fail?
It seems to me like we need more information which we can only get by running the PC lookup at compile time. We can figure out that the PC determines all other values in the PC lookup, and there is only a small number of possible combinations. These should be the cases we branch on, right? In FixedLookup::can_process_fully
, we do have the list of all possible values.
let mut complete = HashSet::new(); | ||
for iteration in 0.. { | ||
let mut progress = false; | ||
// TODO propagate known. |
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.
Not 100% sure what this means! Is that about propagating concrete values?
.iter() | ||
.filter_map(|var| witgen.range_constraint(var).map(|rc| (var, rc))) | ||
.filter(|(_, rc)| rc.try_to_single_value().is_none()) | ||
.sorted() |
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 this has the effect that when in two range widths are equal we pick the column with the smaller ID. Is this so that the generated code is deterministic? I think this deserves a comment then :)
@@ -327,7 +363,7 @@ impl<'a, T: FieldElement, FixedEval: FixedEvaluator<T>> WitgenInference<'a, T, F | |||
|
|||
/// Returns the current best-known range constraint on the given variable | |||
/// combining global range constraints and newly derived local range constraints. | |||
fn range_constraint(&self, variable: &Variable) -> Option<RangeConstraint<T>> { | |||
pub fn range_constraint(&self, variable: &Variable) -> Option<RangeConstraint<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.
I think it would be better to instead have a public method that selects the variable to branch on. Then the processor does not need to care about range constraints at all.
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.
We could use some information specific to the processor, for example if we know it's a single step processor in a VM, we can branch on the first variable in the longest lookup that is binary constrained (it will likely be an instruction flag).
.filter_map(|var| witgen.range_constraint(var).map(|rc| (var, rc))) | ||
.filter(|(_, rc)| rc.try_to_single_value().is_none()) | ||
.sorted() | ||
.min_by_key(|(_, rc)| rc.range_width()) |
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 have some limit on the width? In practice, this will bisect until it's a concrete value, right? Which would take forever for very large ranges?
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 would say we'll tune it later and see where we get with this.
VM::B[1] = VM::B[0]; | ||
if (VM::instr_add[0] == 1) { | ||
if (VM::instr_mul[0] == 1) { | ||
VM::A[1] = -((-(VM::A[0] + VM::B[0]) + -(VM::A[0] * VM::B[0])) + VM::A[0]); |
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.
Interesting. It doesn't know that instruction flags are exclusive. Does that mean that for
enum VariableOrValue<T, V> { | ||
Variable(V), | ||
Value(T), | ||
} | ||
|
||
pub trait FixedEvaluator<T: FieldElement> { | ||
pub trait FixedEvaluator<T: FieldElement>: Clone { |
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.
In practice, we just pass a reference, which implements Clone
, right? I guess the advantage of this is that we can also pass something like Rc<_>
, and it can automatically free the memory?
I guess it's the same for CanProcessCall
below, which is not restricted to be Clone
as well, but it is in practice. I feel like accepting references and not requiring Clone
would reduce verbosity, but no strong opinion...
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'd like to avoid the lifetimes...
}, | ||
vec![assignment(&y, symbol(&x) + number(1))], | ||
vec![assignment(&y, symbol(&x) + number(2))], | ||
)]; |
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.
)]; | |
)]; | |
let witgen_code = witgen_code(&[x.clone()], &effects); | |
assert_eq!( | |
witgen_code, | |
" | |
#[no_mangle] | |
extern \"C\" fn witgen( | |
WitgenFunctionParams{ | |
data, | |
known, | |
row_offset, | |
params, | |
mutable_state, | |
call_machine | |
}: WitgenFunctionParams<FieldElement>, | |
) { | |
let known = known_to_slice(known, data.len); | |
let data = data.to_mut_slice(); | |
let params = params.to_mut_slice(); | |
let p_0 = get_param(params, 0); | |
let p_1; | |
if 7 <= IntType::from(p_0) && IntType::from(p_0) <= 20 { | |
p_1 = (p_0 + FieldElement::from(1));} else { | |
p_1 = (p_0 + FieldElement::from(2));} | |
set_param(params, 1, p_1); | |
set_param(params, 1, p_1); | |
} | |
" | |
); | |
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.
At least for me, it helps to see a concrete example in the tests.
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'll instead add unit tests for the branch code itself. I don't really like these long auto-generated code tests because they need a lot of changes if we decide to do something in a different order (i.e. they don't really test the core that needs to be tested).
BTW, we'll need branching in block machines too, right? For stuff like |
Ah, now I read & (somewhat) understood the issue description ^^
I think this is the way to go, because the number of combinations is exponential and I don't think we want to ask the sub-machine that many times if it can succeed. So with this solution what would happen in the example from the test is:
Sounds nice! Also, I think returning range constraints might be a good idea anyway, because it would give us a "transfer of range constraints" ( |
Indeed, and it is already implemented in this PR: #2300 |
Co-authored-by: Georg Wiese <[email protected]>
…abs/powdr into single_step_with_branching
Co-authored-by: Georg Wiese <[email protected]>
Co-authored-by: Georg Wiese <[email protected]>
…abs/powdr into single_step_with_branching
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.
Vamos!
@@ -62,6 +62,16 @@ impl<T: Display> Display for Value<T> { | |||
} | |||
} | |||
|
|||
/// Return type of the `branch_on` method. | |||
pub struct BranchResult<'a, T: FieldElement, FixedEval> { |
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.
❤️
Infers single-step update code including branching.
TODO:
only_concrete_known()
-setting)