mirror of
https://github.com/powdr-labs/powdr.git
synced 2026-04-20 03:03:25 -04:00
574 lines
23 KiB
Rust
574 lines
23 KiB
Rust
use ast::analyzed::{Expression, Identity, IdentityKind, PolyID, PolynomialReference};
|
|
use itertools::Itertools;
|
|
use number::{DegreeType, FieldElement};
|
|
use parser_util::lines::indent;
|
|
use std::cmp::max;
|
|
use std::collections::HashSet;
|
|
use std::time::Instant;
|
|
|
|
use crate::witgen::identity_processor::{self, IdentityProcessor};
|
|
use crate::witgen::rows::RowUpdater;
|
|
use crate::witgen::IncompleteCause;
|
|
|
|
use super::data_structures::column_map::WitnessColumnMap;
|
|
use super::data_structures::finalizable_data::FinalizableData;
|
|
use super::query_processor::QueryProcessor;
|
|
|
|
use super::rows::{Row, RowFactory, RowPair, UnknownStrategy};
|
|
use super::{EvalError, EvalResult, EvalValue, FixedData, MutableState, QueryCallback};
|
|
|
|
/// Maximal period checked during loop detection.
|
|
const MAX_PERIOD: usize = 4;
|
|
|
|
/// A list of identities with a flag whether it is complete.
|
|
struct CompletableIdentities<'a, T: FieldElement> {
|
|
identities_with_complete: Vec<(&'a Identity<Expression<T>>, bool)>,
|
|
}
|
|
|
|
impl<'a, T: FieldElement> CompletableIdentities<'a, T> {
|
|
fn new(identities: impl Iterator<Item = &'a Identity<Expression<T>>>) -> Self {
|
|
Self {
|
|
identities_with_complete: identities.map(|identity| (identity, false)).collect(),
|
|
}
|
|
}
|
|
|
|
/// Yields immutable references to the identity and mutable references to the complete flag.
|
|
fn iter_mut(&mut self) -> impl Iterator<Item = (&'a Identity<Expression<T>>, &mut bool)> {
|
|
self.identities_with_complete
|
|
.iter_mut()
|
|
.map(|(identity, complete)| (*identity, complete))
|
|
}
|
|
}
|
|
|
|
pub struct VmProcessor<'a, T: FieldElement> {
|
|
/// The global index of the first row of [VmProcessor::data].
|
|
row_offset: DegreeType,
|
|
/// The witness columns belonging to this machine
|
|
witnesses: HashSet<PolyID>,
|
|
/// Whether a given witness column is relevant for this machine (faster than doing a contains check on witnesses)
|
|
is_relevant_witness: WitnessColumnMap<bool>,
|
|
fixed_data: &'a FixedData<'a, T>,
|
|
/// The subset of identities that contains a reference to the next row
|
|
/// (precomputed once for performance reasons)
|
|
identities_with_next_ref: Vec<&'a Identity<Expression<T>>>,
|
|
/// The subset of identities that does not contain a reference to the next row
|
|
/// (precomputed once for performance reasons)
|
|
identities_without_next_ref: Vec<&'a Identity<Expression<T>>>,
|
|
data: FinalizableData<'a, T>,
|
|
last_report: DegreeType,
|
|
last_report_time: Instant,
|
|
row_factory: RowFactory<'a, T>,
|
|
latch: Option<Expression<T>>,
|
|
}
|
|
|
|
impl<'a, T: FieldElement> VmProcessor<'a, T> {
|
|
pub fn new(
|
|
row_offset: DegreeType,
|
|
fixed_data: &'a FixedData<'a, T>,
|
|
identities: &[&'a Identity<Expression<T>>],
|
|
witnesses: HashSet<PolyID>,
|
|
data: FinalizableData<'a, T>,
|
|
row_factory: RowFactory<'a, T>,
|
|
latch: Option<Expression<T>>,
|
|
) -> Self {
|
|
let (identities_with_next, identities_without_next): (Vec<_>, Vec<_>) = identities
|
|
.iter()
|
|
.partition(|identity| identity.contains_next_ref());
|
|
|
|
let is_relevant_witness = WitnessColumnMap::from(
|
|
fixed_data
|
|
.witness_cols
|
|
.keys()
|
|
.map(|poly_id| witnesses.contains(&poly_id)),
|
|
);
|
|
|
|
VmProcessor {
|
|
row_offset,
|
|
witnesses,
|
|
is_relevant_witness,
|
|
fixed_data,
|
|
identities_with_next_ref: identities_with_next,
|
|
identities_without_next_ref: identities_without_next,
|
|
data,
|
|
row_factory,
|
|
last_report: 0,
|
|
last_report_time: Instant::now(),
|
|
latch,
|
|
}
|
|
}
|
|
|
|
pub fn finish(self) -> FinalizableData<'a, T> {
|
|
self.data
|
|
}
|
|
|
|
/// Starting out with a single row (at a given offset), iteratively append rows
|
|
/// until we have exhausted the rows or the latch expression (if available) evaluates to 1.
|
|
pub fn run<Q: QueryCallback<T>>(
|
|
&mut self,
|
|
mutable_state: &mut MutableState<'a, '_, T, Q>,
|
|
) -> EvalValue<&'a PolynomialReference, T> {
|
|
assert!(self.data.len() == 1);
|
|
|
|
// Are we in an infinite loop and can just re-use the old values?
|
|
let mut looping_period = None;
|
|
let mut loop_detection_log_level = log::Level::Info;
|
|
let rows_left = self.fixed_data.degree - self.row_offset + 1;
|
|
let mut finalize_start = 1;
|
|
for row_index in 0..rows_left {
|
|
self.maybe_log_performance(row_index);
|
|
|
|
if (row_index + 1) % 10000 == 0 {
|
|
// Periodically make sure most rows are finalized.
|
|
// Row 0 and the last MAX_PERIOD rows might be needed later, so they are not finalized.
|
|
let finalize_end = row_index as usize - MAX_PERIOD;
|
|
self.data.finalize_range(finalize_start..finalize_end);
|
|
finalize_start = finalize_end;
|
|
}
|
|
|
|
// Check if we are in a loop.
|
|
if looping_period.is_none() && row_index % 100 == 0 && row_index > 0 {
|
|
looping_period = self.rows_are_repeating(row_index);
|
|
if let Some(p) = looping_period {
|
|
log::log!(
|
|
loop_detection_log_level,
|
|
"Found loop with period {p} starting at row {row_index}"
|
|
);
|
|
}
|
|
}
|
|
if let Some(period) = looping_period {
|
|
let proposed_row = self.data[row_index as usize - period].clone();
|
|
if !self.try_proposed_row(row_index, proposed_row, mutable_state) {
|
|
log::log!(
|
|
loop_detection_log_level,
|
|
"Looping failed. Trying to generate regularly again. (Use RUST_LOG=debug to see whether this happens more often.)"
|
|
);
|
|
looping_period = None;
|
|
// For some programs, loop detection will often find loops and then fail.
|
|
// In this case, we don't want to spam the user with debug messages.
|
|
loop_detection_log_level = log::Level::Debug;
|
|
}
|
|
}
|
|
// Note that we exit one iteration early in the non-loop case,
|
|
// because ensure_has_next_row() + compute_row() will already
|
|
// add and compute some values for the next row as well.
|
|
if looping_period.is_none() && row_index != rows_left - 1 {
|
|
self.ensure_has_next_row(row_index);
|
|
self.compute_row(row_index, mutable_state);
|
|
|
|
if let Some(latch) = self.latch.as_ref() {
|
|
// Evaluate latch expression and return if it evaluates to 1.
|
|
let row_pair = RowPair::from_single_row(
|
|
self.row(row_index),
|
|
row_index + self.row_offset,
|
|
self.fixed_data,
|
|
UnknownStrategy::Unknown,
|
|
);
|
|
let latch_value = row_pair
|
|
.evaluate(latch)
|
|
.ok()
|
|
.and_then(|l| l.constant_value());
|
|
|
|
if let Some(latch_value) = latch_value {
|
|
if latch_value.is_one() {
|
|
log::trace!("Machine returns!");
|
|
return EvalValue::complete(vec![]);
|
|
}
|
|
} else {
|
|
return EvalValue::incomplete(IncompleteCause::UnknownLatch);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
assert_eq!(
|
|
self.data.len() as DegreeType + self.row_offset,
|
|
self.fixed_data.degree + 1
|
|
);
|
|
|
|
EvalValue::complete(vec![])
|
|
}
|
|
|
|
/// Checks if the last rows are repeating and returns the period.
|
|
/// Only checks for periods of 1, ..., MAX_PERIOD.
|
|
fn rows_are_repeating(&self, row_index: DegreeType) -> Option<usize> {
|
|
if row_index < MAX_PERIOD as DegreeType {
|
|
return None;
|
|
}
|
|
|
|
let row = row_index as usize;
|
|
(1..MAX_PERIOD).find(|&period| {
|
|
(1..=period).all(|i| {
|
|
self.data[row - i - period]
|
|
.values()
|
|
.zip(self.data[row - i].values())
|
|
.all(|(a, b)| a.value == b.value)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Returns a reference to a given row
|
|
fn row(&self, row_index: DegreeType) -> &Row<'a, T> {
|
|
&self.data[row_index as usize]
|
|
}
|
|
|
|
fn ensure_has_next_row(&mut self, row_index: DegreeType) {
|
|
assert!(self.data.len() as DegreeType > row_index);
|
|
if row_index == self.data.len() as DegreeType - 1 {
|
|
self.data.push(self.row_factory.fresh_row(row_index + 1));
|
|
}
|
|
}
|
|
|
|
fn compute_row<Q: QueryCallback<T>>(
|
|
&mut self,
|
|
row_index: DegreeType,
|
|
mutable_state: &mut MutableState<'a, '_, T, Q>,
|
|
) {
|
|
log::trace!("Row: {}", row_index + self.row_offset);
|
|
|
|
log::trace!(" Going over all identities until no more progress is made");
|
|
// First, go over identities that don't reference the next row,
|
|
// Second, propagate values to the next row by going over identities that do reference the next row.
|
|
let mut identities_without_next_ref =
|
|
CompletableIdentities::new(self.identities_without_next_ref.iter().cloned());
|
|
let mut identities_with_next_ref =
|
|
CompletableIdentities::new(self.identities_with_next_ref.iter().cloned());
|
|
self.loop_until_no_progress(row_index, &mut identities_without_next_ref, mutable_state)
|
|
.and_then(|_| {
|
|
self.loop_until_no_progress(row_index, &mut identities_with_next_ref, mutable_state)
|
|
})
|
|
.map_err(|e| self.report_failure_and_panic_unsatisfiable(row_index, e))
|
|
.unwrap();
|
|
|
|
// Check that the computed row is "final" by asserting that all unknown values can
|
|
// be set to 0.
|
|
log::trace!(" Checking that remaining identities hold when unknown values are set to 0");
|
|
let mut identity_processor = IdentityProcessor::new(self.fixed_data, mutable_state);
|
|
self.process_identities(
|
|
row_index,
|
|
&mut identities_without_next_ref,
|
|
UnknownStrategy::Zero,
|
|
&mut identity_processor,
|
|
)
|
|
.and_then(|_| {
|
|
self.process_identities(
|
|
row_index,
|
|
&mut identities_with_next_ref,
|
|
UnknownStrategy::Zero,
|
|
&mut identity_processor,
|
|
)
|
|
})
|
|
.map_err(|e| self.report_failure_and_panic_underconstrained(row_index, e))
|
|
.unwrap();
|
|
|
|
log::trace!(
|
|
"{}",
|
|
self.row(row_index)
|
|
.render(&format!("===== Row {}", row_index), true, &self.witnesses)
|
|
);
|
|
}
|
|
|
|
/// Loops over all identities and queries, until no further progress is made.
|
|
/// @returns the "incomplete" identities, i.e. identities that contain unknown values.
|
|
fn loop_until_no_progress<Q>(
|
|
&mut self,
|
|
row_index: DegreeType,
|
|
identities: &mut CompletableIdentities<'a, T>,
|
|
mutable_state: &mut MutableState<'a, '_, T, Q>,
|
|
) -> Result<(), Vec<EvalError<T>>>
|
|
where
|
|
Q: FnMut(&str) -> Option<T> + Send + Sync,
|
|
{
|
|
loop {
|
|
let mut identity_processor = IdentityProcessor::new(self.fixed_data, mutable_state);
|
|
let mut progress = self.process_identities(
|
|
row_index,
|
|
identities,
|
|
UnknownStrategy::Unknown,
|
|
&mut identity_processor,
|
|
)?;
|
|
let mut updates = EvalValue::complete(vec![]);
|
|
let mut query_processor =
|
|
QueryProcessor::new(self.fixed_data, mutable_state.query_callback);
|
|
let row_pair = RowPair::new(
|
|
self.row(row_index),
|
|
self.row(row_index + 1),
|
|
row_index + self.row_offset,
|
|
self.fixed_data,
|
|
UnknownStrategy::Unknown,
|
|
);
|
|
for poly_id in self.fixed_data.witness_cols.keys() {
|
|
if self.is_relevant_witness[&poly_id] {
|
|
updates.combine(query_processor.process_query(&row_pair, &poly_id));
|
|
}
|
|
}
|
|
progress |= self.apply_updates(row_index, &updates, || "queries".to_string());
|
|
|
|
if !progress {
|
|
break;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Loops over all identities once and updates the current row and next row.
|
|
/// Arguments:
|
|
/// * `identities`: Identities to process. Completed identities are removed from the list.
|
|
/// * `unknown_strategy`: How to process unknown variables. Either use zero or keep it symbolic.
|
|
/// Returns:
|
|
/// * `Ok(true)`: If progress was made.
|
|
/// * `Ok(false)`: If no progress was made.
|
|
/// * `Err(errors)`: If an error occurred.
|
|
fn process_identities<Q: QueryCallback<T>>(
|
|
&mut self,
|
|
row_index: DegreeType,
|
|
identities: &mut CompletableIdentities<'a, T>,
|
|
unknown_strategy: UnknownStrategy,
|
|
identity_processor: &mut IdentityProcessor<'a, '_, '_, T, Q>,
|
|
) -> Result<bool, Vec<EvalError<T>>> {
|
|
let mut progress = false;
|
|
let mut errors = vec![];
|
|
|
|
for (identity, is_complete) in identities.iter_mut() {
|
|
if *is_complete {
|
|
continue;
|
|
}
|
|
|
|
let is_machine_call = matches!(
|
|
identity.kind,
|
|
IdentityKind::Plookup | IdentityKind::Permutation
|
|
);
|
|
if is_machine_call && unknown_strategy == UnknownStrategy::Zero {
|
|
// The fact that we got to the point where we assume 0 for unknown cells, but this identity
|
|
// is still not complete, means that either the inputs or the machine is under-constrained.
|
|
errors.push(format!("{identity}:\n{}",
|
|
indent("This machine call could not be completed. Either some inputs are missing or the machine is under-constrained.", " ")).into());
|
|
continue;
|
|
}
|
|
|
|
let row_pair = RowPair::new(
|
|
self.row(row_index),
|
|
self.row(row_index + 1),
|
|
row_index + self.row_offset,
|
|
self.fixed_data,
|
|
unknown_strategy,
|
|
);
|
|
let result: EvalResult<'a, T> = identity_processor
|
|
.process_identity(identity, &row_pair)
|
|
.map_err(|err| {
|
|
format!("{identity}:\n{}", indent(&format!("{err}"), " ")).into()
|
|
});
|
|
|
|
match result {
|
|
Ok(eval_value) => {
|
|
if unknown_strategy == UnknownStrategy::Zero {
|
|
assert!(eval_value.constraints.is_empty())
|
|
} else {
|
|
*is_complete = eval_value.is_complete();
|
|
progress |=
|
|
self.apply_updates(row_index, &eval_value, || format!("{identity}"));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
errors.push(e);
|
|
}
|
|
};
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok(progress)
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn apply_updates(
|
|
&mut self,
|
|
row_index: DegreeType,
|
|
updates: &EvalValue<&PolynomialReference, T>,
|
|
source_name: impl Fn() -> String,
|
|
) -> bool {
|
|
if updates.constraints.is_empty() {
|
|
return false;
|
|
}
|
|
let (current, next) = self.data.mutable_row_pair(row_index as usize);
|
|
let mut row_updater = RowUpdater::new(current, next, row_index + self.row_offset);
|
|
row_updater.apply_updates(updates, source_name)
|
|
}
|
|
|
|
fn report_failure_and_panic_unsatisfiable(
|
|
&self,
|
|
row_index: DegreeType,
|
|
failures: Vec<EvalError<T>>,
|
|
) -> ! {
|
|
log::error!(
|
|
"\nError: Row {} failed. Set RUST_LOG=debug for more information.\n",
|
|
row_index + self.row_offset
|
|
);
|
|
log::debug!("Some identities where not satisfiable after the following values were uniquely determined (known nonzero first, then zero, unknown omitted):");
|
|
log::debug!(
|
|
"{}",
|
|
self.row(row_index)
|
|
.render("Current Row", false, &self.witnesses)
|
|
);
|
|
log::debug!(
|
|
"{}",
|
|
self.row(row_index + 1)
|
|
.render("Next Row", false, &self.witnesses)
|
|
);
|
|
log::debug!("Set RUST_LOG=trace to understand why these values were chosen.");
|
|
log::debug!(
|
|
"Assuming these values are correct, the following identities fail:\n{}\n",
|
|
failures
|
|
.iter()
|
|
.map(|r| indent(&r.to_string(), " "))
|
|
.join("\n")
|
|
);
|
|
panic!("Witness generation failed.");
|
|
}
|
|
|
|
fn report_failure_and_panic_underconstrained(
|
|
&self,
|
|
row_index: DegreeType,
|
|
failures: Vec<EvalError<T>>,
|
|
) -> ! {
|
|
log::error!(
|
|
"\nError: Row {} failed. Set RUST_LOG=debug for more information.\n",
|
|
row_index + self.row_offset
|
|
);
|
|
|
|
log::debug!("Some columns could not be determined, but setting them to zero does not satisfy the constraints. This typically means that the system is underconstrained!");
|
|
log::debug!(
|
|
"{}",
|
|
self.row(row_index)
|
|
.render("Current Row", true, &self.witnesses)
|
|
);
|
|
log::debug!(
|
|
"{}",
|
|
self.row(row_index)
|
|
.render("Next Row", true, &self.witnesses)
|
|
);
|
|
log::debug!("\nSet RUST_LOG=trace to understand why these values were (not) chosen.");
|
|
log::debug!(
|
|
"Assuming zero for unknown values, the following identities fail:\n{}\n",
|
|
failures
|
|
.iter()
|
|
.map(|r| indent(&r.to_string(), " "))
|
|
.join("\n")
|
|
);
|
|
panic!("Witness generation failed.");
|
|
}
|
|
|
|
/// Verifies the proposed values for the next row.
|
|
/// TODO this is bad for machines because we might introduce rows in the machine that are then
|
|
/// not used.
|
|
fn try_proposed_row<'b, Q: QueryCallback<T>>(
|
|
&mut self,
|
|
row_index: DegreeType,
|
|
proposed_row: Row<'a, T>,
|
|
mutable_state: &mut MutableState<'a, 'b, T, Q>,
|
|
) -> bool {
|
|
let constraints_valid = {
|
|
let mut identity_processor = IdentityProcessor::new(self.fixed_data, mutable_state);
|
|
self.check_row_pair(row_index, &proposed_row, false, &mut identity_processor)
|
|
&& self.check_row_pair(row_index, &proposed_row, true, &mut identity_processor)
|
|
};
|
|
|
|
if constraints_valid {
|
|
if row_index as usize == self.data.len() - 1 {
|
|
// We might already have added the row in [VmProcessor::ensure_has_next_row]
|
|
self.data[row_index as usize] = proposed_row;
|
|
} else {
|
|
// If the previous row was also added by [VmProcessor::try_propose_row], we won't have an entry
|
|
// for the current row yet.
|
|
assert_eq!(row_index as usize, self.data.len());
|
|
self.data.push(proposed_row);
|
|
}
|
|
} else {
|
|
// Note that we never update the next row if proposing a row succeeds (the happy path).
|
|
// If it doesn't, we re-run compute_next_row on the previous row in order to
|
|
// correctly forward-propagate values via next references.
|
|
self.ensure_has_next_row(row_index - 1);
|
|
self.compute_row(row_index - 1, mutable_state);
|
|
}
|
|
constraints_valid
|
|
}
|
|
|
|
fn check_row_pair<Q: QueryCallback<T>>(
|
|
&self,
|
|
row_index: DegreeType,
|
|
proposed_row: &Row<'a, T>,
|
|
previous: bool,
|
|
identity_processor: &mut IdentityProcessor<'a, '_, '_, T, Q>,
|
|
) -> bool {
|
|
let row_pair = match previous {
|
|
// Check whether identities with a reference to the next row are satisfied
|
|
// when applied to the previous row and the proposed row.
|
|
true => RowPair::new(
|
|
self.row(row_index - 1),
|
|
proposed_row,
|
|
row_index + self.row_offset - 1,
|
|
self.fixed_data,
|
|
UnknownStrategy::Zero,
|
|
),
|
|
// Check whether identities without a reference to the next row are satisfied
|
|
// when applied to the proposed row.
|
|
// Because we never access the next row, we can use [RowPair::from_single_row] here.
|
|
false => RowPair::from_single_row(
|
|
proposed_row,
|
|
row_index + self.row_offset,
|
|
self.fixed_data,
|
|
UnknownStrategy::Zero,
|
|
),
|
|
};
|
|
|
|
// Check identities depending on whether or not they have a reference to the next row:
|
|
// - Those that do should be checked on the previous and proposed row
|
|
// - All others should be checked on the proposed row
|
|
let identities = match previous {
|
|
true => &self.identities_with_next_ref,
|
|
false => &self.identities_without_next_ref,
|
|
};
|
|
|
|
for identity in identities.iter() {
|
|
if identity_processor
|
|
.process_identity(identity, &row_pair)
|
|
.is_err()
|
|
{
|
|
log::debug!("Previous {:?}", self.row(row_index - 1));
|
|
log::debug!("Proposed {:?}", proposed_row);
|
|
log::debug!("Failed on identity: {}", identity);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn maybe_log_performance(&mut self, row_index: DegreeType) {
|
|
if row_index >= self.last_report + 1000 {
|
|
let duration = self.last_report_time.elapsed();
|
|
self.last_report_time = Instant::now();
|
|
|
|
let identity_statistics = identity_processor::get_and_reset_solving_statistics();
|
|
let identities_per_sec =
|
|
((identity_statistics.values().map(|s| s.success).sum::<u64>() as u128 * 1000)
|
|
/ duration.as_micros()) as u64;
|
|
let identities_count = max(identity_statistics.len() as u64, 1);
|
|
let progress_percentage = identity_statistics
|
|
.values()
|
|
.map(|s| s.success * 100 / s.invocations)
|
|
.sum::<u64>()
|
|
/ identities_count;
|
|
|
|
let row = row_index + self.row_offset;
|
|
log::info!(
|
|
"{row} of {} rows ({}%) - {} rows/s, {identities_per_sec}k identities/s, {progress_percentage}% progress",
|
|
self.fixed_data.degree,
|
|
row * 100 / self.fixed_data.degree,
|
|
1_000_000_000 / duration.as_micros()
|
|
);
|
|
self.last_report = row_index;
|
|
}
|
|
}
|
|
}
|