Skip to content

Commit 0cebe16

Browse files
committed
Merge m4b/b1 (#1014 d169edd merged into the landing W) into m4b/b2
2 parents bbf1752 + 70eee3e commit 0cebe16

2 files changed

Lines changed: 182 additions & 52 deletions

File tree

‎prover/src/lfm/proof.rs‎

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,32 @@ pub(crate) fn lfm_prove_with_residency(
171171
hasher: HasherKind,
172172
residency: ResidencyMode,
173173
) -> Result<LfmProof, LfmProveError> {
174+
lfm_execute_and_fill(program, arenas, hasher)?.prove(artifacts, options, residency)
175+
}
176+
177+
/// A program executed and its traces filled: everything [`lfm_prove`] does
178+
/// before the card, held for [`LfmFilled::prove`].
179+
///
180+
/// Split out so a driver can run the host phase while the program's artifacts
181+
/// are still being built (they hold the card; the execution and the fill do
182+
/// not read them). The two halves are [`lfm_prove_with_residency`] cut in
183+
/// two, in the same order, on whatever thread calls each.
184+
pub(crate) struct LfmFilled {
185+
traces: LfmTraces,
186+
public_words: Vec<(u32, LfmWord)>,
187+
hasher: HasherKind,
188+
execute_secs: f64,
189+
fill_secs: f64,
190+
exec_split: super::executor::ExecSplit,
191+
}
192+
193+
/// The host half of [`lfm_prove_with_residency`]: execute `program` over
194+
/// `arenas` and fill its traces for `hasher`.
195+
pub(crate) fn lfm_execute_and_fill(
196+
program: &LfmProgram,
197+
arenas: &[Vec<LfmWord>],
198+
hasher: HasherKind,
199+
) -> Result<LfmFilled, LfmProveError> {
174200
// ★ THE SPLIT OF THE `prove` FIELD. The driver's per-node TIMING line prints
175201
// this whole function as one number, and the three statements below are
176202
// three different machines: `execute` is a single-threaded interpreter,
@@ -203,46 +229,79 @@ pub(crate) fn lfm_prove_with_residency(
203229
drop(memory);
204230

205231
let t = Instant::now();
206-
let mut traces = build_traces_with_hasher(program, &records, hasher);
232+
let traces = build_traces_with_hasher(program, &records, hasher);
207233
let fill_secs = t.elapsed().as_secs_f64();
208234
// Same reason, larger: the records are what the fill consumes, and they are
209235
// dead the moment it returns. The traces it produced are the live set from
210236
// here on; holding the records as well doubles the values through the card
211237
// phase.
212238
drop(records);
213-
214-
let t = Instant::now();
215-
let waited_before = super::device_permit::waited_secs();
216-
let proof = prove_traces_with_hasher(
217-
artifacts,
218-
&mut traces,
219-
&public_words,
220-
options,
221-
hasher,
222-
residency,
223-
)
224-
.map_err(LfmProveError::Prover)?;
225-
// The wait is SUBTRACTED rather than left inside, so `multi_prove` means
226-
// the same thing at one worker and at two.
227-
let permit_wait = (super::device_permit::waited_secs() - waited_before).max(0.0);
228-
let multi_prove_secs = (t.elapsed().as_secs_f64() - permit_wait).max(0.0);
229-
230-
LAST_PROVE_SPLIT.with(|c| {
231-
c.set(Some(ProveSplit {
232-
execute: execute_secs,
233-
fill: fill_secs,
234-
multi_prove: multi_prove_secs,
235-
permit_wait,
236-
exec: exec_split,
237-
}))
238-
});
239-
240-
Ok(LfmProof {
241-
proof,
239+
Ok(LfmFilled {
240+
traces,
242241
public_words,
242+
hasher,
243+
execute_secs,
244+
fill_secs,
245+
exec_split,
243246
})
244247
}
245248

249+
impl LfmFilled {
250+
/// The card half of [`lfm_prove_with_residency`]: prove the filled traces
251+
/// against `artifacts`, which must be built for the hasher they were
252+
/// filled with (the same agreement [`lfm_prove_with_hasher`] asserts).
253+
pub(crate) fn prove(
254+
self,
255+
artifacts: &LfmArtifacts,
256+
options: &ProofOptions,
257+
residency: ResidencyMode,
258+
) -> Result<LfmProof, LfmProveError> {
259+
let LfmFilled {
260+
mut traces,
261+
public_words,
262+
hasher,
263+
execute_secs,
264+
fill_secs,
265+
exec_split,
266+
} = self;
267+
assert_eq!(
268+
artifacts.hasher, hasher,
269+
"artifacts were built for {:?} but the traces were filled for {hasher:?}",
270+
artifacts.hasher
271+
);
272+
let t = Instant::now();
273+
let waited_before = super::device_permit::waited_secs();
274+
let proof = prove_traces_with_hasher(
275+
artifacts,
276+
&mut traces,
277+
&public_words,
278+
options,
279+
hasher,
280+
residency,
281+
)
282+
.map_err(LfmProveError::Prover)?;
283+
// The wait is SUBTRACTED rather than left inside, so `multi_prove` means
284+
// the same thing at one worker and at two.
285+
let permit_wait = (super::device_permit::waited_secs() - waited_before).max(0.0);
286+
let multi_prove_secs = (t.elapsed().as_secs_f64() - permit_wait).max(0.0);
287+
288+
LAST_PROVE_SPLIT.with(|c| {
289+
c.set(Some(ProveSplit {
290+
execute: execute_secs,
291+
fill: fill_secs,
292+
multi_prove: multi_prove_secs,
293+
permit_wait,
294+
exec: exec_split,
295+
}))
296+
});
297+
298+
Ok(LfmProof {
299+
proof,
300+
public_words,
301+
})
302+
}
303+
}
304+
246305
/// Proves an already-built trace set against `artifacts`.
247306
///
248307
/// Split out of [`lfm_prove`] so callers that need to inspect or corrupt a

‎prover/src/lfm/whir_block_tests.rs‎

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use super::compiler::{LfmProgram, compile};
2222
use super::executor::execute;
2323
use super::per_table_aggregator::{DerivedChild, LegCells, hint_public_words, publics_arena};
2424
use super::per_table_aggregator_tests::{RealChild, child_arena_words, real_child_timed};
25-
use super::proof::{LfmProof, aggregation_wrap_options, lfm_prove};
25+
use super::proof::{
26+
LfmProof, aggregation_wrap_options, decide_lfm_residency, lfm_execute_and_fill, lfm_prove,
27+
};
2628
use super::whir_block::{
2729
BLOCK_FAN_IN, BlockPartition, LEAF_PERMS_CAP, LeafChecks, WhirBlockPlan, artifacts_of,
2830
block_leaf_arena, emit_share, id_words, leaf_partition, leaf_program_with, out_halves,
@@ -1143,13 +1145,53 @@ struct LevelTiming {
11431145
programs: Vec<(f64, f64)>,
11441146
}
11451147

1148+
/// A value one thread publishes once and others wait for. A publisher that
1149+
/// unwinds before publishing leaves an error behind ([`PublishGuard`]), so no
1150+
/// waiter blocks on a value that will never come.
1151+
struct Published<T>(std::sync::OnceLock<Result<T, String>>);
1152+
1153+
impl<T> Published<T> {
1154+
fn new() -> Self {
1155+
Self(std::sync::OnceLock::new())
1156+
}
1157+
1158+
fn wait(&self) -> Result<&T, String> {
1159+
self.0.wait().as_ref().map_err(Clone::clone)
1160+
}
1161+
1162+
fn take(self) -> Result<T, String> {
1163+
self.0
1164+
.into_inner()
1165+
.unwrap_or_else(|| Err("never published".to_string()))
1166+
}
1167+
}
1168+
1169+
/// Publishes an error on drop unless [`PublishGuard::publish`] ran first.
1170+
struct PublishGuard<'a, T>(&'a Published<T>);
1171+
1172+
impl<T> PublishGuard<'_, T> {
1173+
fn publish(self, value: Result<T, String>) {
1174+
let _ = self.0.0.set(value);
1175+
}
1176+
}
1177+
1178+
impl<T> Drop for PublishGuard<'_, T> {
1179+
fn drop(&mut self) {
1180+
let _ = self.0.0.set(Err("the publisher unwound".to_string()));
1181+
}
1182+
}
1183+
11461184
/// The tree proved the way a prover would run it, with the leaves' programs
11471185
/// emitted beforehand (`leaves`, while phase B ran):
1148-
/// 1. the leaves' artifacts, in parallel;
1186+
/// 1. the leaves' artifacts, in parallel, on a thread of their own (they hold
1187+
/// the card);
11491188
/// 2. the nodes' programs and artifacts — functions of the leaves' artifacts,
11501189
/// not of any proof — on a thread of their own, beside
11511190
/// 3. the leaves proved `siblings` at a time, each harvested without its
1152-
/// verify;
1191+
/// verify: with `beside`, each leaf executes and fills its traces while the
1192+
/// artifacts are built (neither reads them) and waits for them only to
1193+
/// prove; without, it waits for every artifact first (the order before,
1194+
/// `W3_EXEC_BESIDE_ARTIFACTS=0`);
11531195
/// 4. each level above proved over the harvested children.
11541196
///
11551197
/// Returns each level's timing and every proof with its artifacts, level by
@@ -1160,6 +1202,7 @@ fn prove_tree_pipelined(
11601202
proof: &BlockWhirProof,
11611203
leaves: Vec<LfmProgram>,
11621204
siblings: usize,
1205+
beside: bool,
11631206
) -> Result<
11641207
(
11651208
Vec<LevelTiming>,
@@ -1171,27 +1214,32 @@ fn prove_tree_pipelined(
11711214
let wrap = aggregation_wrap_options();
11721215
let words = plan.child_layout().total();
11731216
let t_level = std::time::Instant::now();
1174-
1175-
// 1. the leaves' artifacts.
1176-
let leaf_built: Vec<(super::registry::LfmArtifacts, DerivedChild, f64)> = {
1177-
use rayon::prelude::*;
1178-
leaves
1179-
.par_iter()
1180-
.map(|program| -> Result<_, String> {
1181-
let t = std::time::Instant::now();
1182-
let artifacts = artifacts_of(program, &wrap);
1183-
let derived = DerivedChild::from_artifacts(&artifacts, &wrap, words)?;
1184-
Ok((artifacts, derived, t.elapsed().as_secs_f64()))
1185-
})
1186-
.collect::<Result<_, String>>()?
1187-
};
11881217
let shape = plan.levels();
11891218
let mut timings = Vec::with_capacity(shape.len() + 1);
11901219
let mut proofs = Vec::new();
1220+
let built: Published<Vec<(super::registry::LfmArtifacts, DerivedChild, f64)>> =
1221+
Published::new();
11911222

11921223
let (leaf_proved, nodes) = std::thread::scope(|scope| {
1224+
// 1. the leaves' artifacts.
1225+
scope.spawn(|| {
1226+
use rayon::prelude::*;
1227+
let guard = PublishGuard(&built);
1228+
guard.publish(
1229+
leaves
1230+
.par_iter()
1231+
.map(|program| -> Result<_, String> {
1232+
let t = std::time::Instant::now();
1233+
let artifacts = artifacts_of(program, &wrap);
1234+
let derived = DerivedChild::from_artifacts(&artifacts, &wrap, words)?;
1235+
Ok((artifacts, derived, t.elapsed().as_secs_f64()))
1236+
})
1237+
.collect::<Result<_, String>>(),
1238+
);
1239+
});
11931240
// 2. the nodes' programs, from the leaves' derived shapes.
11941241
let nodes = scope.spawn(|| -> Result<Vec<Vec<TreeNode>>, String> {
1242+
let leaf_built = built.wait()?;
11951243
let mut owned: Vec<Vec<TreeNode>> = Vec::with_capacity(shape.len());
11961244
for (lv, arities) in shape.iter().enumerate() {
11971245
let top = lv + 1 == shape.len();
@@ -1224,15 +1272,23 @@ fn prove_tree_pipelined(
12241272
// 3. the leaves, `siblings` at a time.
12251273
let proved = in_index_order(leaves.len(), siblings, |k| -> Result<_, String> {
12261274
let arenas = block_leaf_arena(plan, proof, k)?;
1275+
if !beside {
1276+
built.wait()?;
1277+
}
12271278
let t = std::time::Instant::now();
1228-
let lfm = lfm_prove(&leaves[k], &leaf_built[k].0, &arenas, &wrap)
1279+
let filled = lfm_execute_and_fill(&leaves[k], &arenas, crate::hash_pin::BLOCK_HASHER)
1280+
.map_err(|e| format!("leaf {k}: {e:?}"))?;
1281+
let artifacts = &built.wait()?[k].0;
1282+
let lfm = filled
1283+
.prove(artifacts, &wrap, decide_lfm_residency())
12291284
.map_err(|e| format!("leaf {k}: {e:?}"))?;
12301285
let prove = t.elapsed().as_secs_f64();
1231-
let child = harvest_child(leaf_built[k].0.clone(), wrap.clone(), &lfm);
1286+
let child = harvest_child(artifacts.clone(), wrap.clone(), &lfm);
12321287
Ok((lfm, child, prove))
12331288
});
12341289
(proved, nodes.join())
12351290
});
1291+
let leaf_built = built.take()?;
12361292
let leaf_proved: Vec<(LfmProof, RealChild, f64)> =
12371293
leaf_proved.into_iter().collect::<Result<_, String>>()?;
12381294
let nodes = nodes.map_err(|_| "the node builder panicked".to_string())??;
@@ -1324,6 +1380,9 @@ fn the_whir_block_tree_on_a_real_block() {
13241380
let leaves = knob("W3_LEAVES");
13251381
let siblings = knob("W3_SIBLINGS").unwrap_or(3);
13261382
let fan_in = knob("W3_FAN_IN").unwrap_or(BLOCK_FAN_IN);
1383+
// `W3_EXEC_BESIDE_ARTIFACTS=0|1` (default 1): the leaves execute and fill
1384+
// while their artifacts are built, or (0, the control) after all of them.
1385+
let beside = knob("W3_EXEC_BESIDE_ARTIFACTS").is_none_or(|v| v != 0);
13271386
// `BLOCK_WHIR_ARGUE=batched|per-table` (production: batched): each group's
13281387
// tables argued together, at the format's bin cap (`BLOCK_WHIR_ARGUE_CAP=k`
13291388
// for 2^k), or each on its own.
@@ -1556,9 +1615,12 @@ fn the_whir_block_tree_on_a_real_block() {
15561615
plan.prepared().len()
15571616
);
15581617

1618+
// The readouts above run on the clock, between the base and the tree: a
1619+
// prover prints none of them, so the whole block is also given without.
1620+
let readouts = t0.elapsed().as_secs_f64() - base;
15591621
let t = std::time::Instant::now();
15601622
let (timings, proofs) =
1561-
prove_tree_pipelined(&plan, &proof, programs, siblings).expect("the tree proves");
1623+
prove_tree_pipelined(&plan, &proof, programs, siblings, beside).expect("the tree proves");
15621624
let top = &proofs.last().expect("a top").1;
15631625
let tree = t.elapsed().as_secs_f64();
15641626
let whole = t0.elapsed().as_secs_f64();
@@ -1575,8 +1637,17 @@ fn the_whir_block_tree_on_a_real_block() {
15751637
);
15761638
}
15771639
println!(
1578-
"W3 RECURSION: {:.2}s after the base (tree {tree:.2}s) · whole block {whole:.2}s",
1579-
whole - base
1640+
"W3 RECURSION: {:.2}s after the base (tree {tree:.2}s) · whole block {whole:.2}s · whole excl. harness readouts {:.2}s (readouts {readouts:.2}s) · leaves execute beside their artifacts {beside}",
1641+
whole - base,
1642+
whole - readouts
1643+
);
1644+
// The top proof's bytes: two runs prove the same ones under
1645+
// LAMBDA_VM_FIXED_TRACE_HASH=1 and LAMBDA_VM_DETERMINISTIC_GRIND=1.
1646+
let top_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&top.proof).expect("serialize the top");
1647+
println!(
1648+
"W3 TOP DIGEST: {} (blake3 of the top proof's {} bytes)",
1649+
&blake3::hash(&top_bytes).to_hex()[..32],
1650+
top_bytes.len()
15801651
);
15811652

15821653
// Off the clock: the harness's checks.

0 commit comments

Comments
 (0)