Skip to content
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 inference. #2219

Merged
merged 54 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
aeec462
Expressions and solving routines.
chriseth Dec 9, 2024
3749925
Fixes and one more test.
chriseth Dec 9, 2024
41d1270
Remove comment.
chriseth Dec 9, 2024
ba5fb7b
Merge remote-tracking branch 'origin/main' into jit_solving
chriseth Dec 10, 2024
f5e5edd
Add assertions.
chriseth Dec 10, 2024
09773df
clippy
chriseth Dec 10, 2024
cb5638d
Only mention constraint in error message.
chriseth Dec 10, 2024
e348031
witgen inference.
chriseth Dec 10, 2024
fe7908e
Generic vars for symbolic expression.
chriseth Dec 10, 2024
0a2099e
Merge remote-tracking branch 'origin/main' into jit_solving
chriseth Dec 10, 2024
5014950
Also implement operations for non-ref.
chriseth Dec 10, 2024
34f52c6
Merge remote-tracking branch 'origin/jit_solving' into witgen_inference
chriseth Dec 10, 2024
e3e1fdb
witgen inference.
chriseth Dec 10, 2024
d21c061
Move "from_var" fn.
chriseth Dec 10, 2024
6bbaf54
Use rc.
chriseth Dec 10, 2024
1070ce9
use retain.
chriseth Dec 10, 2024
ff6a2f7
Fix bug and check for zero in "bitor"
chriseth Dec 10, 2024
aa91ce3
Merge remote-tracking branch 'origin/jit_solving' into witgen_inference
chriseth Dec 10, 2024
e6843ba
Simplify known.
chriseth Dec 10, 2024
372f417
Merge branch 'jit_solving' into witgen_inference
chriseth Dec 10, 2024
eb62678
Add test.
chriseth Dec 10, 2024
53dddb4
Add completeness flag.
chriseth Dec 11, 2024
67e238e
Change completeness condition.
chriseth Dec 11, 2024
9a8b3ad
pow
chriseth Dec 11, 2024
236f056
remove todo
chriseth Dec 11, 2024
cad7f37
Simplify and fix solving and remove unused conversion.
chriseth Dec 11, 2024
ef33a56
Make use of range constraints in division check.
chriseth Dec 11, 2024
b3e81e3
Typos
chriseth Dec 11, 2024
84c5d75
Comment display and remove parentheses for unary.
chriseth Dec 11, 2024
99cbb3d
extend documentation.
chriseth Dec 11, 2024
7c2f994
clippy
chriseth Dec 11, 2024
a21ca0c
simplify "allows_value"
chriseth Dec 11, 2024
479a96d
Store all range constraints with expressions.
chriseth Dec 11, 2024
1a77808
Improved allows_value.
chriseth Dec 11, 2024
8c588f8
Add another case for division.
chriseth Dec 11, 2024
969e2ba
Fix tests.
chriseth Dec 11, 2024
88cd264
Rename Variable to Symbol and relax one check.
chriseth Dec 11, 2024
8183620
clippy
chriseth Dec 11, 2024
d7d37b0
Merge remote-tracking branch 'origin/jit_solving' into witgen_inference
chriseth Dec 11, 2024
be36d96
fix test
chriseth Dec 11, 2024
ba0e2ce
Merge remote-tracking branch 'origin/main' into jit_solving
chriseth Dec 12, 2024
69af634
Merge branch 'jit_solving' into witgen_inference
chriseth Dec 12, 2024
bfb5371
Access to fixed columns.
chriseth Dec 12, 2024
f7bc144
Support permutations.
chriseth Dec 12, 2024
6ab422e
Fix a bug and add another test.
chriseth Dec 12, 2024
111902f
Update executor/src/witgen/jit/witgen_inference.rs
chriseth Dec 12, 2024
2b73e09
review
chriseth Dec 12, 2024
720645f
Merge branch 'witgen_inference' of ssh://github.com/powdr-labs/powdr …
chriseth Dec 12, 2024
f57bdd7
Rename reference evaluator.
chriseth Dec 12, 2024
fea920e
Fix range constraint for bitwise operations.
chriseth Dec 12, 2024
fd363b2
Fix xor test.
chriseth Dec 12, 2024
1ed01be
Merge remote-tracking branch 'origin/main' into witgen_inference
chriseth Dec 12, 2024
b715662
Remove (for now) unused function.
chriseth Dec 12, 2024
a905288
trick clippy.
chriseth Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 98 additions & 52 deletions executor/src/witgen/jit/affine_symbolic_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum Effect<T: FieldElement, V> {
RangeConstraint(V, RangeConstraint<T>),
/// A run-time assertion. If this fails, we have conflicting constraints.
Assertion(Assertion<T, V>),
/// a call to a different machine.
MachineCall(u64, Vec<MachineCallArgument<T, V>>),
}

