Skip to content

Commit 3733b59

Browse files
committed
Merge remote-tracking branch 'origin/feat/disk-spill-v2-parallel-r2' into feat/ethrex-empty-block-v2
2 parents 0e99f2f + 914b632 commit 3733b59

21 files changed

Lines changed: 1463 additions & 212 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ tikv-jemallocator = "0.6"
1313
tikv-jemalloc-ctl = { version = "0.6", features = ["stats"], optional = true }
1414

1515
[features]
16+
default = ["disk-spill"]
1617
jemalloc-stats = ["dep:tikv-jemalloc-ctl"]
18+
disk-spill = ["prover/disk-spill"]
1719
instruments = ["prover/instruments"]

bin/cli/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ fn cmd_prove(elf_path: PathBuf, output_path: PathBuf, blowup: Option<u8>, time:
262262
#[cfg(feature = "jemalloc-stats")]
263263
let tracker = heap_tracker::HeapTracker::start();
264264

265+
if cfg!(feature = "disk-spill") {
266+
eprintln!("Disk-spill: enabled");
267+
}
268+
265269
let start = Instant::now();
266270
let proof = match blowup {
267271
Some(b) => {

crypto/crypto/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ serde = { version = "1.0", default-features = false, features = [
1818
rayon = { version = "1.8.0", optional = true }
1919
rand = { version = "0.8.5", default-features = false }
2020
rand_chacha = { version = "0.3.1", default-features = false }
21+
memmap2 = { version = "0.9", optional = true }
22+
tempfile = { version = "3", optional = true }
2123

2224
[dev-dependencies]
2325
math = { path = "../math", features = ["test-utils"] }
@@ -31,4 +33,5 @@ asm = ["sha3/asm"]
3133
std = ["math/std", "sha3/std", "serde?/std"]
3234
serde = ["dep:serde"]
3335
parallel = ["dep:rayon"]
36+
disk-spill = ["std", "dep:memmap2", "dep:tempfile"]
3437
alloc = []

crypto/crypto/src/merkle_tree/merkle.rs

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ impl Display for Error {
2222
#[cfg(feature = "std")]
2323
impl std::error::Error for Error {}
2424

25+
/// File-backed mmap storage for Merkle tree nodes.
26+
///
27+
/// After `spill_nodes_to_disk()`, the in-memory node vector is freed and
28+
/// node access goes through this mmap instead.
29+
#[cfg(feature = "disk-spill")]
30+
pub(crate) struct MmapNodeBacking {
31+
mmap: memmap2::Mmap,
32+
/// Owns the file descriptor backing the mmap. Dropping it would close
33+
/// the descriptor and invalidate the mmap.
34+
_file: std::fs::File,
35+
node_count: usize,
36+
node_size: usize,
37+
}
38+
2539
/// The struct for the Merkle tree, consisting of the root and the nodes.
2640
/// A typical tree would look like this
2741
/// root
@@ -31,11 +45,14 @@ impl std::error::Error for Error {}
3145
/// leaf 1 leaf 2 leaf 3 leaf 4
3246
/// The bottom leafs correspond to the hashes of the elements, while each upper
3347
/// layer contains the hash of the concatenation of the daughter nodes.
34-
#[derive(Clone)]
48+
#[cfg_attr(not(feature = "disk-spill"), derive(Clone))]
3549
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3650
pub struct MerkleTree<B: IsMerkleTreeBackend> {
3751
pub root: B::Node,
3852
nodes: Vec<B::Node>,
53+
#[cfg(feature = "disk-spill")]
54+
#[cfg_attr(feature = "serde", serde(skip))]
55+
mmap_backing: Option<MmapNodeBacking>,
3956
}
4057

4158
const ROOT: usize = 0;
@@ -78,14 +95,44 @@ where
7895
Some(MerkleTree {
7996
root: nodes[ROOT].clone(),
8097
nodes,
98+
#[cfg(feature = "disk-spill")]
99+
mmap_backing: None,
81100
})
82101
}
83102

103+
/// Total number of nodes in the tree (inner + leaves).
104+
#[inline]
105+
fn node_count(&self) -> usize {
106+
#[cfg(feature = "disk-spill")]
107+
if let Some(ref backing) = self.mmap_backing {
108+
return backing.node_count;
109+
}
110+
self.nodes.len()
111+
}
112+
113+
/// Access a node by index, returning a reference.
114+
///
115+
/// Returns `None` if `idx` is out of bounds.
116+
#[inline]
117+
fn node_get(&self, idx: usize) -> Option<&B::Node> {
118+
#[cfg(feature = "disk-spill")]
119+
if let Some(ref backing) = self.mmap_backing {
120+
if idx < backing.node_count {
121+
// SAFETY: spill_nodes_to_disk writes self.nodes as contiguous bytes
122+
// to this mmap and asserts align_of::<B::Node>() == 1 at compile time.
123+
let ptr = unsafe { backing.mmap.as_ptr().add(idx * backing.node_size) };
124+
return Some(unsafe { &*(ptr as *const B::Node) });
125+
}
126+
return None;
127+
}
128+
self.nodes.get(idx)
129+
}
130+
84131
/// Returns a Merkle proof for the element/s at position pos
85132
/// For example, give me an inclusion proof for the 3rd element in the
86133
/// Merkle tree
87134
pub fn get_proof_by_pos(&self, pos: usize) -> Option<Proof<B::Node>> {
88-
let pos = pos + self.nodes.len() / 2;
135+
let pos = pos + self.node_count() / 2;
89136
let Ok(merkle_path) = self.build_merkle_path(pos) else {
90137
return None;
91138
};
@@ -101,12 +148,12 @@ where
101148
/// Returns the Merkle path for the element/s for the leaf at position pos
102149
fn build_merkle_path(&self, pos: usize) -> Result<Vec<B::Node>, Error> {
103150
// Pre-allocate based on tree depth (log2 of tree size)
104-
let tree_depth = (self.nodes.len() + 1).ilog2() as usize;
151+
let tree_depth = (self.node_count() + 1).ilog2() as usize;
105152
let mut merkle_path = Vec::with_capacity(tree_depth);
106153
let mut pos = pos;
107154

108155
while pos != ROOT {
109-
let Some(node) = self.nodes.get(sibling_index(pos)) else {
156+
let Some(node) = self.node_get(sibling_index(pos)) else {
110157
// out of bounds, exit returning the current merkle_path
111158
return Err(Error::OutOfBounds);
112159
};
@@ -141,7 +188,7 @@ where
141188
return Err(Error::EmptyPositionList);
142189
}
143190

144-
let num_leaves = (self.nodes.len() + 1).div_ceil(2);
191+
let num_leaves = (self.node_count() + 1).div_ceil(2);
145192

146193
// Validate all positions are within bounds
147194
for &pos in pos_list {
@@ -154,15 +201,19 @@ where
154201
// of the leaves.
155202
let leaf_positions = pos_list
156203
.iter()
157-
.map(|pos| pos + self.nodes.len() / 2)
204+
.map(|pos| pos + self.node_count() / 2)
158205
.collect::<Vec<usize>>();
159206
// We get the positions of the nodes for the batch proof.
160207
let batch_auth_path_positions = self.get_batch_auth_path_positions(&leaf_positions);
161208

162209
// We get the nodes for the batch proof.
163210
let batch_auth_path_nodes = batch_auth_path_positions
164211
.iter()
165-
.map(|pos| self.nodes[*pos].clone())
212+
.map(|pos| {
213+
self.node_get(*pos)
214+
.expect("batch auth path position in bounds")
215+
.clone()
216+
})
166217
.collect();
167218

168219
Ok(BatchProof {
@@ -188,7 +239,7 @@ where
188239
let mut obtainable: BTreeSet<usize> = leaf_positions.iter().cloned().collect();
189240

190241
// Number of levels in tree
191-
let num_levels = (self.nodes.len() + 1).ilog2();
242+
let num_levels = (self.node_count() + 1).ilog2();
192243

193244
// Iter lefevel-by-level from leaves to root.
194245
for _ in 0..num_levels - 1 {
@@ -217,4 +268,57 @@ where
217268
// This makes the proof ordered from bottom (nodes closer to leaves) to top (nodes loser to root).
218269
auth_path_set.into_iter().rev().collect()
219270
}
271+
272+
/// Write tree nodes to a temp file, mmap it, and free the in-memory vector.
273+
/// Node access methods read from the mmap after this call.
274+
#[cfg(feature = "disk-spill")]
275+
pub fn spill_nodes_to_disk(&mut self) -> std::io::Result<()>
276+
where
277+
B::Node: Copy,
278+
{
279+
const {
280+
assert!(
281+
align_of::<B::Node>() == 1,
282+
"B::Node must have alignment 1 for mmap safety"
283+
)
284+
}
285+
use std::io::Write;
286+
287+
if self.nodes.is_empty() {
288+
return Ok(());
289+
}
290+
291+
let node_size = core::mem::size_of::<B::Node>();
292+
let node_count = self.nodes.len();
293+
let total_bytes = node_count * node_size;
294+
295+
let file = tempfile::tempfile()?;
296+
file.set_len(total_bytes as u64)?;
297+
{
298+
let mut writer = std::io::BufWriter::new(&file);
299+
// SAFETY: B::Node is a plain byte array ([u8; N]), so casting
300+
// the contiguous Vec to a byte slice is valid.
301+
let bytes = unsafe {
302+
core::slice::from_raw_parts(self.nodes.as_ptr() as *const u8, total_bytes)
303+
};
304+
writer.write_all(bytes)?;
305+
writer.flush()?;
306+
}
307+
308+
// SAFETY: tempfile() creates an anonymous file with no filesystem path,
309+
// so no other process can open or modify it.
310+
let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
311+
312+
// Free the heap allocation
313+
self.nodes = Vec::new();
314+
315+
self.mmap_backing = Some(MmapNodeBacking {
316+
mmap,
317+
_file: file,
318+
node_count,
319+
node_size,
320+
});
321+
322+
Ok(())
323+
}
220324
}

crypto/math/src/field/element.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use super::traits::{IsPrimeField, IsSubFieldOf, LegendreSymbol};
4040

4141
/// A field element with operations algorithms defined in `F`
4242
#[allow(clippy::derived_hash_with_manual_eq)]
43+
#[repr(transparent)]
4344
#[derive(Debug, Clone, Hash, Copy)]
4445
pub struct FieldElement<F: IsField> {
4546
value: F::BaseType,

crypto/stark/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ itertools = "0.11.0"
2222
# Parallelization crates
2323
rayon = { version = "1.8.0", optional = true }
2424

25+
# Disk-spill: mmap LDE data to reduce heap memory during proving
26+
memmap2 = { version = "0.9", optional = true }
27+
tempfile = { version = "3", optional = true }
28+
libc = { version = "0.2", optional = true }
29+
2530
# wasm
2631
wasm-bindgen = { version = "0.2", optional = true }
2732
serde-wasm-bindgen = { version = "0.5", optional = true }
@@ -40,6 +45,7 @@ instruments = [] # This enab
4045
debug-checks = [] # Enables validate_trace + bus balance report in prover
4146
parallel = ["dep:rayon", "crypto/parallel"]
4247
wasm = ["dep:wasm-bindgen", "dep:serde-wasm-bindgen", "dep:web-sys"]
48+
disk-spill = ["dep:memmap2", "dep:tempfile", "dep:libc", "crypto/disk-spill"]
4349

4450

4551
[package.metadata.wasm-pack.profile.dev]

crypto/stark/src/fri/fri_commitment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use math::{
44
traits::AsBytes,
55
};
66

7-
#[derive(Clone)]
7+
#[cfg_attr(not(feature = "disk-spill"), derive(Clone))]
88
pub struct FriLayer<F, B>
99
where
1010
F: IsField,

0 commit comments

Comments
 (0)