From af81f9b1011e04367b3bf1368064e9cf43076075 Mon Sep 17 00:00:00 2001 From: Agustin Borgna <agustin.borgna@quantinuum.com> Date: Thu, 19 Oct 2023 09:24:02 +0100 Subject: [PATCH 1/3] Add sparse convex check benchmarks --- benches/benchmarks/convex.rs | 29 +++++++++++++++++++++++++---- src/algorithms/convex.rs | 10 +++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/benches/benchmarks/convex.rs b/benches/benchmarks/convex.rs index 7daf59f..2dfa1aa 100644 --- a/benches/benchmarks/convex.rs +++ b/benches/benchmarks/convex.rs @@ -1,4 +1,5 @@ use criterion::{black_box, criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; +use itertools::Itertools; use portgraph::{algorithms::ConvexChecker, PortView}; use super::generators::make_two_track_dag; @@ -22,15 +23,15 @@ fn bench_convex_construction(c: &mut Criterion) { /// We benchmark the worst case scenario, where the "subgraph" is the /// entire graph itself. -fn bench_convex(c: &mut Criterion) { - let mut g = c.benchmark_group("Runtime convexity check"); +fn bench_convex_full(c: &mut Criterion) { + let mut g = c.benchmark_group("Runtime convexity check. Full graph."); g.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); for size in [100, 1_000, 10_000] { let graph = make_two_track_dag(size); let mut checker = ConvexChecker::new(&graph); g.bench_with_input( - BenchmarkId::new("check_convexity", size), + BenchmarkId::new("check_convexity_full", size), &size, |b, _size| b.iter(|| black_box(checker.is_node_convex(graph.nodes_iter()))), ); @@ -38,10 +39,30 @@ fn bench_convex(c: &mut Criterion) { g.finish(); } +/// We benchmark the an scenario where the size of the "subgraph" is sub-linear on the size of the graph. +fn bench_convex_sparse(c: &mut Criterion) { + let mut g = c.benchmark_group("Runtime convexity check. Sparse subgraph on an n^2 size graph."); + g.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for size in [100usize, 1_000, 5_000] { + let graph_size = size.pow(2); + let graph = make_two_track_dag(graph_size); + let mut checker = ConvexChecker::new(&graph); + let nodes = graph.nodes_iter().step_by(graph_size / size).collect_vec(); + g.bench_with_input( + BenchmarkId::new("check_convexity_sparse", size), + &size, + |b, _size| b.iter(|| black_box(checker.is_node_convex(nodes.iter().copied()))), + ); + } + g.finish(); +} + criterion_group! { name = benches; config = Criterion::default(); targets = - bench_convex, + bench_convex_full, + bench_convex_sparse, bench_convex_construction } diff --git a/src/algorithms/convex.rs b/src/algorithms/convex.rs index 551de0a..c966b9a 100644 --- a/src/algorithms/convex.rs +++ b/src/algorithms/convex.rs @@ -14,14 +14,14 @@ use crate::{Direction, LinkView, NodeIndex, PortIndex, SecondaryMap, UnmanagedDe use super::TopoSort; -/// A pre-computed datastructure for fast convexity checking. +/// A pre-computed data structure for fast convexity checking. pub struct ConvexChecker<G> { graph: G, // The nodes in topological order topsort_nodes: Vec<NodeIndex>, // The index of a node in the topological order (the inverse of topsort_nodes) topsort_ind: UnmanagedDenseMap<NodeIndex, usize>, - // A temporary datastructure used during `is_convex` + // A temporary data structure used during `is_convex` causal: CausalVec, } @@ -60,7 +60,7 @@ where /// past and in the future of another node of the subgraph. /// /// This function requires mutable access to `self` because it uses a - /// temporary datastructure within the object. + /// temporary data structure within the object. /// /// ## Arguments /// @@ -111,7 +111,7 @@ where /// Whether a subgraph is convex. /// /// A subgraph is convex if there is no path between two nodes of the - /// sugraph that has an edge outside of the subgraph. + /// subgraph that has an edge outside of the subgraph. /// /// Equivalently, we check the following two conditions: /// - There is no node that is both in the past and in the future of @@ -119,7 +119,7 @@ where /// - There is no edge from an output port to an input port. /// /// This function requires mutable access to `self` because it uses a - /// temporary datastructure within the object. + /// temporary data structure within the object. /// /// ## Arguments /// From 5ff42a4bfbddaa3bc6dead7ea9d7c36a03ecc37d Mon Sep 17 00:00:00 2001 From: Agustin Borgna <agustin.borgna@quantinuum.com> Date: Thu, 19 Oct 2023 10:36:28 +0100 Subject: [PATCH 2/3] Reduce the number of nodes explored in is_node_convex By using the toposorted order and keeping track of nodes in the causal future. --- src/algorithms/convex.rs | 108 +++++++++++++++------------------------ 1 file changed, 40 insertions(+), 68 deletions(-) diff --git a/src/algorithms/convex.rs b/src/algorithms/convex.rs index c966b9a..3ad92a6 100644 --- a/src/algorithms/convex.rs +++ b/src/algorithms/convex.rs @@ -6,9 +6,6 @@ use std::collections::BTreeSet; -use bitvec::bitvec; -use bitvec::vec::BitVec; - use crate::algorithms::toposort; use crate::{Direction, LinkView, NodeIndex, PortIndex, SecondaryMap, UnmanagedDenseMap}; @@ -21,8 +18,6 @@ pub struct ConvexChecker<G> { topsort_nodes: Vec<NodeIndex>, // The index of a node in the topological order (the inverse of topsort_nodes) topsort_ind: UnmanagedDenseMap<NodeIndex, usize>, - // A temporary data structure used during `is_convex` - causal: CausalVec, } impl<G> ConvexChecker<G> @@ -40,12 +35,10 @@ where for (i, &n) in topsort_nodes.iter().enumerate() { topsort_ind.set(n, i); } - let causal = CausalVec::new(topsort_nodes.len()); Self { graph, topsort_nodes, topsort_ind, - causal, } } @@ -78,31 +71,53 @@ where /// in some topological order. In the worst case this will traverse every /// node in the graph and can be improved on in the future. pub fn is_node_convex(&mut self, nodes: impl IntoIterator<Item = NodeIndex>) -> bool { + // The nodes in the subgraph, in topological order. let nodes: BTreeSet<_> = nodes.into_iter().map(|n| self.topsort_ind[n]).collect(); + if nodes.is_empty() { + return true; + } + + // The range of considered nodes, as positions in the toposorted vector. + // Since the nodes are ordered, any node outside of this range will + // necessarily be outside the convex hull. let min_ind = *nodes.first().unwrap(); let max_ind = *nodes.last().unwrap(); - for ind in min_ind..=max_ind { - let n = self.topsort_nodes[ind]; - let mut in_inds = { - let in_neighs = self.graph.input_neighbours(n); - in_neighs + let node_range = min_ind..=max_ind; + + let mut node_iter = nodes.iter().copied().peekable(); + + // Nodes in the causal future of `nodes` (inside `node_range`). + let mut other_nodes = BTreeSet::new(); + + loop { + if node_iter.peek().is_none() { + break; + } + if other_nodes.is_empty() || node_iter.peek() < other_nodes.first() { + let current = node_iter.next().unwrap(); + let current_node = self.topsort_nodes[current]; + for neighbour in self.graph.output_neighbours(current_node) .map(|n| self.topsort_ind[n]) - .filter(|&ind| ind >= min_ind) - }; - if nodes.contains(&ind) { - if in_inds.any(|ind| self.causal.get(ind) == Causal::Future) { - // There is a node in the past that is also in the future! - return false; + .filter(|ind| node_range.contains(ind)) + { + if !nodes.contains(&neighbour) { + other_nodes.insert(neighbour); + } } - self.causal.set(ind, Causal::Past); } else { - let ind_causal = match in_inds - .any(|ind| nodes.contains(&ind) || self.causal.get(ind) == Causal::Future) + let current = other_nodes.pop_first().unwrap(); + let current_node = self.topsort_nodes[current]; + for neighbour in self.graph.output_neighbours(current_node) + .map(|n| self.topsort_ind[n]) + .filter(|ind| node_range.contains(ind)) { - true => Causal::Future, - false => Causal::Past, - }; - self.causal.set(ind, ind_causal); + if nodes.contains(&neighbour) { + // A non-subgraph node in the causal future of the subgraph has an output neighbour in the subgraph. + return false; + } else { + other_nodes.insert(neighbour); + } + } } } true @@ -148,49 +163,6 @@ where } } -/// Whether a node is in the past or in the future of a subgraph. -#[derive(Default, Clone, Debug, PartialEq, Eq)] -enum Causal { - #[default] - Past, - Future, -} - -/// A memory-efficient substitute for `Vec<Causal>`. -struct CausalVec(BitVec); - -impl From<bool> for Causal { - fn from(b: bool) -> Self { - match b { - true => Self::Future, - false => Self::Past, - } - } -} - -impl From<Causal> for bool { - fn from(c: Causal) -> Self { - match c { - Causal::Past => false, - Causal::Future => true, - } - } -} - -impl CausalVec { - fn new(len: usize) -> Self { - Self(bitvec![0; len]) - } - - fn set(&mut self, index: usize, causal: Causal) { - self.0.set(index, causal.into()); - } - - fn get(&self, index: usize) -> Causal { - self.0[index].into() - } -} - #[cfg(test)] mod tests { use crate::{LinkMut, NodeIndex, PortGraph, PortMut, PortView}; From 2c2dfa2defe51c0abbe064475a98950d224df243 Mon Sep 17 00:00:00 2001 From: Agustin Borgna <agustin.borgna@quantinuum.com> Date: Thu, 19 Oct 2023 20:09:59 +0100 Subject: [PATCH 3/3] convexity checks no longer require &mut --- benches/benchmarks/convex.rs | 4 ++-- src/algorithms/convex.rs | 20 ++++++++++++-------- src/view/subgraph.rs | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/benches/benchmarks/convex.rs b/benches/benchmarks/convex.rs index 2dfa1aa..237a658 100644 --- a/benches/benchmarks/convex.rs +++ b/benches/benchmarks/convex.rs @@ -29,7 +29,7 @@ fn bench_convex_full(c: &mut Criterion) { for size in [100, 1_000, 10_000] { let graph = make_two_track_dag(size); - let mut checker = ConvexChecker::new(&graph); + let checker = ConvexChecker::new(&graph); g.bench_with_input( BenchmarkId::new("check_convexity_full", size), &size, @@ -47,7 +47,7 @@ fn bench_convex_sparse(c: &mut Criterion) { for size in [100usize, 1_000, 5_000] { let graph_size = size.pow(2); let graph = make_two_track_dag(graph_size); - let mut checker = ConvexChecker::new(&graph); + let checker = ConvexChecker::new(&graph); let nodes = graph.nodes_iter().step_by(graph_size / size).collect_vec(); g.bench_with_input( BenchmarkId::new("check_convexity_sparse", size), diff --git a/src/algorithms/convex.rs b/src/algorithms/convex.rs index 3ad92a6..b034139 100644 --- a/src/algorithms/convex.rs +++ b/src/algorithms/convex.rs @@ -70,7 +70,7 @@ where /// that are in the interval between the first and last node of the subgraph /// in some topological order. In the worst case this will traverse every /// node in the graph and can be improved on in the future. - pub fn is_node_convex(&mut self, nodes: impl IntoIterator<Item = NodeIndex>) -> bool { + pub fn is_node_convex(&self, nodes: impl IntoIterator<Item = NodeIndex>) -> bool { // The nodes in the subgraph, in topological order. let nodes: BTreeSet<_> = nodes.into_iter().map(|n| self.topsort_ind[n]).collect(); if nodes.is_empty() { @@ -96,7 +96,9 @@ where if other_nodes.is_empty() || node_iter.peek() < other_nodes.first() { let current = node_iter.next().unwrap(); let current_node = self.topsort_nodes[current]; - for neighbour in self.graph.output_neighbours(current_node) + for neighbour in self + .graph + .output_neighbours(current_node) .map(|n| self.topsort_ind[n]) .filter(|ind| node_range.contains(ind)) { @@ -107,7 +109,9 @@ where } else { let current = other_nodes.pop_first().unwrap(); let current_node = self.topsort_nodes[current]; - for neighbour in self.graph.output_neighbours(current_node) + for neighbour in self + .graph + .output_neighbours(current_node) .map(|n| self.topsort_ind[n]) .filter(|ind| node_range.contains(ind)) { @@ -147,7 +151,7 @@ where /// Any edge between two nodes of the subgraph that does not have an explicit /// input or output port is considered within the subgraph. pub fn is_convex( - &mut self, + &self, nodes: impl IntoIterator<Item = NodeIndex>, inputs: impl IntoIterator<Item = PortIndex>, outputs: impl IntoIterator<Item = PortIndex>, @@ -197,7 +201,7 @@ mod tests { #[test] fn induced_convexity_test() { let (g, [i1, i2, i3, n1, n2, o1, o2]) = graph(); - let mut checker = ConvexChecker::new(&g); + let checker = ConvexChecker::new(&g); assert!(checker.is_node_convex([i1, i2, i3])); assert!(checker.is_node_convex([i1, n2])); @@ -212,7 +216,7 @@ mod tests { #[test] fn edge_convexity_test() { let (g, [i1, i2, _, n1, n2, _, o2]) = graph(); - let mut checker = ConvexChecker::new(&g); + let checker = ConvexChecker::new(&g); assert!(checker.is_convex( [i1, n2], @@ -245,7 +249,7 @@ mod tests { fn dangling_input() { let mut g = PortGraph::new(); let n = g.add_node(1, 1); - let mut checker = ConvexChecker::new(&g); + let checker = ConvexChecker::new(&g); assert!(checker.is_node_convex([n])); } @@ -254,7 +258,7 @@ mod tests { let mut g = PortGraph::new(); let n = g.add_node(1, 1); g.add_node(1, 1); - let mut checker = ConvexChecker::new(&g); + let checker = ConvexChecker::new(&g); assert!(checker.is_node_convex([n])); } } diff --git a/src/view/subgraph.rs b/src/view/subgraph.rs index c7ef184..cc82b43 100644 --- a/src/view/subgraph.rs +++ b/src/view/subgraph.rs @@ -96,12 +96,12 @@ where /// Whether the subgraph is convex. pub fn is_convex(&self) -> bool { - let mut checker = ConvexChecker::new(self.graph()); - self.is_convex_with_checker(&mut checker) + let checker = ConvexChecker::new(self.graph()); + self.is_convex_with_checker(&checker) } /// Whether the subgraph is convex, using a pre-existing checker. - pub fn is_convex_with_checker(&self, checker: &mut ConvexChecker<G>) -> bool { + pub fn is_convex_with_checker(&self, checker: &ConvexChecker<G>) -> bool { checker.is_convex( self.nodes_iter(), self.context().inputs.iter().copied(),