/// A run-time assertion. If this fails, we have conflicting constraints.
Expand Down Expand Up @@ -57,6 +59,32 @@ impl<T: FieldElement, V> Assertion<T, V> {
}
}

pub enum MachineCallArgument<T: FieldElement, V> {
Known(SymbolicExpression<T, V>),
Unknown(AffineSymbolicExpression<T, V>),
}

#[derive(Default)]
pub struct ProcessResult<T: FieldElement, V> {
pub effects: Vec<Effect<T, V>>,
pub complete: bool,
}

impl<T: FieldElement, V> ProcessResult<T, V> {
pub fn empty() -> Self {
Self {
effects: vec![],
complete: false,
}
}
pub fn complete(effects: Vec<Effect<T, V>>) -> Self {
Self {
effects,
complete: true,
}
}
}

/// Represents an expression `a_1 * x_1 + ... + a_k * x_k + offset`,
/// where the `a_i` and `offset` are symbolic expressions, i.e. values known at run-time
/// (which can still include variables or symbols, which are only known at run-time),
Expand Down Expand Up @@ -134,6 +162,15 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
}
}

/// If this expression contains a single unknown variable, returns it.
pub fn single_unknown_variable(&self) -> Option<&V> {
if self.coefficients.len() == 1 {
self.coefficients.keys().next()
} else {
None
}
}

/// Tries to multiply this expression with another one.
/// Returns `None` if the result would be quadratic, i.e.
/// if both expressions contain unknown variables.
Expand All @@ -148,15 +185,17 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {

/// Solves the equation `self = 0` and returns how to compute the solution.
/// The solution can contain assignments to multiple variables.
/// If no way to solve the equation has been found, returns the empty vector.
/// If no way to solve the equation (and no way to derive new range
/// constraints) has been found, but it still contains
/// unknown variables, returns an empty, incomplete result.
/// If the equation is known to be unsolvable, returns an error.
pub fn solve(&self) -> Result<Vec<Effect<T, V>>, EvalError<T>> {
pub fn solve(&self) -> Result<ProcessResult<T, V>, EvalError<T>> {
Ok(match self.coefficients.len() {
0 => {
if self.offset.is_known_nonzero() {
return Err(EvalError::ConstraintUnsatisfiable(self.to_string()));
} else {
vec![]
ProcessResult::complete(vec![])
}
}
1 => {
Expand All @@ -169,43 +208,48 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
if coeff.is_known_nonzero() {
// In this case, we can always compute a solution.
let value = self.offset.field_div(&-coeff);
vec![Effect::Assignment(var.clone(), value)]
ProcessResult::complete(vec![Effect::Assignment(var.clone(), value)])
} else if self.offset.is_known_nonzero() {
// If the offset is not zero, then the coefficient must be non-zero,
// otherwise the constraint is violated.
let value = self.offset.field_div(&-coeff);
vec![
ProcessResult::complete(vec![
Assertion::assert_is_nonzero(coeff.clone()),
Effect::Assignment(var.clone(), value),
]
])
} else {
// If this case, we could have an equation of the form
// 0 * X = 0, which is valid and generates no information about X.
vec![]
ProcessResult::empty()
}
}
_ => {
let r = self.solve_bit_decomposition();
if !r.is_empty() {
if r.complete {
r
} else {
let negated = -self;
let r = negated.solve_bit_decomposition();
if !r.is_empty() {
if r.complete {
r
} else {
self.transfer_constraints()
let effects = self
.transfer_constraints()
.into_iter()
.chain(negated.transfer_constraints())
.collect()
.collect();
ProcessResult {
effects,
complete: false,
}
}
}
}
})
}

/// Tries to solve a bit-decomposition equation.
fn solve_bit_decomposition(&self) -> Vec<Effect<T, V>> {
fn solve_bit_decomposition(&self) -> ProcessResult<T, V> {
// All the coefficients need to be known numbers and the
// variables need to be range-constrained.
let constrained_coefficients = self
Expand All @@ -218,7 +262,7 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
})
.collect::<Option<Vec<_>>>();
let Some(constrained_coefficients) = constrained_coefficients else {
return vec![];
return ProcessResult::empty();
};

// Check if they are mutually exclusive and compute assignments.
Expand All @@ -228,7 +272,7 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
let mask = *constraint.multiple(coeff).mask();
if !(mask & covered_bits).is_zero() {
// Overlapping range constraints.
return vec![];
return ProcessResult::empty();
} else {
covered_bits |= mask;
}
Expand All @@ -240,26 +284,26 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
}

if covered_bits >= T::modulus() {
return vec![];
return ProcessResult::empty();
}

// We need to assert that the masks cover the offset,
// We need to assert that the masks cover "-offset",
// otherwise the equation is not solvable.
// We assert offset & !masks == 0 <=> offset == offset | masks.
// We assert -offset & !masks == 0 <=> -offset == -offset | masks.
// We use the latter since we cannot properly bit-negate inside the field.
effects.push(Assertion::assert_eq(
self.offset.clone(),
&self.offset | &T::from(covered_bits).into(),
-&self.offset,
-&self.offset | T::from(covered_bits).into(),
));

effects
ProcessResult::complete(effects)
}

fn transfer_constraints(&self) -> Vec<Effect<T, V>> {
fn transfer_constraints(&self) -> Option<Effect<T, V>> {
// We are looking for X = a * Y + b * Z + ... or -X = a * Y + b * Z + ...
// where X is least constrained.

let Some((solve_for, solve_for_coefficient)) = self
let (solve_for, solve_for_coefficient) = self
.coefficients
.iter()
.filter(|(_var, coeff)| coeff.is_known_one() || coeff.is_known_minus_one())
Expand All @@ -269,13 +313,10 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
.get(var)
.map(|c| c.range_width())
.unwrap_or_else(|| T::modulus())
})
else {
return vec![];
};
})?;

