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

feat(memory): allow more than 1 memory reads in the same clock cycle (same context & word address) #1626

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Added `miden_core::mast::MastForest::advice_map` to load it into the advice provider before the `MastForest` execution (#1574).
- Optimized the computation of the DEEP queries in the recursive verifier (#1594).
- Added validity checks for the inputs to the recursive verifier (#1596).
- Allow multiple memory reads in the same clock cycle (#1626)

## 0.11.0 (2024-11-04)

Expand Down
32 changes: 30 additions & 2 deletions air/src/constraints/chiplets/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ mod tests;
// ================================================================================================

/// The number of constraints on the management of the memory chiplet.
pub const NUM_CONSTRAINTS: usize = 18;
pub const NUM_CONSTRAINTS: usize = 19;
/// The degrees of constraints on the management of the memory chiplet. All constraint degrees are
/// increased by 3 due to the selectors for the memory chiplet.
pub const CONSTRAINT_DEGREES: [usize; NUM_CONSTRAINTS] = [
5, 5, 5, 5, // Enforce that rw, ew, idx0 and idx1 are binary.
7, 6, 9, 8, // Constrain the values in the d inverse column.
8, // Enforce values in ctx, word, clk transition correctly.
7, // Enforce the correct value for the f_scw flag.
8, // Enforce that accesses in same context, word and clock are reads.
9, 9, 9, 9, // Constrain the values in the first row of the chiplet.
9, 9, 9, 9, // Constrain the values in all rows of the chiplet except the first.
];
Expand Down Expand Up @@ -77,6 +78,13 @@ pub fn enforce_constraints<E: FieldElement>(
index +=
enforce_flag_same_context_and_word(frame, &mut result[index..], memory_flag_not_last_row);

// Enforce that accesses in same context, word and clock are reads.
index += enforce_same_context_word_addr_and_clock(
frame,
&mut result[index..],
memory_flag_not_last_row,
);

// Constrain the memory values.
enforce_values(frame, &mut result[index..], memory_flag_not_last_row, memory_flag_first_row);
}
Expand Down Expand Up @@ -156,6 +164,26 @@ fn enforce_flag_same_context_and_word<E: FieldElement>(
1
}

/// Enforces that when memory is accessed in the same context, word, and clock cycle, the access is
/// a read.
fn enforce_same_context_word_addr_and_clock<E: FieldElement>(
frame: &EvaluationFrame<E>,
result: &mut [E],
memory_flag_not_last_row: E,
) -> usize {
// Note: when the context and word address don't change, `d_inv_next` contains the inverse of
// the clock change.
let clk_no_change = binary_not(frame.clk_change() * frame.d_inv_next());

result[0] = memory_flag_not_last_row
* frame.f_scw_next()
* clk_no_change
* binary_not(frame.is_read()) // read in current row
* binary_not(frame.is_read_next()); // read in next row

1
}

/// A constraint evaluation function to enforce that memory is initialized to zero when it is read
/// before being written and that when existing memory values are read they remain unchanged.
///
Expand Down Expand Up @@ -418,7 +446,7 @@ impl<E: FieldElement> EvaluationFrameExt<E> for &EvaluationFrame<E> {

#[inline(always)]
fn clk_change(&self) -> E {
self.change(MEMORY_CLK_COL_IDX) - E::ONE
self.change(MEMORY_CLK_COL_IDX)
}

#[inline(always)]
Expand Down
2 changes: 1 addition & 1 deletion air/src/constraints/chiplets/memory/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ fn get_test_frame(

// Set the delta in the next row according to the specified delta type.
let delta: u64 = match delta_type {
MemoryTestDeltaType::Clock => delta_row[MemoryTestDeltaType::Clock as usize] - 1,
MemoryTestDeltaType::Clock => delta_row[MemoryTestDeltaType::Clock as usize],
MemoryTestDeltaType::Context => delta_row[MemoryTestDeltaType::Context as usize],
MemoryTestDeltaType::Word => delta_row[MemoryTestDeltaType::Word as usize],
};
Expand Down
16 changes: 11 additions & 5 deletions docs/src/design/chiplets/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,17 @@ where:
- `word_addr` contains the memory address of the first element in the word. Values in this column must increase monotonically for a given context but there can be gaps between two consecutive values of up to $2^{32}$. Values in this column must be divisible by 4. Also, two consecutive values can be the same.
- `idx0` and `idx1` are selector columns used to identify which element in the word is being accessed. Specifically, the index within the word is computed as `idx1 * 2 + idx0`.
- However, when `ew` is set to $1$ (indicating that a word is accessed), these columns are meaningless and are set to $0$.
- `clk` contains clock cycle at which the memory operation happened. Values in this column must increase monotonically for a given context and memory word but there can be gaps between two consecutive values of up to $2^{32}$. In AIR constraint description below, we refer to this column as $i$.
- `clk` contains clock cycle at which the memory operation happened. Values in this column must increase monotonically for a given context and memory word but there can be gaps between two consecutive values of up to $2^{32}$.
- Unlike the previously described approaches, we allow `clk` to be constant in the same context/word address, with the restriction that when this is the case, then only reads are allowed.
- `v0, v1, v2, v3` columns contain field elements stored at a given context/word/clock cycle after the memory operation.
- Columns `d0` and `d1` contain lower and upper $16$ bits of the delta between two consecutive context IDs, addresses, or clock cycles. Specifically:
- When the context changes within a frame, these columns contain $(ctx' - ctx)$ in the "next" row.
- When the context remains the same but the word address changes within a frame, these columns contain $(a' - a)$ in the "next" row.
- When both the context and the word address remain the same within a frame, these columns contain $(clk' - clk - 1)$ in the "next" row.
- When both the context and the word address remain the same within a frame, these columns contain $(clk' - clk)$ in the "next" row.
- Column `t` contains the inverse of the delta between two consecutive context IDs, addresses, or clock cycles. Specifically:
- When the context changes within a frame, this column contains the inverse of $(ctx' - ctx)$ in the "next" row.
- When the context remains the same but the word address changes within a frame, this column contains the inverse of $(a' - a)$ in the "next" row.
- When both the context and the word address remain the same within a frame, this column contains the inverse of $(clk' - clk - 1)$ in the "next" row.
- When both the context and the word address remain the same within a frame, this column contains the inverse of $(clk' - clk)$ in the "next" row.
- Column `f_scw` stands for "flag same context and word address", which is set to $1$ when the current and previous rows have the same context and word address, and $0$ otherwise.

For every memory access operation (i.e., read or write a word or element), a new row is added to the memory table. If neither `ctx` nor `addr` have changed, the `v` columns are set to equal the values from the previous row (except for any element written to). If `ctx` or `addr` have changed, then the `v` columns are initialized to $0$ (except for any element written to).
Expand Down Expand Up @@ -265,10 +266,15 @@ To enforce the values of context ID, word address, and clock cycle grow monotoni
f_{mem\_nl} \cdot \left(n_0 \cdot \Delta ctx + (1 - n_0) \cdot (n_1 \cdot \Delta a + (1 - n_1) \cdot \Delta clk) \right) - (2^{16} \cdot d_1' + d_0') = 0 \text{ | degree} = 8
$$

Where $\Delta clk = clk' - clk - 1$.

In addition to this constraint, we also need to make sure that the values in registers $d_0$ and $d_1$ are less than $2^{16}$, and this can be done with [range checks](../range.md).

Next, we need to ensure that when the context, word address and clock are constant in a frame, then only read operations are allowed in that clock cycle.

>$$
f_{mem\_nl} \cdot f_{scw}' \cdot (1 - \Delta clk \cdot t') \cdot (1 - rw) \cdot (1 - rw') = 0 \text{ | degree} = 8
$$


Next, for all frames where the "current" and "next" rows are in the chiplet, we need to ensure that the value of the `f_scw` column in the "next" row is set to $1$ when the context and word address are the same, and $0$ otherwise.

>$$
Expand Down
13 changes: 2 additions & 11 deletions miden/tests/integration/operations/io_ops/mem_ops.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
use processor::ContextId;
use prover::ExecutionError;
use test_utils::expect_exec_error_matches;
use vm_core::FieldElement;

use super::{apply_permutation, build_op_test, build_test, Felt, ToElements, TRUNCATE_STACK_PROC};

// LOADING SINGLE ELEMENT ONTO THE STACK (MLOAD)
Expand Down Expand Up @@ -237,7 +232,7 @@ fn read_after_write() {
// MISC
// ================================================================================================

/// Ensures that the processor returns an error when 2 memory operations occur in the same context,
/// Ensures that the processor returns no error when 2 memory operations occur in the same context,
/// at the same address, and in the same clock cycle (which is what RCOMBBASE does when `stack[13] =
/// stack[14] = 0`).
#[test]
Expand All @@ -246,9 +241,5 @@ fn mem_reads_same_clock_cycle() {

let test = build_test!(asm_op);

expect_exec_error_matches!(
test,
ExecutionError::DuplicateMemoryAccess{ctx, addr, clk }
if ctx == ContextId::from(0) && addr == 0 && clk == Felt::ONE
);
test.prove_and_verify(vec![0], false);
}
4 changes: 2 additions & 2 deletions processor/src/chiplets/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ impl Memory {
} else if prev_addr != addr {
u64::from(addr - prev_addr)
} else {
clk - prev_clk - 1
clk - prev_clk
};

let (delta_hi, delta_lo) = split_u32_into_u16(delta);
Expand Down Expand Up @@ -359,7 +359,7 @@ impl Memory {
} else if prev_addr != felt_addr {
felt_addr - prev_addr
} else {
clk - prev_clk - ONE
clk - prev_clk
};

let (delta_hi, delta_lo) = split_element_u32_into_u16(delta);
Expand Down
30 changes: 20 additions & 10 deletions processor/src/chiplets/memory/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ impl MemorySegmentTrace {
let (word_addr, addr_idx_in_word) = addr_to_word_addr_and_idx(addr);

match self.0.entry(word_addr) {
// If this is the first access to the ctx/word pair, then all values in the word are
// initialized to 0, except for when the address being written to.
Entry::Vacant(vacant_entry) => {
// If this is the first access to the ctx/word pair, then all values in the word
// are initialized to 0, except for the address being written.
let word = {
let mut word = Word::default();
word[addr_idx_in_word as usize] = value;
Expand All @@ -185,12 +185,15 @@ impl MemorySegmentTrace {
vacant_entry.insert(vec![access]);
Ok(())
},
// If the ctx/word pair has been accessed before, then the values in the word are the
// same as the previous access, except for when the address being written to.
Entry::Occupied(mut occupied_entry) => {
// If the ctx/word pair has been accessed before, then the values in the word are
// the same as the previous access, except for the address being written.
let addr_trace = occupied_entry.get_mut();
if addr_trace.last().expect("empty address trace").clk() == clk {
Err(ExecutionError::DuplicateMemoryAccess { ctx, addr, clk })
// The same address is accessed more than once in the same clock cycle. This is
// an error, since this access is a write, and the only valid accesses are
// reads when in the same clock cycle.
Err(ExecutionError::IllegalMemoryAccess { ctx, addr, clk })
} else {
let word = {
let mut last_word = addr_trace.last().expect("empty address trace").word();
Expand Down Expand Up @@ -236,16 +239,19 @@ impl MemorySegmentTrace {
let access =
MemorySegmentAccess::new(clk, MemoryOperation::Write, MemoryAccessType::Word, word);
match self.0.entry(word_addr) {
// All values in the word are set to the word being written.
Entry::Vacant(vacant_entry) => {
// All values in the word are set to the word being written.
vacant_entry.insert(vec![access]);
Ok(())
},
// All values in the word are set to the word being written.
Entry::Occupied(mut occupied_entry) => {
// All values in the word are set to the word being written.
let addr_trace = occupied_entry.get_mut();
if addr_trace.last().expect("empty address trace").clk() == clk {
Err(ExecutionError::DuplicateMemoryAccess { ctx, addr, clk })
// The same address is accessed more than once in the same clock cycle. This is
// an error, since this access is a write, and the only valid accesses are
// reads when in the same clock cycle.
Err(ExecutionError::IllegalMemoryAccess { ctx, addr, clk })
} else {
addr_trace.push(access);
Ok(())
Expand Down Expand Up @@ -301,8 +307,12 @@ impl MemorySegmentTrace {
// If the ctx/word pair has been accessed before, then the values in the word are
// the same as the previous access.
let addr_trace = occupied_entry.get_mut();
if addr_trace.last().expect("empty address trace").clk() == clk {
Err(ExecutionError::DuplicateMemoryAccess { ctx, addr: word_addr, clk })
let last_access = addr_trace.last().expect("empty address trace");
if last_access.clk() == clk && last_access.operation() == MemoryOperation::Write {
// The same address is accessed more than once in the same clock cycle. This is
// an error, since the previous access was a write, and the only valid accesses
// are reads when in the same clock cycle.
Err(ExecutionError::IllegalMemoryAccess { ctx, addr: word_addr, clk })
} else {
let last_word = addr_trace.last().expect("empty address trace").word();
let access = MemorySegmentAccess::new(
Expand Down
27 changes: 13 additions & 14 deletions processor/src/chiplets/memory/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ fn read_trace_row(trace: &[Vec<Felt>], step: usize) -> [Felt; MEMORY_TRACE_WIDTH
row
}

/// Note: For this to work properly, the context and address accessed in the first row *must be* 0.
fn build_trace_row(
memory_access: MemoryAccess,
prev_row: [Felt; MEMORY_TRACE_WIDTH],
Expand Down Expand Up @@ -557,20 +558,18 @@ fn build_trace_row(
row[V_COL_RANGE.start + 2] = word_values[2];
row[V_COL_RANGE.start + 3] = word_values[3];

if prev_row != [ZERO; MEMORY_TRACE_WIDTH] {
let delta = if row[CTX_COL_IDX] != prev_row[CTX_COL_IDX] {
row[CTX_COL_IDX] - prev_row[CTX_COL_IDX]
} else if row[WORD_COL_IDX] != prev_row[WORD_COL_IDX] {
row[WORD_COL_IDX] - prev_row[WORD_COL_IDX]
} else {
row[CLK_COL_IDX] - prev_row[CLK_COL_IDX] - ONE
};

let (hi, lo) = super::split_element_u32_into_u16(delta);
row[D0_COL_IDX] = lo;
row[D1_COL_IDX] = hi;
row[D_INV_COL_IDX] = delta.inv();
}
let delta = if row[CTX_COL_IDX] != prev_row[CTX_COL_IDX] {
row[CTX_COL_IDX] - prev_row[CTX_COL_IDX]
} else if row[WORD_COL_IDX] != prev_row[WORD_COL_IDX] {
row[WORD_COL_IDX] - prev_row[WORD_COL_IDX]
} else {
row[CLK_COL_IDX] - prev_row[CLK_COL_IDX]
};

let (hi, lo) = super::split_element_u32_into_u16(delta);
row[D0_COL_IDX] = lo;
row[D1_COL_IDX] = hi;
row[D_INV_COL_IDX] = delta.inv();

if row[WORD_COL_IDX] == prev_row[WORD_COL_IDX] && row[CTX_COL_IDX] == prev_row[CTX_COL_IDX] {
row[FLAG_SAME_CONTEXT_AND_WORD] = ONE;
Expand Down
4 changes: 2 additions & 2 deletions processor/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ pub enum ExecutionError {
DecoratorNotFoundInForest { decorator_id: DecoratorId },
#[error("division by zero at clock cycle {0}")]
DivideByZero(RowIndex),
#[error("memory address {addr} in context {ctx} accessed twice in clock cycle {clk}")]
DuplicateMemoryAccess { ctx: ContextId, addr: u32, clk: Felt },
#[error("failed to execute the dynamic code block provided by the stack with root {hex}; the block could not be found",
hex = to_hex(.0.as_bytes())
)]
Expand All @@ -60,6 +58,8 @@ pub enum ExecutionError {
},
#[error("failed to generate signature: {0}")]
FailedSignatureGeneration(&'static str),
#[error("memory address {addr} in context {ctx} was read and written, or written twice, in the same clock cycle {clk}")]
IllegalMemoryAccess { ctx: ContextId, addr: u32, clk: Felt },
#[error("Updating FMP register from {0} to {1} failed because {1} is outside of {FMP_MIN}..{FMP_MAX}")]
InvalidFmpValue(Felt, Felt),
#[error("FRI domain segment value cannot exceed 3, but was {0}")]
Expand Down
46 changes: 44 additions & 2 deletions processor/src/operations/io_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,13 @@ impl Process {

#[cfg(test)]
mod tests {
use vm_core::{utils::ToElements, Word, ONE, ZERO};
use vm_core::{assert_matches, utils::ToElements, Word, ONE, ZERO};

use super::{
super::{super::AdviceProvider, Operation, MIN_STACK_DEPTH},
Felt, Host, Process,
};
use crate::{AdviceSource, ContextId, DefaultHost};
use crate::{AdviceSource, ContextId, DefaultHost, ExecutionError};

#[test]
fn op_push() {
Expand Down Expand Up @@ -635,6 +635,48 @@ mod tests {
assert_eq!(expected, process.stack.trace_state());
}

/// Ensures that reading and writing in the same clock cycle results in an error.
#[test]
fn read_and_write_in_same_clock_cycle() {
let mut process = Process::new_dummy_with_decoder_helpers_and_empty_stack();
assert_eq!(0, process.chiplets.memory().num_accessed_words());

// emulate reading and writing in the same clock cycle
process.ensure_trace_capacity();
process.op_mload().unwrap();
assert_matches!(
process.op_mstore(),
Err(ExecutionError::IllegalMemoryAccess { ctx: _, addr: _, clk: _ })
);
}

/// Ensures that writing twice in the same clock cycle results in an error.
#[test]
fn write_twice_in_same_clock_cycle() {
let mut process = Process::new_dummy_with_decoder_helpers_and_empty_stack();
assert_eq!(0, process.chiplets.memory().num_accessed_words());

// emulate reading and writing in the same clock cycle
process.ensure_trace_capacity();
process.op_mstore().unwrap();
assert_matches!(
process.op_mstore(),
Err(ExecutionError::IllegalMemoryAccess { ctx: _, addr: _, clk: _ })
);
}

/// Ensures that reading twice in the same clock cycle does NOT result in an error.
#[test]
fn read_twice_in_same_clock_cycle() {
let mut process = Process::new_dummy_with_decoder_helpers_and_empty_stack();
assert_eq!(0, process.chiplets.memory().num_accessed_words());

// emulate reading in the same clock cycle
process.ensure_trace_capacity();
process.op_mload().unwrap();
process.op_mload().unwrap();
}

// HELPER METHODS
// --------------------------------------------------------------------------------------------

Expand Down
Loading
Loading