From 7cbb608ebf3368fc48bfe8673d31b98ebaf6d309 Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:31:55 +0200 Subject: [PATCH 1/6] refactor(lance-fold): consume Mask-RISC upstream hop kernel --- experiments/lance-fold-hop/src/lib.rs | 191 ++++++++++---------------- 1 file changed, 73 insertions(+), 118 deletions(-) diff --git a/experiments/lance-fold-hop/src/lib.rs b/experiments/lance-fold-hop/src/lib.rs index 64c6aaeec..4c12e731a 100644 --- a/experiments/lance-fold-hop/src/lib.rs +++ b/experiments/lance-fold-hop/src/lib.rs @@ -12,64 +12,16 @@ #[cfg(feature = "lance")] pub mod lance { - use lance_graph_mask_risc::{ - execute_into, words_for, Foreign, ForeignPlane, LaneRef, MaskOp, Operand, - Out, Planes, Program, Scratch, Terminal, + use lance_graph_mask_risc::{words_for, Scratch}; + pub use lance_graph_mask_risc::{ + EdgeColumns, GraphSnapshot as Snapshot, HopError, OutgoingHop, }; - /// Edges are not a second graph object: these are borrowed SoA columns. - /// Row IDs are local positions, not canonical thought GUIDs. - #[derive(Clone, Copy)] - pub struct EdgeColumns<'a> { - pub sources: &'a [u32], - pub targets: &'a [u32], - } - - /// A selected witness version over one stable row order. - #[derive(Clone, Copy)] - pub struct Snapshot<'a> { - pub witness_version: u64, - pub node_rows: usize, - /// LSB-first, one bit per node. Scope authorization is already applied. - pub visible_nodes: &'a [u64], - /// LSB-first, one bit per edge, selected at this witness version. - pub live_edges: &'a [u64], - } - - #[derive(Debug, PartialEq, Eq)] - pub enum HopError { - StaleWitness, - InvalidShape, - InvalidSeed, - InvisibleSeed, - Executor(String), - } - - fn contains(mask: &[u64], row: usize) -> bool { - mask.get(row / 64) - .is_some_and(|word| (word & (1u64 << (row % 64))) != 0) - } - - fn valid_tail(mask: &[u64], rows: usize) -> bool { - if mask.len() != words_for(rows) { - return false; - } - if rows == 0 || rows % 64 == 0 { - return true; - } - mask.last() - .is_some_and(|word| (word >> (rows % 64)) == 0) - } - - /// Execute one scoped, witness-pinned outgoing adjacency fold. - /// - /// The only query evaluator is Mask-RISC: an edge-presence-gated Gather - /// from the source frontier, a visibility-gated Gather through target IDs, - /// and ScatterOrU32 into the *demanded* one-hop result plane. + /// Convenience shape for the fork's existing single-seed BFS differential. /// - /// An implementation of repeat/iterative BFS must add an explicit - /// continuation contract upstream rather than quietly reusing the result - /// as an undocumented intermediate. + /// All graph execution and authorization live in lance-graph's reusable + /// OutgoingHop kernel. This fork constructs only the requested seed mask, + /// owns temporary memory, and translates the single-seed error vocabulary. pub fn one_hop( edges: EdgeColumns<'_>, snapshot: Snapshot<'_>, @@ -79,75 +31,29 @@ pub mod lance { if snapshot.witness_version != expected_witness { return Err(HopError::StaleWitness); } - if snapshot.node_rows > u32::MAX as usize - || edges.sources.len() != edges.targets.len() - || !valid_tail(snapshot.visible_nodes, snapshot.node_rows) - || !valid_tail(snapshot.live_edges, edges.sources.len()) - || edges.sources.iter().chain(edges.targets.iter()).any( - |&id| id as usize >= snapshot.node_rows - ) - { - return Err(HopError::InvalidShape); - } + let kernel = OutgoingHop::new(edges.sources.len(), snapshot.node_rows)?; if seed >= snapshot.node_rows { return Err(HopError::InvalidSeed); } - if !contains(snapshot.visible_nodes, seed) { + if !snapshot.visible_nodes.get(seed / 64).is_some_and(|word| { + (word & (1u64 << (seed % 64))) != 0 + }) { return Err(HopError::InvisibleSeed); } - - let mut frontier = vec![0u64; words_for(snapshot.node_rows)]; + let mut frontier = vec![0; words_for(snapshot.node_rows)]; frontier[seed / 64] |= 1u64 << (seed % 64); - let mut targets = vec![0u64; words_for(snapshot.node_rows)]; - - if edges.sources.is_empty() { - return Ok(targets); - } - - let planes_masks = [snapshot.live_edges]; - let lanes = [ - LaneRef::U32(edges.sources), - LaneRef::U32(edges.targets), - ]; - let planes = Planes { - n_rows: edges.sources.len(), - masks: &planes_masks, - lanes: &lanes, - }; - let foreign_planes = [ - ForeignPlane { words: &frontier, rows: snapshot.node_rows }, - ForeignPlane { words: snapshot.visible_nodes, rows: snapshot.node_rows }, - ]; - let foreign = Foreign { planes: &foreign_planes, lanes: &[] }; - let program = Program::new( - vec![ - MaskOp::Gather { - lane: 0, - foreign: 0, - under: Some(Operand::Plane(0)), - dst: 0, - }, - MaskOp::Gather { - lane: 1, - foreign: 1, - under: Some(Operand::Scratch(0)), - dst: 1, - }, - ], - Terminal::ScatterOrU32 { - mask: Operand::Scratch(1), - lane: 1, - out_rows: snapshot.node_rows as u32, - }, - ); - let mut scratch = Scratch::for_program(&program, edges.sources.len()) - .map_err(|error| HopError::Executor(format!("{error:?}")))?; - execute_into( - &program, &planes, &foreign, &mut scratch, Out::Mask(&mut targets), - ) - .map_err(|error| HopError::Executor(format!("{error:?}")))?; - - Ok(targets) + let mut out = vec![0; words_for(snapshot.node_rows)]; + let mut scratch = Scratch::for_program(kernel.program(), edges.sources.len()) + .map_err(HopError::Executor)?; + kernel.run( + edges, + snapshot, + &frontier, + expected_witness, + &mut scratch, + &mut out, + )?; + Ok(out) } } @@ -297,4 +203,53 @@ mod tests { snapshot, 0, 1, ), Err(HopError::InvalidShape)); } + #[test] + fn multi_seed_bfs_layer_uses_upstream_kernel_and_reusable_scratch() { + use lance_graph_mask_risc::{OutgoingHop, Scratch}; + + let edges = [(0, 1), (0, 65), (65, 129), (65, 70)]; + let sources: Vec = edges.iter().map(|e| e.0).collect(); + let targets: Vec = edges.iter().map(|e| e.1).collect(); + let visible = mask(130, &(0..130).collect::>()); + let live = mask(edges.len(), &(0..edges.len()).collect::>()); + let frontier = mask(130, &[0, 65]); + let kernel = OutgoingHop::new(edges.len(), 130).unwrap(); + let mut scratch = Scratch::for_program(kernel.program(), edges.len()).unwrap(); + let mut out = vec![u64::MAX; words_for(130)]; + + let snapshot = Snapshot { + witness_version: 15, node_rows: 130, + visible_nodes: &visible, live_edges: &live, + }; + kernel.run( + EdgeColumns { sources: &sources, targets: &targets }, + snapshot, &frontier, 15, &mut scratch, &mut out, + ).unwrap(); + let want: BTreeSet = [0usize, 65usize].iter() + .flat_map(|&seed| { + petgraph_first_bfs_layer(130, &edges, &live, &visible, seed) + }) + .collect(); + assert_eq!(ids(&out, 130), want); + + // Reuse the same plan and scratch on a second witness. The third + // edge disappeared; stable node ordinals have not changed. + let modified_live = mask(edges.len(), &[0, 1, 3]); + let second = Snapshot { + witness_version: 16, live_edges: &modified_live, ..snapshot + }; + kernel.run( + EdgeColumns { sources: &sources, targets: &targets }, + second, &frontier, 16, &mut scratch, &mut out, + ).unwrap(); + let want: BTreeSet = [0usize, 65usize].iter() + .flat_map(|&seed| { + petgraph_first_bfs_layer( + 130, &edges, &modified_live, &visible, seed, + ) + }) + .collect(); + assert_eq!(ids(&out, 130), want); + } + } From 1c2fa1ee51013b247d82295a1200ebae57ebfd0b Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:32:19 +0200 Subject: [PATCH 2/6] fix(lance-fold): convert petgraph oracle ordinals to u32 lanes --- experiments/lance-fold-hop/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experiments/lance-fold-hop/src/lib.rs b/experiments/lance-fold-hop/src/lib.rs index 4c12e731a..36c80f897 100644 --- a/experiments/lance-fold-hop/src/lib.rs +++ b/experiments/lance-fold-hop/src/lib.rs @@ -208,8 +208,8 @@ mod tests { use lance_graph_mask_risc::{OutgoingHop, Scratch}; let edges = [(0, 1), (0, 65), (65, 129), (65, 70)]; - let sources: Vec = edges.iter().map(|e| e.0).collect(); - let targets: Vec = edges.iter().map(|e| e.1).collect(); + let sources: Vec = edges.iter().map(|e| e.0 as u32).collect(); + let targets: Vec = edges.iter().map(|e| e.1 as u32).collect(); let visible = mask(130, &(0..130).collect::>()); let live = mask(edges.len(), &(0..edges.len()).collect::>()); let frontier = mask(130, &[0, 65]); From 21780dd077326a1378b6fc495ff01bfdc6c47908 Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:32:22 +0200 Subject: [PATCH 3/6] build(lance-fold): pin Rust 1.99 MSRV for probe and upstream kernel SHA --- experiments/lance-fold-hop/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experiments/lance-fold-hop/Cargo.toml b/experiments/lance-fold-hop/Cargo.toml index 1ee6ff871..359f53dc5 100644 --- a/experiments/lance-fold-hop/Cargo.toml +++ b/experiments/lance-fold-hop/Cargo.toml @@ -2,6 +2,7 @@ name = "petgraph-lance-fold-hop" version = "0.0.1" edition = "2024" +rust-version = "1.99" publish = false description = "Fork-first differential one-hop execution on lance-graph Mask-RISC" @@ -15,4 +16,4 @@ lance = ["dep:lance-graph-mask-risc"] [dependencies] petgraph = { path = "../../crates/petgraph", default-features = false, features = ["std"] } -lance-graph-mask-risc = { git = "https://github.com/AdaWorldAPI/lance-graph", rev = "b10e46a134c8429b32bafe1d8a463678f8844816", optional = true } +lance-graph-mask-risc = { git = "https://github.com/AdaWorldAPI/lance-graph", rev = "66636da46107edcc3280237db088172f652cacd5", optional = true } From c374bc457e221d2c43ccf73b3b43e2a74f2a85d0 Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:32:24 +0200 Subject: [PATCH 4/6] ci(lance-fold): use Rust 1.99.0 consistently for cargo test --- .github/workflows/lance-fold-hop.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lance-fold-hop.yml b/.github/workflows/lance-fold-hop.yml index c8c48fca8..5933178a5 100644 --- a/.github/workflows/lance-fold-hop.yml +++ b/.github/workflows/lance-fold-hop.yml @@ -17,6 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: '1.99.0' + components: rustfmt, clippy - name: Test direct Mask-RISC hop against petgraph BFS run: cargo test --manifest-path experiments/lance-fold-hop/Cargo.toml --features lance --verbose From dae6005fb05c4038bf7e819ee77ebe4bde2d2d6e Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:32:59 +0200 Subject: [PATCH 5/6] build: pin fork development toolchain to Rust 1.99.0 --- rust-toolchain.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..f107793e0 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,8 @@ +[toolchain] +channel = "1.99.0" +components = ["rustfmt", "clippy"] +profile = "minimal" + +# This is the fork's tested development/CI toolchain, NOT the minimum +# supported Rust version of upstream petgraph. The latter remains 1.85 +# in Cargo.toml until an actual public API needs a newer compiler. From cd4dc4bfceb3c6983b75dc85cf20cf901e70287f Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Mon, 19 Oct 2026 17:33:03 +0200 Subject: [PATCH 6/6] docs(lance-fold): explain upstream fold consumption and Rust 1.99 Cargo pin --- experiments/lance-fold-hop/README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/experiments/lance-fold-hop/README.md b/experiments/lance-fold-hop/README.md index 86563c58f..f06dadead 100644 --- a/experiments/lance-fold-hop/README.md +++ b/experiments/lance-fold-hop/README.md @@ -10,9 +10,11 @@ Run: cargo test --manifest-path experiments/lance-fold-hop/Cargo.toml --features lance ``` -The optional dependency is pinned to a specific `lance-graph` git revision. -The standalone probe directly passes borrowed SoA edge columns and resident -snapshot masks to `lance-graph-mask-risc`: +The optional dependency is pinned to the specific `lance-graph` commit that introduces +`lance-graph-mask-risc::OutgoingHop`. The petgraph-side lowering has now been +replaced with a small wrapper over that upstream kernel. +The upstream kernel directly passes borrowed SoA edge columns and resident +snapshot masks into `lance-graph-mask-risc`: 1. `Gather(source, frontier)` gated by edge presence; 2. `Gather(target, visible)` gated by the surviving edges; @@ -21,7 +23,7 @@ snapshot masks to `lance-graph-mask-risc`: The oracle is petgraph's real `Bfs` advancing one step over a separately constructed graph **in tests only**. The runtime fold path does not create a petgraph `Graph`, neighbor vectors, an adjacency list, or a graph-trait -adapter. Six differential/security tests cover multiple mask words, duplicate +adapter. Seven differential/security tests cover multi-seed frontiers, multiple mask words, duplicate edges, invisible nodes, absent edges, distinct witness versions, unauthorized seeds, invalid tails, and stale witnesses. @@ -36,5 +38,12 @@ continuation primitive. Do not introduce a second interpreter or speculative `lance-graph-core` yet. Once Gremlin and petgraph both consume the same primitive, extract the smaller core if it actually reduces complexity. -This branch has **not** been compiled or benchmarked by the authoring agent; +Build with the fork's pinned **Rust/Cargo 1.99.0** toolchain; its optional +experiment declares `rust-version = "1.99"`. The upstream petgraph workspace +still advertises **MSRV 1.85**, since no upstream-compatible API needs a bump. +Do not run blanket `cargo upgrade`: the fork has no tracked root Cargo.lock, +and this experiment deliberately pins the lance-graph commit. Dependency +updates must be reviewed separately from a toolchain upgrade. + +This change has **not** been compiled or benchmarked by the authoring agent; CI and local Rust toolchains are required for validation.