// This only works if the coefficients are all known.
let Some(summands) = self
let summands = self
.coefficients
.iter()
.filter(|(var, _)| *var != solve_for)
Expand All @@ -285,19 +326,14 @@ impl<T: FieldElement, V: Ord + Clone + Display> AffineSymbolicExpression<T, V> {
Some(rc.multiple(coeff))
})
.chain(std::iter::once(self.offset.range_constraint()))
.collect::<Option<Vec<_>>>()
else {
return vec![];
};
let Some(constraint) = summands.into_iter().reduce(|c1, c2| c1.combine_sum(&c2)) else {
return vec![];
};
.collect::<Option<Vec<_>>>()?;
let constraint = summands.into_iter().reduce(|c1, c2| c1.combine_sum(&c2))?;
let constraint = if solve_for_coefficient.is_known_one() {
-constraint
} else {
constraint
};
vec![Effect::RangeConstraint(solve_for.clone(), constraint)]
Some(Effect::RangeConstraint(solve_for.clone(), constraint))
}
}

Expand Down Expand Up @@ -394,6 +430,8 @@ impl<T: FieldElement, V: Clone + Ord> Mul<&SymbolicExpression<T, V>>

#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;

use powdr_number::GoldilocksField;

use super::*;
Expand All @@ -419,16 +457,18 @@ mod test {
let x = &Ase::from_known_symbol("X", None);
let y = &Ase::from_known_symbol("Y", None);
let constr = x + y - from_number(10);
// We cannot solve it but also cannot know it is unsolvable.
assert!(constr.solve().unwrap().is_empty());
// We cannot solve it, but we can also not learn anything new from it.
let result = constr.solve().unwrap();
assert!(result.complete && result.effects.is_empty());
Comment on lines +460 to +462
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems confusing, if we cannot solve it, why is it complete?

I think the answer is that there are no unknown symbols, right? But in that case, it is already solved, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it says it in the comment "we cannot learn anything new from it"

// But if we know the values, we can be sure there is a conflict.
assert!(from_number(10).solve().is_err());
}

#[test]
fn solvable_without_vars() {
let constr = &from_number(0);
assert!(constr.solve().unwrap().is_empty());
let result = constr.solve().unwrap();
assert!(result.complete && result.effects.is_empty());
}

