use crate::BinaryInput;
use pil2_std_lib::Std;
use proofman_fields::PrimeField64;
use rayon::prelude::*;
use std::ops::Range;
pub const RANGE_16_BITS: usize = 0xFFFF + 1;
pub fn fill_and_tally<R, T, Fill>(
rows: &mut [R],
inputs: &[T],
inputs_per_row: usize,
fill: Fill,
) -> Vec<u32>
where
R: Send,
T: Sync,
Fill: Fn(&mut R, &[T], &mut [u32]) + Sync + Send,
{
assert!(inputs_per_row > 0, "a row must take at least one input");
assert_eq!(
rows.len(),
inputs.len().div_ceil(inputs_per_row),
"the rows must hold exactly the {} inputs, {inputs_per_row} to a row",
inputs.len(),
);
let tasks = rayon::current_num_threads().max(1);
let rows_per_task = rows.len().div_ceil(tasks).max(1);
rows.par_chunks_mut(rows_per_task)
.zip(inputs.par_chunks(rows_per_task * inputs_per_row))
.map(|(row_chunk, input_chunk)| {
let mut multiplicities = vec![0u32; RANGE_16_BITS];
for (row, row_inputs) in row_chunk.iter_mut().zip(input_chunk.chunks(inputs_per_row)) {
fill(row, row_inputs, &mut multiplicities);
}
multiplicities
})
.reduce_with(|mut acc, task| {
for (total, count) in acc.iter_mut().zip(&task) {
*total += count;
}
acc
})
.unwrap_or_else(|| vec![0u32; RANGE_16_BITS])
}
pub const REGION_BITS: u32 = 16;
pub const REGION_ROWS: usize = 1 << REGION_BITS;
const PROMOTE_SHARE: usize = 1024;
const PROMOTE_FLOOR: u32 = 64;
pub struct SparseTally {
regions: Vec<Vec<u32>>,
hits: Vec<u32>,
spill: Vec<u64>,
promote: u32,
table_rows: u64,
}
impl SparseTally {
pub fn new(table_rows: u64, lookups: usize) -> Self {
let regions = (table_rows as usize).div_ceil(REGION_ROWS);
Self {
regions: vec![Vec::new(); regions],
hits: vec![0; regions],
spill: Vec::new(),
promote: u32::try_from(lookups / PROMOTE_SHARE).unwrap_or(u32::MAX).max(PROMOTE_FLOOR),
table_rows,
}
}
#[inline(always)]
pub fn inc(&mut self, row: u64) {
debug_assert!(
row < self.table_rows,
"row {row} is past the {} of the table",
self.table_rows
);
let index = (row >> REGION_BITS) as usize;
let region = &mut self.regions[index];
if region.is_empty() {
self.hits[index] += 1;
if self.hits[index] < self.promote {
self.spill.push(row);
return;
}
region.resize(REGION_ROWS, 0);
}
region[row as usize & (REGION_ROWS - 1)] += 1;
}
fn merge(mut self, other: Self) -> Self {
assert_eq!(self.table_rows, other.table_rows, "the tallies are of different tables");
for (into, from) in self.regions.iter_mut().zip(other.regions) {
if from.is_empty() {
continue;
}
if into.is_empty() {
*into = from;
continue;
}
for (total, count) in into.iter_mut().zip(&from) {
*total += count;
}
}
self.spill.extend(other.spill);
self
}
pub fn flush<F: PrimeField64>(self, std: &Std<F>, table_id: usize) {
let table_rows = self.table_rows;
for (region, counts) in self.regions.into_iter().enumerate() {
if counts.is_empty() {
continue;
}
let start = (region * REGION_ROWS) as u64;
let len = REGION_ROWS.min((table_rows - start) as usize);
std.inc_virtual_rows_ranged(table_id, Some(start), &counts[..len]);
}
if !self.spill.is_empty() {
std.inc_virtual_row_batch_one(table_id, &self.spill);
}
}
#[cfg(test)]
fn counts(&self) -> std::collections::HashMap<u64, u32> {
let mut counts = std::collections::HashMap::new();
for (region, histogram) in self.regions.iter().enumerate() {
let base = (region * REGION_ROWS) as u64;
for (row, &count) in histogram.iter().enumerate() {
if count != 0 {
*counts.entry(base + row as u64).or_insert(0) += count;
}
}
}
for &row in &self.spill {
*counts.entry(row).or_insert(0) += 1;
}
counts
}
}
pub const SKEWED_OTHERS: usize = 64;
#[derive(Default)]
pub struct SkewedTally {
zeros: u64,
others: Vec<u64>,
row_slot: usize,
stopped_at: Option<usize>,
unfinished: Vec<Range<usize>>,
#[cfg(debug_assertions)]
last_slot: Option<usize>,
}
impl SkewedTally {
#[inline(always)]
pub fn at_row(&mut self, first_slot: usize) {
self.row_slot = first_slot;
}
#[inline(always)]
pub fn inc(&mut self, lane: usize, value: u64) {
#[cfg(debug_assertions)]
{
let slot = self.row_slot + lane;
debug_assert!(
self.last_slot != Some(slot),
"slot {slot} was range checked twice; the resume point cannot express that",
);
self.last_slot = Some(slot);
}
if self.stopped_at.is_some() {
return;
}
if value == 0 {
self.zeros += 1;
return;
}
if self.others.len() == SKEWED_OTHERS {
self.stopped_at = Some(self.row_slot + lane);
return;
}
self.others.push(value);
}
fn finish(&mut self, end: usize) {
if let Some(start) = self.stopped_at.take() {
self.unfinished.push(start..end);
}
}
fn merge(mut self, other: Self) -> Self {
debug_assert!(
self.stopped_at.is_none() && other.stopped_at.is_none(),
"a task was merged before `finish` turned where it stopped into a range",
);
self.zeros += other.zeros;
self.others.extend(other.others);
self.unfinished.extend(other.unfinished);
self
}
#[cfg(test)]
fn counted(&self) -> (u64, Vec<u64>, Vec<Range<usize>>) {
(self.zeros, self.others.clone(), self.unfinished.clone())
}
#[must_use]
pub fn flush<F: PrimeField64>(self, std: &Std<F>, range_id: usize) -> Vec<Range<usize>> {
if self.zeros > 0 {
std.range_check(range_id, 0u64, self.zeros);
}
if !self.others.is_empty() {
std.range_check_batch_one(range_id, &self.others);
}
self.unfinished
}
}
pub struct FillTally {
pub table: SparseTally,
pub range: SkewedTally,
}
impl FillTally {
fn new(table_rows: u64, lookups: usize) -> Self {
Self { table: SparseTally::new(table_rows, lookups), range: SkewedTally::default() }
}
#[inline(always)]
pub fn inc(&mut self, row: u64) {
self.table.inc(row);
}
#[inline(always)]
pub fn inc_range(&mut self, lane: usize, value: u64) {
self.range.inc(lane, value);
}
fn merge(self, other: Self) -> Self {
Self { table: self.table.merge(other.table), range: self.range.merge(other.range) }
}
}
struct InputCursor<'a> {
chunks: &'a [Vec<BinaryInput>],
chunk: usize,
offset: usize,
}
impl<'a> InputCursor<'a> {
fn new(chunks: &'a [Vec<BinaryInput>], starts: &[usize], slot: usize) -> Self {
let chunk = starts.partition_point(|&start| start <= slot) - 1;
Self { chunks, chunk, offset: slot - starts[chunk] }
}
#[inline(always)]
fn next(&mut self) -> Option<&'a BinaryInput> {
while self.chunk < self.chunks.len() {
let chunk = &self.chunks[self.chunk];
if self.offset < chunk.len() {
let input = &chunk[self.offset];
self.offset += 1;
return Some(input);
}
self.chunk += 1;
self.offset = 0;
}
None
}
}
pub fn for_each_operation_in<Visit>(
inputs: &[Vec<BinaryInput>],
slots: &[Range<usize>],
mut visit: Visit,
) where
Visit: FnMut(&BinaryInput),
{
if slots.is_empty() {
return;
}
let starts = chunk_starts(inputs);
for range in slots {
let mut cursor = InputCursor::new(inputs, &starts, range.start);
for slot in range.clone() {
let input =
cursor.next().unwrap_or_else(|| panic!("slot {slot} is past the operations"));
visit(input);
}
}
}
fn chunk_starts(chunks: &[Vec<BinaryInput>]) -> Vec<usize> {
let mut starts = Vec::with_capacity(chunks.len() + 1);
let mut total = 0;
starts.push(0);
for chunk in chunks {
total += chunk.len();
starts.push(total);
}
starts
}
#[allow(clippy::too_many_arguments)]
pub fn fill_slots_and_tally<R, Slot, Pad>(
rows: &mut [R],
inputs: &[Vec<BinaryInput>],
total_inputs: usize,
lanes_x_row: usize,
table_rows: u64,
lookups_x_slot: usize,
slot: Slot,
pad: Pad,
) -> FillTally
where
R: Send,
Slot: Fn(&mut R, usize, &BinaryInput, &mut FillTally) + Sync + Send,
Pad: Fn(&mut R, usize) + Sync + Send,
{
assert!(lanes_x_row > 0, "a row must take at least one operation");
assert_eq!(
rows.len(),
total_inputs.div_ceil(lanes_x_row),
"the rows must hold exactly the {total_inputs} operations, {lanes_x_row} to a row",
);
let starts = chunk_starts(inputs);
assert_eq!(
starts[inputs.len()],
total_inputs,
"the chunks hold {} operations, not the {total_inputs} announced",
starts[inputs.len()],
);
let tasks = rayon::current_num_threads().max(1);
let rows_per_task = rows.len().div_ceil(tasks).max(1);
let lookups_x_task = rows_per_task * lanes_x_row * lookups_x_slot;
rows.par_chunks_mut(rows_per_task)
.enumerate()
.map(|(task, row_chunk)| {
let mut tally = FillTally::new(table_rows, lookups_x_task);
let mut done = task * rows_per_task * lanes_x_row;
let mut cursor = InputCursor::new(inputs, &starts, done);
for row in row_chunk.iter_mut() {
let filled = lanes_x_row.min(total_inputs - done);
tally.range.at_row(done);
for lane in 0..filled {
let input = cursor.next().expect("the cursor holds one input per filled slot");
slot(row, lane, input, &mut tally);
}
for lane in filled..lanes_x_row {
pad(row, lane);
}
done += filled;
}
tally.range.finish(done);
tally
})
.reduce_with(FillTally::merge)
.unwrap_or_else(|| FillTally::new(table_rows, lookups_x_task))
}
#[cfg(test)]
mod tests {
use super::*;
const CHUNKINGS: &[&[usize]] = &[
&[],
&[0],
&[1],
&[0, 0, 5, 0, 0],
&[3, 1, 4, 1, 5, 9, 2, 6],
&[1, 1, 1, 1, 1, 1, 1],
&[100, 0, 1, 37],
&[1000, 1, 1000],
];
const _: () = assert!(2001 < REGION_ROWS);
fn chunked(lengths: &[usize]) -> Vec<Vec<BinaryInput>> {
let mut next = 0u64;
lengths
.iter()
.map(|&len| {
(0..len)
.map(|_| {
next += 1;
BinaryInput::new(0, next - 1, 0)
})
.collect()
})
.collect()
}
#[test]
fn the_sparse_tally_matches_a_serial_count() {
const TABLE_ROWS: u64 = 8_781_824;
let rows: Vec<u64> = (0..40_000u64)
.map(|i| if i % 3 == 0 { i % 5_000 } else { (i * 2_654_435_761) % TABLE_ROWS })
.chain([0, TABLE_ROWS - 1, REGION_ROWS as u64, REGION_ROWS as u64 - 1])
.collect();
let mut expected = std::collections::HashMap::new();
for &row in &rows {
*expected.entry(row).or_insert(0u32) += 1;
}
for lookups in [0, 40_000, 400_000, 40_000_000] {
let mut merged: Option<SparseTally> = None;
for part in rows.chunks(rows.len().div_ceil(7)) {
let mut tally = SparseTally::new(TABLE_ROWS, lookups);
for &row in part {
tally.inc(row);
}
merged = Some(match merged {
None => tally,
Some(acc) => acc.merge(tally),
});
}
let merged = merged.expect("the rows are not empty");
assert_eq!(merged.counts(), expected, "lookups {lookups}");
}
}
#[test]
fn the_spill_is_bounded_by_the_promotion_threshold() {
const TABLE_ROWS: u64 = 8_781_824;
let lookups = 1_000_000;
let mut tally = SparseTally::new(TABLE_ROWS, lookups);
let promote = tally.promote as usize;
let regions = tally.regions.len();
for i in 0..lookups as u64 {
tally.inc((i % regions as u64) * REGION_ROWS as u64 + i % 251);
}
assert!(
tally.spill.len() <= regions * promote,
"spilled {} rows, more than the {regions} regions x {promote} bound",
tally.spill.len(),
);
assert!(tally.regions.iter().all(|region| !region.is_empty()), "a region stayed cold");
}
#[test]
#[should_panic(expected = "is past the")]
fn a_row_past_a_ragged_table_is_rejected() {
let ragged = REGION_ROWS as u64 + 7;
assert_ne!(ragged % REGION_ROWS as u64, 0, "the table must not end on a region boundary");
let mut tally = SparseTally::new(ragged, 0);
tally.inc(ragged);
}
#[test]
fn the_flush_range_stops_at_the_table() {
for table_rows in [
crate::BinaryBasicTableSM::TABLE_ROWS,
crate::BinaryExtensionTableSM::TABLE_ROWS,
REGION_ROWS as u64,
REGION_ROWS as u64 + 1,
1,
] {
let tally = SparseTally::new(table_rows, 0);
let regions = tally.regions.len() as u64;
assert!(
(regions - 1) * (REGION_ROWS as u64) < table_rows,
"{table_rows} rows got a region that holds nothing"
);
let mut covered = 0u64;
for region in 0..regions {
let start = region * REGION_ROWS as u64;
covered += (REGION_ROWS as u64).min(table_rows - start);
}
assert_eq!(covered, table_rows, "{table_rows} rows");
}
}
#[test]
fn every_operation_reaches_its_slot_in_order() {
for lengths in CHUNKINGS {
for lanes_x_row in [1usize, 2, 3, 4, 8] {
let inputs = chunked(lengths);
let total: usize = lengths.iter().sum();
let rows_used = total.div_ceil(lanes_x_row);
let mut rows = vec![vec![u64::MAX; lanes_x_row]; rows_used];
let tally = fill_slots_and_tally(
&mut rows,
&inputs,
total,
lanes_x_row,
REGION_ROWS as u64,
1,
|row, lane, input, tally| {
row[lane] = input.a;
tally.inc(input.a % REGION_ROWS as u64);
},
|row, lane| row[lane] = u64::MAX,
);
let seen: Vec<u64> =
rows.iter().flatten().copied().filter(|&v| v != u64::MAX).collect();
assert_eq!(
seen,
(0..total as u64).collect::<Vec<_>>(),
"{lengths:?} at {lanes_x_row} lanes"
);
let padded: usize = rows.iter().flatten().filter(|&&v| v == u64::MAX).count();
assert_eq!(
padded,
rows_used * lanes_x_row - total,
"{lengths:?} at {lanes_x_row} lanes"
);
assert_eq!(
tally.table.counts(),
(0..total as u64).map(|index| (index, 1)).collect(),
"{lengths:?} at {lanes_x_row} lanes"
);
}
}
}
#[test]
fn the_cursor_starts_at_any_slot() {
for lengths in CHUNKINGS {
let inputs = chunked(lengths);
let starts = chunk_starts(&inputs);
let total: usize = lengths.iter().sum();
assert_eq!(starts[inputs.len()], total, "{lengths:?}");
for slot in 0..=total {
let mut cursor = InputCursor::new(&inputs, &starts, slot);
for expected in slot..total {
let input = cursor.next().expect("an operation is left");
assert_eq!(input.a, expected as u64, "{lengths:?} from slot {slot}");
}
assert!(cursor.next().is_none(), "{lengths:?} from slot {slot}: past the end");
}
}
}
#[test]
fn the_tally_matches_a_serial_count() {
for inputs_per_row in [1usize, 3, 5] {
for count in [0usize, 1, 7, 1000] {
let inputs: Vec<u64> = (0..count as u64).map(|i| (i * 7) % 300).collect();
let rows = count.div_ceil(inputs_per_row);
let mut filled = vec![0u64; rows];
let multiplicities =
fill_and_tally(&mut filled, &inputs, inputs_per_row, |row, row_inputs, m| {
*row = row_inputs.len() as u64;
for &input in row_inputs {
m[input as usize] += 1;
}
});
let mut expected = vec![0u32; RANGE_16_BITS];
for &input in &inputs {
expected[input as usize] += 1;
}
assert_eq!(multiplicities, expected, "{count} inputs, {inputs_per_row} per row");
assert_eq!(filled.iter().sum::<u64>(), count as u64);
}
}
}
#[test]
fn the_split_never_exceeds_the_thread_count() {
let threads = rayon::current_num_threads().max(1);
for rows in [1usize, 2, threads - 1, threads, threads + 1, 7 * threads + 3, 100_000] {
let rows_per_task = rows.div_ceil(threads).max(1);
assert!(
rows.div_ceil(rows_per_task) <= threads,
"{rows} rows split into {} chunks, more than the {threads} threads",
rows.div_ceil(rows_per_task),
);
}
}
#[test]
#[should_panic(expected = "the rows must hold exactly")]
fn mismatched_rows_and_inputs_are_an_error() {
fill_and_tally(&mut [0u64; 3], &[0u64; 10], 3, |_, _, _| {});
}
#[test]
fn nothing_to_fill_tallies_nothing() {
let multiplicities = fill_and_tally(&mut [0u64; 0], &[0u64; 0], 4, |_, _, m| m[1] += 1);
assert_eq!(multiplicities.len(), RANGE_16_BITS);
assert!(multiplicities.iter().all(|&m| m == 0));
}
fn skewed(values: &[u64], end: usize) -> SkewedTally {
let mut tally = SkewedTally::default();
for (slot, &value) in values.iter().enumerate() {
tally.at_row(slot);
tally.inc(0, value);
}
tally.finish(end);
tally
}
#[test]
fn the_skewed_tally_separates_the_zeros_from_the_rest() {
assert_eq!(skewed(&[], 0).counted(), (0, vec![], vec![]), "an untouched tally is empty");
let tally = skewed(&[0, 0, 7, 0, 0x123456, 0, 0], 7);
assert_eq!(tally.counted(), (5, vec![7, 0x123456], vec![]));
}
#[test]
fn merging_skewed_tallies_adds_both_sides() {
let merged = skewed(&[0, 0, 9], 3).merge(skewed(&[0, 4, 0, 0], 4));
assert_eq!(merged.counted(), (5, vec![9, 4], vec![]));
let full = vec![1u64; SKEWED_OTHERS];
let merged = skewed(&full, full.len()).merge(skewed(&full, full.len()));
assert_eq!(merged.counted(), (0, vec![1; 2 * SKEWED_OTHERS], vec![]));
}
#[allow(clippy::single_range_in_vec_init)]
#[test]
fn a_skewed_tally_that_stops_hands_back_only_what_is_left() {
let mut values = vec![3u64; SKEWED_OTHERS + 1];
values.extend([0, 0, 5]);
let tally = skewed(&values, 1000);
assert_eq!(
tally.counted(),
(0, vec![3; SKEWED_OTHERS], vec![SKEWED_OTHERS..1000]),
"the kept values stand and the rest of the run goes back",
);
let merged = skewed(&[0, 0, 8], 3).merge(tally);
let kept = [vec![8u64], vec![3; SKEWED_OTHERS]].concat();
assert_eq!(merged.counted(), (2, kept, vec![SKEWED_OTHERS..1000]));
}
#[test]
fn the_slots_handed_back_start_where_the_counting_stopped() {
let values = vec![7u64; SKEWED_OTHERS + 20];
let tally = skewed(&values, values.len());
let (_, kept, unfinished) = tally.counted();
assert_eq!(unfinished, vec![SKEWED_OTHERS..values.len()]);
assert_eq!(
kept.len() + unfinished.iter().map(|range| range.len()).sum::<usize>(),
values.len(),
"every slot is either counted or handed back, and none is both",
);
}
#[test]
fn the_fill_tallies_the_range_checks_too() {
for lengths in CHUNKINGS {
let inputs = chunked(lengths);
let total: usize = lengths.iter().sum();
let mut rows = vec![0u64; total];
let tally = fill_slots_and_tally(
&mut rows,
&inputs,
total,
1,
REGION_ROWS as u64,
1,
|row, lane, input, tally| {
*row = input.a;
tally.inc(input.a % REGION_ROWS as u64);
tally.inc_range(lane, if input.a % 101 == 100 { input.a } else { 0 });
},
|_, _| unreachable!("one operation to a row leaves nothing to pad"),
);
let expected: Vec<u64> = (0..total as u64).filter(|index| index % 101 == 100).collect();
let (zeros, mut kept, unfinished) = tally.range.counted();
kept.sort_unstable();
assert_eq!((zeros, kept), ((total - expected.len()) as u64, expected), "{lengths:?}");
assert!(unfinished.is_empty(), "{lengths:?}: far too few to reach the bound");
}
}
#[test]
fn the_fill_hands_back_the_slots_it_did_not_count() {
for lengths in CHUNKINGS {
let inputs = chunked(lengths);
let total: usize = lengths.iter().sum();
let mut rows = vec![0u64; total];
let tally = fill_slots_and_tally(
&mut rows,
&inputs,
total,
1,
REGION_ROWS as u64,
1,
|row, lane, input, tally| {
*row = input.a;
tally.inc(input.a % REGION_ROWS as u64);
tally.inc_range(lane, input.a + 1);
},
|_, _| unreachable!("one operation to a row leaves nothing to pad"),
);
let (zeros, kept, unfinished) = tally.range.counted();
assert_eq!(zeros, 0, "{lengths:?}");
let mut walked = Vec::new();
for_each_operation_in(&inputs, &unfinished, |input| walked.push(input.a + 1));
let mut seen = [kept, walked].concat();
seen.sort_unstable();
assert_eq!(seen, (1..=total as u64).collect::<Vec<_>>(), "{lengths:?}");
}
}
#[allow(clippy::single_range_in_vec_init)]
#[test]
fn walking_slot_ranges_visits_exactly_them() {
for lengths in CHUNKINGS {
let inputs = chunked(lengths);
let total: usize = lengths.iter().sum();
let mut walked = Vec::new();
for_each_operation_in(&inputs, &[], |input| walked.push(input.a));
assert!(walked.is_empty(), "{lengths:?}: no ranges, no operations");
for_each_operation_in(&inputs, &[0..total], |input| walked.push(input.a));
assert_eq!(walked, (0..total as u64).collect::<Vec<_>>(), "{lengths:?}: the whole run");
if total >= 4 {
let (first, second) = (1..total / 2, total / 2 + 1..total);
let mut walked = Vec::new();
for_each_operation_in(&inputs, &[first.clone(), second.clone()], |input| {
walked.push(input.a as usize)
});
assert_eq!(walked, first.chain(second).collect::<Vec<_>>(), "{lengths:?}");
}
}
}
}