#[test]
Expand All @@ -440,9 +480,10 @@ mod test {
let seven = from_number(7);
let ten = from_number(10);
let constr = mul(&two, &x) + mul(&seven, &y) - ten;
let effects = constr.solve().unwrap();
assert_eq!(effects.len(), 1);
let Effect::Assignment(var, expr) = &effects[0] else {
let result = constr.solve().unwrap();
assert!(result.complete);
assert_eq!(result.effects.len(), 1);
let Effect::Assignment(var, expr) = &result.effects[0] else {
panic!("Expected assignment");
};
assert_eq!(var.to_string(), "X");
Expand All @@ -459,12 +500,14 @@ mod test {
let ten = from_number(10);
let constr = mul(&z, &x) + mul(&seven, &y) - ten.clone();
// If we do not range-constrain z, we cannot solve since we don't know if it might be zero.
let effects = constr.solve().unwrap();
assert_eq!(effects.len(), 0);
let result = constr.solve().unwrap();
assert!(!result.complete && result.effects.is_empty());
let z =
Ase::from_known_symbol("z", Some(RangeConstraint::from_range(10.into(), 20.into())));
let constr = mul(&z, &x) + mul(&seven, &y) - ten;
let effects = constr.solve().unwrap();
let result = constr.solve().unwrap();
assert!(result.complete);
let effects = result.effects;
let Effect::Assignment(var, expr) = &effects[0] else {
panic!("Expected assignment");
};
Expand All @@ -488,17 +531,19 @@ mod test {
+ ten.clone()
+ z.clone();
// Without range constraints, this is not solvable.
assert!(constr.solve().unwrap().is_empty());
let result = constr.solve().unwrap();
assert!(!result.complete && result.effects.is_empty());
// Now add the range constraint on a, it should be solvable.
let a = Ase::from_unknown_variable("a", rc.clone());
let constr = mul(&a, &from_number(0x100))
+ mul(&b, &from_number(0x10000))
+ mul(&c, &from_number(0x1000000))
+ ten.clone()
+ z;
let effects = constr
.solve()
.unwrap()
let result = constr.solve().unwrap();
assert!(result.complete);
let effects = result
.effects
.into_iter()
.map(|effect| match effect {
Effect::Assignment(v, expr) => format!("{v} = {expr};\n"),
Expand All @@ -521,7 +566,7 @@ mod test {
"a = ((-(10 + Z) & 65280) // 256);
b = ((-(10 + Z) & 16711680) // 65536);
c = ((-(10 + Z) & 4278190080) // 16777216);
assert (10 + Z) == ((10 + Z) | 4294967040);
assert -(10 + Z) == (-(10 + Z) | 4294967040);
"
);
}
Expand All @@ -540,9 +585,10 @@ assert (10 + Z) == ((10 + Z) | 4294967040);
+ mul(&c, &from_number(0x1000000))
+ ten
- z;
let effects = constr
.solve()
.unwrap()
let result = constr.solve().unwrap();
assert!(!result.complete);
let effects = result
.effects
.into_iter()
.map(|effect| match effect {
Effect::RangeConstraint(v, rc) => format!("{v}: {rc};\n"),
Expand Down
59 changes: 59 additions & 0 deletions executor/src/witgen/jit/cell.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::{
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
};

use powdr_ast::analyzed::AlgebraicReference;

/// The identifier of a witness cell in the trace table.
/// The `row_offset` is relative to a certain "zero row" defined
/// by the component that uses this data structure.
#[derive(Debug, Clone, Eq)]
pub struct Cell {
/// Name of the column, used only for display purposes.
pub column_name: String,
pub id: u64,
pub row_offset: i32,
}

impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.row_offset.hash(state);
}
}

impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.row_offset == other.row_offset
}
}

impl Ord for Cell {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.id, self.row_offset).cmp(&(other.id, other.row_offset))
}
}

impl PartialOrd for Cell {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Cell {
pub fn from_reference(r: &AlgebraicReference, row_offset: i32) -> Self {
assert!(r.is_witness());
Self {
column_name: r.name.clone(),
id: r.poly_id.id,
row_offset: r.next as i32 + row_offset,
}
}
}

impl Display for Cell {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}[{}]", self.column_name, self.row_offset)
}
}
Loading
Loading